forked from AkkomaGang/akkoma-fe
Compare commits
156 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 |
451 changed files with 15233 additions and 9511 deletions
6
.babelrc
6
.babelrc
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env"],
|
"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
|
"comments": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,9 @@ module.exports = {
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
},
|
},
|
||||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||||
extends: [
|
extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
|
||||||
'plugin:vue/recommended'
|
|
||||||
],
|
|
||||||
// required to lint *.vue files
|
// required to lint *.vue files
|
||||||
plugins: [
|
plugins: ['vue', 'import'],
|
||||||
'vue',
|
|
||||||
'import'
|
|
||||||
],
|
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
rules: {
|
rules: {
|
||||||
// allow paren-less arrow functions
|
// 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."
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"singleAttributePerLine": true
|
||||||
|
}
|
12
README.md
12
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)
|
![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)
|
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
|
||||||
- Custom emoji reactions
|
- Custom emoji reactions
|
||||||
|
|
||||||
# For Translators
|
# 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.
|
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
|
# 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
|
## Build Setup
|
||||||
|
|
||||||
|
@ -52,4 +52,4 @@ Edit config.json for configuration.
|
||||||
|
|
||||||
### Login methods
|
### Login methods
|
||||||
|
|
||||||
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
|
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
|
||||||
|
|
|
@ -11,14 +11,17 @@ var webpackConfig = require('./webpack.prod.conf')
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
' Tip:\n' +
|
' Tip:\n' +
|
||||||
' Built files are meant to be served over an HTTP server.\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...')
|
var spinner = ora('building for production...')
|
||||||
spinner.start()
|
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)
|
rm('-rf', assetsPath)
|
||||||
mkdir('-p', assetsPath)
|
mkdir('-p', assetsPath)
|
||||||
cp('-R', 'static/*', assetsPath)
|
cp('-R', 'static/*', assetsPath)
|
||||||
|
@ -26,11 +29,13 @@ cp('-R', 'static/*', assetsPath)
|
||||||
webpack(webpackConfig, function (err, stats) {
|
webpack(webpackConfig, function (err, stats) {
|
||||||
spinner.stop()
|
spinner.stop()
|
||||||
if (err) throw err
|
if (err) throw err
|
||||||
process.stdout.write(stats.toString({
|
process.stdout.write(
|
||||||
colors: true,
|
stats.toString({
|
||||||
modules: false,
|
colors: true,
|
||||||
children: false,
|
modules: false,
|
||||||
chunks: false,
|
children: false,
|
||||||
chunkModules: false
|
chunks: false,
|
||||||
}) + '\n')
|
chunkModules: false
|
||||||
|
}) + '\n'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,8 +2,7 @@ var semver = require('semver')
|
||||||
var chalk = require('chalk')
|
var chalk = require('chalk')
|
||||||
var packageConfig = require('../package.json')
|
var packageConfig = require('../package.json')
|
||||||
var exec = function (cmd) {
|
var exec = function (cmd) {
|
||||||
return require('child_process')
|
return require('child_process').execSync(cmd).toString().trim()
|
||||||
.execSync(cmd).toString().trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var versionRequirements = [
|
var versionRequirements = [
|
||||||
|
@ -24,16 +23,23 @@ module.exports = function () {
|
||||||
for (var i = 0; i < versionRequirements.length; i++) {
|
for (var i = 0; i < versionRequirements.length; i++) {
|
||||||
var mod = versionRequirements[i]
|
var mod = versionRequirements[i]
|
||||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||||
warnings.push(mod.name + ': ' +
|
warnings.push(
|
||||||
chalk.red(mod.currentVersion) + ' should be ' +
|
mod.name +
|
||||||
chalk.green(mod.versionRequirement)
|
': ' +
|
||||||
|
chalk.red(mod.currentVersion) +
|
||||||
|
' should be ' +
|
||||||
|
chalk.green(mod.versionRequirement)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (warnings.length) {
|
if (warnings.length) {
|
||||||
console.log('')
|
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()
|
console.log()
|
||||||
for (var i = 0; i < warnings.length; i++) {
|
for (var i = 0; i < warnings.length; i++) {
|
||||||
var warning = warnings[i]
|
var warning = warnings[i]
|
||||||
|
|
|
@ -6,9 +6,10 @@ var express = require('express')
|
||||||
var webpack = require('webpack')
|
var webpack = require('webpack')
|
||||||
var opn = require('opn')
|
var opn = require('opn')
|
||||||
var proxyMiddleware = require('http-proxy-middleware')
|
var proxyMiddleware = require('http-proxy-middleware')
|
||||||
var webpackConfig = process.env.NODE_ENV === 'testing'
|
var webpackConfig =
|
||||||
? require('./webpack.prod.conf')
|
process.env.NODE_ENV === 'testing'
|
||||||
: require('./webpack.dev.conf')
|
? require('./webpack.prod.conf')
|
||||||
|
: require('./webpack.dev.conf')
|
||||||
|
|
||||||
// default port where dev server listens for incoming traffic
|
// default port where dev server listens for incoming traffic
|
||||||
var port = process.env.PORT || config.dev.port
|
var port = process.env.PORT || config.dev.port
|
||||||
|
@ -50,7 +51,10 @@ app.use(devMiddleware)
|
||||||
app.use(hotMiddleware)
|
app.use(hotMiddleware)
|
||||||
|
|
||||||
// serve pure static assets
|
// 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'))
|
app.use(staticPath, express.static('./static'))
|
||||||
|
|
||||||
module.exports = app.listen(port, function (err) {
|
module.exports = app.listen(port, function (err) {
|
||||||
|
|
|
@ -4,7 +4,8 @@ var sass = require('sass')
|
||||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
|
|
||||||
exports.assetsPath = function (_path) {
|
exports.assetsPath = function (_path) {
|
||||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
var assetsSubDirectory =
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
? config.build.assetsSubDirectory
|
? config.build.assetsSubDirectory
|
||||||
: config.dev.assetsSubDirectory
|
: config.dev.assetsSubDirectory
|
||||||
return path.posix.join(assetsSubDirectory, _path)
|
return path.posix.join(assetsSubDirectory, _path)
|
||||||
|
@ -13,7 +14,7 @@ exports.assetsPath = function (_path) {
|
||||||
exports.cssLoaders = function (options) {
|
exports.cssLoaders = function (options) {
|
||||||
options = options || {}
|
options = options || {}
|
||||||
|
|
||||||
function generateLoaders (loaders) {
|
function generateLoaders(loaders) {
|
||||||
// Extract CSS when that option is specified
|
// Extract CSS when that option is specified
|
||||||
// (which is the case during production build)
|
// (which is the case during production build)
|
||||||
if (options.extract) {
|
if (options.extract) {
|
||||||
|
@ -27,11 +28,11 @@ exports.cssLoaders = function (options) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
test: /\.(post)?css$/,
|
test: /\.(post)?css$/,
|
||||||
use: generateLoaders(['css-loader', 'postcss-loader']),
|
use: generateLoaders(['css-loader', 'postcss-loader'])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.less$/,
|
test: /\.less$/,
|
||||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']),
|
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader'])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.sass$/,
|
test: /\.sass$/,
|
||||||
|
@ -52,8 +53,8 @@ exports.cssLoaders = function (options) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.styl(us)?$/,
|
test: /\.styl(us)?$/,
|
||||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
|
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader'])
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ var { VueLoaderPlugin } = require('vue-loader')
|
||||||
var env = process.env.NODE_ENV
|
var env = process.env.NODE_ENV
|
||||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
// 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
|
// various preprocessor loaders added to vue-loader at the end of this file
|
||||||
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
|
var cssSourceMapDev = env === 'development' && config.dev.cssSourceMap
|
||||||
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
|
var cssSourceMapProd = env === 'production' && config.build.productionSourceMap
|
||||||
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
|
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
|
||||||
|
|
||||||
var now = Date.now()
|
var now = Date.now()
|
||||||
|
@ -18,9 +18,12 @@ module.exports = {
|
||||||
app: './src/main.js'
|
app: './src/main.js'
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
|
hashFunction: 'sha256', // Workaround for builds with OpenSSL 3.
|
||||||
path: config.build.assetsRoot,
|
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'
|
filename: '[name].js'
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
|
@ -30,17 +33,15 @@ module.exports = {
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx', '.vue', '.mjs'],
|
extensions: ['.js', '.jsx', '.vue', '.mjs'],
|
||||||
modules: [
|
modules: [path.join(__dirname, '../node_modules')],
|
||||||
path.join(__dirname, '../node_modules')
|
|
||||||
],
|
|
||||||
fallback: {
|
fallback: {
|
||||||
"url": require.resolve("url/"),
|
url: require.resolve('url/')
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
'static': path.resolve(__dirname, '../static'),
|
static: path.resolve(__dirname, '../static'),
|
||||||
'src': path.resolve(__dirname, '../src'),
|
src: path.resolve(__dirname, '../src'),
|
||||||
'assets': path.resolve(__dirname, '../src/assets'),
|
assets: path.resolve(__dirname, '../src/assets'),
|
||||||
'components': path.resolve(__dirname, '../src/components'),
|
components: path.resolve(__dirname, '../src/components'),
|
||||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -66,14 +67,15 @@ module.exports = {
|
||||||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||||
type: 'javascript/auto',
|
type: 'javascript/auto',
|
||||||
loader: '@intlify/vue-i18n-loader',
|
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')
|
path.resolve(__dirname, '../src/i18n')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.mjs$/,
|
test: /\.mjs$/,
|
||||||
include: /node_modules/,
|
include: /node_modules/,
|
||||||
type: "javascript/auto"
|
type: 'javascript/auto'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
|
@ -114,10 +116,8 @@ module.exports = {
|
||||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [new VueLoaderPlugin()]
|
||||||
new VueLoaderPlugin()
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,9 @@ var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
|
||||||
// add hot-reload related code to entry chunks
|
// add hot-reload related code to entry chunks
|
||||||
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
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, {
|
module.exports = merge(baseWebpackConfig, {
|
||||||
|
@ -20,10 +22,10 @@ module.exports = merge(baseWebpackConfig, {
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': config.dev.env,
|
'process.env': config.dev.env,
|
||||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
COMMIT_HASH: JSON.stringify('DEV'),
|
||||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
DEV_OVERRIDES: JSON.stringify(config.dev.settings),
|
||||||
'__VUE_OPTIONS_API__': true,
|
__VUE_OPTIONS_API__: true,
|
||||||
'__VUE_PROD_DEVTOOLS__': false
|
__VUE_PROD_DEVTOOLS__: false
|
||||||
}),
|
}),
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
|
|
@ -2,23 +2,27 @@ var path = require('path')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var webpack = require('webpack')
|
var webpack = require('webpack')
|
||||||
const WorkboxPlugin = require('workbox-webpack-plugin');
|
const WorkboxPlugin = require('workbox-webpack-plugin')
|
||||||
var { merge } = require('webpack-merge')
|
var { merge } = require('webpack-merge')
|
||||||
var baseWebpackConfig = require('./webpack.base.conf')
|
var baseWebpackConfig = require('./webpack.base.conf')
|
||||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
var HtmlWebpackPlugin = require('html-webpack-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')
|
? require('../config/test.env')
|
||||||
: config.build.env
|
: config.build.env
|
||||||
|
|
||||||
let commitHash = require('child_process')
|
let commitHash = require('child_process')
|
||||||
.execSync('git rev-parse --short HEAD')
|
.execSync('git rev-parse --short HEAD')
|
||||||
.toString();
|
.toString()
|
||||||
|
|
||||||
var webpackConfig = merge(baseWebpackConfig, {
|
var webpackConfig = merge(baseWebpackConfig, {
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
module: {
|
module: {
|
||||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
rules: utils.styleLoaders({
|
||||||
|
sourceMap: config.dev.cssSourceMap,
|
||||||
|
extract: true
|
||||||
|
})
|
||||||
},
|
},
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
optimization: {
|
optimization: {
|
||||||
|
@ -36,15 +40,15 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
new WorkboxPlugin.InjectManifest({
|
new WorkboxPlugin.InjectManifest({
|
||||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||||
swDest: 'sw-pleroma.js',
|
swDest: 'sw-pleroma.js',
|
||||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024
|
||||||
}),
|
}),
|
||||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': env,
|
'process.env': env,
|
||||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
COMMIT_HASH: JSON.stringify(commitHash),
|
||||||
'DEV_OVERRIDES': JSON.stringify(undefined),
|
DEV_OVERRIDES: JSON.stringify(undefined),
|
||||||
'__VUE_OPTIONS_API__': true,
|
__VUE_OPTIONS_API__: true,
|
||||||
'__VUE_PROD_DEVTOOLS__': false
|
__VUE_PROD_DEVTOOLS__: false
|
||||||
}),
|
}),
|
||||||
// extract css into its own file
|
// extract css into its own file
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
|
@ -54,9 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
// you can customize output by editing /index.html
|
// you can customize output by editing /index.html
|
||||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: process.env.NODE_ENV === 'testing'
|
filename:
|
||||||
? 'index.html'
|
process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index,
|
||||||
: config.build.index,
|
|
||||||
template: 'index.html',
|
template: 'index.html',
|
||||||
inject: true,
|
inject: true,
|
||||||
minify: {
|
minify: {
|
||||||
|
@ -69,7 +72,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
},
|
},
|
||||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||||
chunksSortMode: 'auto'
|
chunksSortMode: 'auto'
|
||||||
}),
|
})
|
||||||
// split vendor js into its own file
|
// split vendor js into its own file
|
||||||
// extract webpack runtime and module manifest to its own file in order to
|
// extract webpack runtime and module manifest to its own file in order to
|
||||||
// prevent vendor hash from being updated whenever app bundle is updated
|
// prevent vendor hash from being updated whenever app bundle is updated
|
||||||
|
@ -87,9 +90,7 @@ if (config.build.productionGzip) {
|
||||||
asset: '[path].gz[query]',
|
asset: '[path].gz[query]',
|
||||||
algorithm: 'gzip',
|
algorithm: 'gzip',
|
||||||
test: new RegExp(
|
test: new RegExp(
|
||||||
'\\.(' +
|
'\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
|
||||||
config.build.productionGzipExtensions.join('|') +
|
|
||||||
')$'
|
|
||||||
),
|
),
|
||||||
threshold: 10240,
|
threshold: 10240,
|
||||||
minRatio: 0.8
|
minRatio: 0.8
|
||||||
|
|
|
@ -38,6 +38,11 @@ module.exports = {
|
||||||
assetsSubDirectory: 'static',
|
assetsSubDirectory: 'static',
|
||||||
assetsPublicPath: '/',
|
assetsPublicPath: '/',
|
||||||
proxyTable: {
|
proxyTable: {
|
||||||
|
'/manifest.json': {
|
||||||
|
target,
|
||||||
|
changeOrigin: true,
|
||||||
|
cookieDomainRewrite: 'localhost'
|
||||||
|
},
|
||||||
'/api': {
|
'/api': {
|
||||||
target,
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
@ -54,7 +59,7 @@ module.exports = {
|
||||||
cookieDomainRewrite: 'localhost',
|
cookieDomainRewrite: 'localhost',
|
||||||
ws: true,
|
ws: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Origin': target
|
Origin: target
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'/oauth/revoke': {
|
'/oauth/revoke': {
|
||||||
|
@ -71,7 +76,7 @@ module.exports = {
|
||||||
target,
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
cookieDomainRewrite: 'localhost'
|
cookieDomainRewrite: 'localhost'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||||
// with this option, according to the CSS-Loader README
|
// with this option, according to the CSS-Loader README
|
||||||
|
|
40
index.html
40
index.html
|
@ -1,21 +1,25 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
<meta charset="utf-8" />
|
||||||
<title>Akkoma</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no" />
|
||||||
<link rel="stylesheet" href="/static/font/css/fontello.css">
|
<title>Akkoma</title>
|
||||||
<link rel="stylesheet" href="/static/font/css/animation.css">
|
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||||
<link rel="stylesheet" href="/static/font/tiresias.css">
|
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
<link rel="stylesheet" href="/static/mfm.css">
|
||||||
<link rel="stylesheet" href="/static/mfm.css">
|
<link rel="stylesheet" href="/static/custom.css">
|
||||||
<!--server-generated-meta-->
|
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<!--server-generated-meta-->
|
||||||
</head>
|
<link rel="icon" type="image/png" href="/favicon.svg" />
|
||||||
<body class="hidden">
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
</head>
|
||||||
<div id="app"></div>
|
|
||||||
<div id="modal"></div>
|
<body class="hidden">
|
||||||
<!-- built files will be auto injected -->
|
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
||||||
</body>
|
<div id="app"></div>
|
||||||
|
<div id="modal"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
15
package.json
15
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "pleroma_fe",
|
"name": "pleroma_fe",
|
||||||
"version": "3.2.0",
|
"version": "3.5.0",
|
||||||
"description": "A frontend for Akkoma instances",
|
"description": "A frontend for Akkoma instances",
|
||||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -18,19 +18,21 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.17.8",
|
"@babel/runtime": "7.17.8",
|
||||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||||
|
"@floatingghost/pinch-zoom-element": "^1.3.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
|
||||||
"@vuelidate/core": "^2.0.0",
|
"@vuelidate/core": "^2.0.0",
|
||||||
"@vuelidate/validators": "^2.0.0",
|
"@vuelidate/validators": "^2.0.0",
|
||||||
|
"blurhash": "^2.0.4",
|
||||||
"body-scroll-lock": "2.7.1",
|
"body-scroll-lock": "2.7.1",
|
||||||
"chromatism": "3.0.0",
|
"chromatism": "3.0.0",
|
||||||
"click-outside-vue3": "4.0.1",
|
"click-outside-vue3": "4.0.1",
|
||||||
"cropperjs": "1.5.12",
|
"cropperjs": "1.5.12",
|
||||||
"diff": "3.5.0",
|
"diff": "3.5.0",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
|
"iso-639-1": "^2.1.15",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"parse-link-header": "^2.0.0",
|
"parse-link-header": "^2.0.0",
|
||||||
|
@ -56,7 +58,7 @@
|
||||||
"@vue/babel-plugin-jsx": "1.1.1",
|
"@vue/babel-plugin-jsx": "1.1.1",
|
||||||
"@vue/compiler-sfc": "^3.1.0",
|
"@vue/compiler-sfc": "^3.1.0",
|
||||||
"@vue/test-utils": "^2.0.2",
|
"@vue/test-utils": "^2.0.2",
|
||||||
"autoprefixer": "6.7.7",
|
"autoprefixer": "^10.4.13",
|
||||||
"babel-loader": "^9.1.0",
|
"babel-loader": "^9.1.0",
|
||||||
"babel-plugin-lodash": "3.3.4",
|
"babel-plugin-lodash": "3.3.4",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
|
@ -67,11 +69,13 @@
|
||||||
"css-loader": "^6.7.2",
|
"css-loader": "^6.7.2",
|
||||||
"custom-event-polyfill": "^1.0.7",
|
"custom-event-polyfill": "^1.0.7",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-config-standard": "^17.0.0",
|
"eslint-config-standard": "^17.0.0",
|
||||||
"eslint-friendly-formatter": "^4.0.1",
|
"eslint-friendly-formatter": "^4.0.1",
|
||||||
"eslint-loader": "^4.0.2",
|
"eslint-loader": "^4.0.2",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-promise": "^6.1.1",
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
"eslint-plugin-standard": "^5.0.0",
|
"eslint-plugin-standard": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.7.0",
|
"eslint-plugin-vue": "^9.7.0",
|
||||||
|
@ -82,7 +86,6 @@
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"http-proxy-middleware": "0.21.0",
|
"http-proxy-middleware": "0.21.0",
|
||||||
"inject-loader": "2.0.1",
|
"inject-loader": "2.0.1",
|
||||||
"iso-639-1": "2.1.15",
|
|
||||||
"isparta-loader": "2.0.0",
|
"isparta-loader": "2.0.0",
|
||||||
"json-loader": "0.5.7",
|
"json-loader": "0.5.7",
|
||||||
"karma": "6.3.17",
|
"karma": "6.3.17",
|
||||||
|
@ -101,9 +104,11 @@
|
||||||
"nightwatch": "0.9.21",
|
"nightwatch": "0.9.21",
|
||||||
"opn": "4.0.2",
|
"opn": "4.0.2",
|
||||||
"ora": "0.4.1",
|
"ora": "0.4.1",
|
||||||
|
"postcss": "^8.4.19",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-loader": "^7.0.2",
|
||||||
"postcss-sass": "^0.5.0",
|
"postcss-sass": "^0.5.0",
|
||||||
|
"prettier": "2.8.1",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"sass": "^1.56.0",
|
"sass": "^1.56.0",
|
||||||
"sass-loader": "^13.2.0",
|
"sass-loader": "^13.2.0",
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [require('autoprefixer')]
|
||||||
require('autoprefixer')
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:base"]
|
||||||
"config:base"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
83
src/App.js
83
src/App.js
|
@ -24,7 +24,9 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
UserPanel,
|
UserPanel,
|
||||||
NavPanel,
|
NavPanel,
|
||||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
Notifications: defineAsyncComponent(() =>
|
||||||
|
import('./components/notifications/notifications.vue')
|
||||||
|
),
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
|
@ -44,17 +46,20 @@ export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline'
|
mobileActivePanel: 'timeline'
|
||||||
}),
|
}),
|
||||||
created () {
|
created() {
|
||||||
// Load the locale from the storage
|
// Load the locale from the storage
|
||||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
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)
|
window.addEventListener('resize', this.updateMobileState)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted() {
|
||||||
window.removeEventListener('resize', this.updateMobileState)
|
window.removeEventListener('resize', this.updateMobileState)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
classes () {
|
classes() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'-reverse': this.reverseLayout,
|
'-reverse': this.reverseLayout,
|
||||||
|
@ -64,48 +69,76 @@ export default {
|
||||||
'-' + this.layoutType
|
'-' + this.layoutType
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser() {
|
||||||
userBackground () { return this.currentUser.background_image },
|
return this.$store.state.users.currentUser
|
||||||
instanceBackground () {
|
},
|
||||||
|
userBackground() {
|
||||||
|
return this.currentUser.background_image
|
||||||
|
},
|
||||||
|
instanceBackground() {
|
||||||
return this.mergedConfig.hideInstanceWallpaper
|
return this.mergedConfig.hideInstanceWallpaper
|
||||||
? null
|
? null
|
||||||
: this.$store.state.instance.background
|
: this.$store.state.instance.background
|
||||||
},
|
},
|
||||||
background () { return this.userBackground || this.instanceBackground },
|
background() {
|
||||||
bgStyle () {
|
return this.userBackground || this.instanceBackground
|
||||||
|
},
|
||||||
|
bgStyle() {
|
||||||
if (this.background) {
|
if (this.background) {
|
||||||
return {
|
return {
|
||||||
'--body-background-image': `url(${this.background})`
|
'--body-background-image': `url(${this.background})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
suggestionsEnabled() {
|
||||||
showInstanceSpecificPanel () {
|
return this.$store.state.instance.suggestionsEnabled
|
||||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
},
|
||||||
|
showInstanceSpecificPanel() {
|
||||||
|
return (
|
||||||
|
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||||
!this.$store.getters.mergedConfig.hideISP &&
|
!this.$store.getters.mergedConfig.hideISP &&
|
||||||
this.$store.state.instance.instanceSpecificPanelContent
|
this.$store.state.instance.instanceSpecificPanelContent
|
||||||
|
)
|
||||||
},
|
},
|
||||||
newPostButtonShown () {
|
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 },
|
showFeaturesPanel() {
|
||||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
return this.$store.state.instance.showFeaturesPanel
|
||||||
layoutType () { return this.$store.state.interface.layoutType },
|
},
|
||||||
privateMode () { return this.$store.state.instance.private },
|
editingAvailable() {
|
||||||
reverseLayout () {
|
return this.$store.state.instance.editingAvailable
|
||||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
},
|
||||||
|
layoutType() {
|
||||||
|
return this.$store.state.interface.layoutType
|
||||||
|
},
|
||||||
|
privateMode() {
|
||||||
|
return this.$store.state.instance.private
|
||||||
|
},
|
||||||
|
reverseLayout() {
|
||||||
|
const { thirdColumnMode, sidebarRight: reverseSetting } =
|
||||||
|
this.$store.getters.mergedConfig
|
||||||
if (this.layoutType !== 'wide') {
|
if (this.layoutType !== 'wide') {
|
||||||
return reverseSetting
|
return reverseSetting
|
||||||
} else {
|
} else {
|
||||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
return thirdColumnMode === 'notifications'
|
||||||
|
? reverseSetting
|
||||||
|
: !reverseSetting
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
noSticky() {
|
||||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
return this.$store.getters.mergedConfig.disableStickyHeaders
|
||||||
|
},
|
||||||
|
showScrollbars() {
|
||||||
|
return this.$store.getters.mergedConfig.showScrollbars
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateMobileState () {
|
updateMobileState() {
|
||||||
this.$store.dispatch('setLayoutWidth', windowWidth())
|
this.$store.dispatch('setLayoutWidth', windowWidth())
|
||||||
this.$store.dispatch('setLayoutHeight', windowHeight())
|
this.$store.dispatch('setLayoutHeight', windowHeight())
|
||||||
}
|
}
|
||||||
|
|
136
src/App.scss
136
src/App.scss
|
@ -1,6 +1,7 @@
|
||||||
// stylelint-disable rscss/class-format
|
// stylelint-disable rscss/class-format
|
||||||
@import './_variables.scss';
|
@import './_variables.scss';
|
||||||
|
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||||
|
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
|
||||||
:root {
|
:root {
|
||||||
--navbar-height: 3.5rem;
|
--navbar-height: 3.5rem;
|
||||||
--post-line-height: 1.4;
|
--post-line-height: 1.4;
|
||||||
|
@ -12,8 +13,8 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: $system-sans-serif;
|
||||||
font-family: var(--interfaceFont, sans-serif);
|
font-family: var(--interfaceFont, $system-sans-serif);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
|
@ -22,84 +23,13 @@ body {
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
background: var(--bg);
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
display: none;
|
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 {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
|
@ -110,7 +40,7 @@ h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
i[class*=icon-],
|
i[class*='icon-'],
|
||||||
.svg-inline--fa {
|
.svg-inline--fa {
|
||||||
color: $fallback--icon;
|
color: $fallback--icon;
|
||||||
color: var(--icon, $fallback--icon);
|
color: var(--icon, $fallback--icon);
|
||||||
|
@ -128,6 +58,7 @@ nav {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: var(--navbar-height);
|
height: var(--navbar-height);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
backdrop-filter: blur(12px) saturate(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
|
@ -182,7 +113,7 @@ nav {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--miniColumn) var(--maxiColumn);
|
grid-template-columns: var(--miniColumn) var(--maxiColumn);
|
||||||
grid-template-areas: "sidebar content";
|
grid-template-areas: 'sidebar content';
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
@ -191,6 +122,7 @@ nav {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
|
padding: 0 calc(var(--columnGap) / 2);
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
--___columnMargin: var(--columnGap);
|
--___columnMargin: var(--columnGap);
|
||||||
|
@ -228,7 +160,9 @@ nav {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin-left: calc(var(--___paddingIncrease) * -1);
|
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
|
// 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
|
// might implement old style of hiding scrollbars later if there's demand
|
||||||
|
@ -236,7 +170,9 @@ nav {
|
||||||
&:not(.-show-scrollbar) {
|
&:not(.-show-scrollbar) {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
margin-right: calc(var(--___paddingIncrease) * -1);
|
margin-right: calc(var(--___paddingIncrease) * -1);
|
||||||
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
padding-right: calc(
|
||||||
|
var(--___paddingIncrease) + var(--___columnMargin) / 2
|
||||||
|
);
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -276,21 +212,21 @@ nav {
|
||||||
|
|
||||||
&.-reverse:not(.-wide):not(.-mobile) {
|
&.-reverse:not(.-wide):not(.-mobile) {
|
||||||
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
||||||
grid-template-areas: "content sidebar";
|
grid-template-areas: 'content sidebar';
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-wide {
|
&.-wide {
|
||||||
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
|
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
|
||||||
grid-template-areas: "sidebar content notifs";
|
grid-template-areas: 'sidebar content notifs';
|
||||||
|
|
||||||
&.-reverse {
|
&.-reverse {
|
||||||
grid-template-areas: "notifs content sidebar";
|
grid-template-areas: 'notifs content sidebar';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-mobile {
|
&.-mobile {
|
||||||
grid-template-columns: 100vw;
|
grid-template-columns: 100vw;
|
||||||
grid-template-areas: "content";
|
grid-template-areas: 'content';
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
|
@ -347,7 +283,7 @@ nav {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
i[class*=icon-],
|
i[class*='icon-'],
|
||||||
.svg-inline--fa {
|
.svg-inline--fa {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnText, $fallback--text);
|
color: var(--btnText, $fallback--text);
|
||||||
|
@ -363,7 +299,9 @@ nav {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&: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);
|
box-shadow: var(--buttonPressedShadow);
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnPressedText, $fallback--text);
|
color: var(--btnPressedText, $fallback--text);
|
||||||
|
@ -396,7 +334,9 @@ nav {
|
||||||
color: var(--btnToggledText, $fallback--text);
|
color: var(--btnToggledText, $fallback--text);
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--btnToggled, $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);
|
box-shadow: var(--buttonPressedShadow);
|
||||||
|
|
||||||
svg,
|
svg,
|
||||||
|
@ -461,14 +401,15 @@ textarea,
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: $fallback--inputRadius;
|
border-radius: $fallback--inputRadius;
|
||||||
border-radius: var(--inputRadius, $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);
|
box-shadow: var(--inputShadow);
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--input, $fallback--fg);
|
background-color: var(--input, $fallback--fg);
|
||||||
color: $fallback--lightText;
|
color: $fallback--lightText;
|
||||||
color: var(--inputText, $fallback--lightText);
|
color: var(--inputText, $fallback--lightText);
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-family: var(--inputFont, sans-serif);
|
font-family: var(--interfaceFont, sans-serif);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -479,13 +420,13 @@ textarea,
|
||||||
padding: 0 var(--_padding);
|
padding: 0 var(--_padding);
|
||||||
|
|
||||||
&:disabled,
|
&:disabled,
|
||||||
&[disabled=disabled],
|
&[disabled='disabled'],
|
||||||
&.disabled {
|
&.disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[type=range] {
|
&[type='range'] {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -493,12 +434,13 @@ textarea,
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[type=radio] {
|
&[type='radio'] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
&:checked + label::before {
|
&:checked + label::before {
|
||||||
box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
|
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);
|
background-color: var(--accent, $fallback--link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -519,7 +461,7 @@ textarea,
|
||||||
height: 1.1em;
|
height: 1.1em;
|
||||||
border-radius: 100%; // Radio buttons should always be circle
|
border-radius: 100%; // Radio buttons should always be circle
|
||||||
box-shadow: 0 0 2px black inset;
|
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;
|
margin-right: 0.5em;
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--input, $fallback--fg);
|
background-color: var(--input, $fallback--fg);
|
||||||
|
@ -533,7 +475,7 @@ textarea,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[type=checkbox] {
|
&[type='checkbox'] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
&:checked + label::before {
|
&:checked + label::before {
|
||||||
|
@ -559,7 +501,7 @@ textarea,
|
||||||
border-radius: $fallback--checkboxRadius;
|
border-radius: $fallback--checkboxRadius;
|
||||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||||
box-shadow: 0 0 2px black inset;
|
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;
|
margin-right: 0.5em;
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--input, $fallback--fg);
|
background-color: var(--input, $fallback--fg);
|
||||||
|
@ -594,8 +536,8 @@ option {
|
||||||
.hide-number-spinner {
|
.hide-number-spinner {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
|
|
||||||
&[type=number]::-webkit-inner-spin-button,
|
&[type='number']::-webkit-inner-spin-button,
|
||||||
&[type=number]::-webkit-outer-spin-button {
|
&[type='number']::-webkit-outer-spin-button {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
:to="{ name: 'login' }"
|
:to="{ name: 'login' }"
|
||||||
class="panel-body"
|
class="panel-body"
|
||||||
>
|
>
|
||||||
{{ $t("login.hint") }}
|
{{ $t('login.hint') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|
|
@ -4,7 +4,7 @@ $darkened-background: whitesmoke;
|
||||||
|
|
||||||
$fallback--bg: #121a24;
|
$fallback--bg: #121a24;
|
||||||
$fallback--fg: #182230;
|
$fallback--fg: #182230;
|
||||||
$fallback--faint: rgba(185, 185, 186, .5);
|
$fallback--faint: rgba(185, 185, 186, 0.5);
|
||||||
$fallback--text: #b9b9ba;
|
$fallback--text: #b9b9ba;
|
||||||
$fallback--link: #d8a070;
|
$fallback--link: #d8a070;
|
||||||
$fallback--icon: #666;
|
$fallback--icon: #666;
|
||||||
|
@ -16,8 +16,8 @@ $fallback--cBlue: #0095ff;
|
||||||
$fallback--cGreen: #0fa00f;
|
$fallback--cGreen: #0fa00f;
|
||||||
$fallback--cOrange: orange;
|
$fallback--cOrange: orange;
|
||||||
|
|
||||||
$fallback--alertError: rgba(211,16,20,.5);
|
$fallback--alertError: rgba(211, 16, 20, 0.5);
|
||||||
$fallback--alertWarning: rgba(111,111,20,.5);
|
$fallback--alertWarning: rgba(111, 111, 20, 0.5);
|
||||||
|
|
||||||
$fallback--panelRadius: 10px;
|
$fallback--panelRadius: 10px;
|
||||||
$fallback--checkboxRadius: 2px;
|
$fallback--checkboxRadius: 2px;
|
||||||
|
@ -28,6 +28,14 @@ $fallback--avatarRadius: 4px;
|
||||||
$fallback--avatarAltRadius: 10px;
|
$fallback--avatarAltRadius: 10px;
|
||||||
$fallback--attachmentRadius: 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;
|
$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 { createRouter, createWebHistory } from 'vue-router'
|
||||||
import vClickOutside from 'click-outside-vue3'
|
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 App from '../App.vue'
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
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 { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||||
|
@ -23,7 +31,9 @@ const parsedInitialResults = () => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (!staticInitialResults) {
|
if (!staticInitialResults) {
|
||||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
staticInitialResults = JSON.parse(
|
||||||
|
document.getElementById('initial-results').textContent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return staticInitialResults
|
return staticInitialResults
|
||||||
}
|
}
|
||||||
|
@ -71,18 +81,30 @@ const getInstanceConfig = async ({ store }) => {
|
||||||
const textlimit = data.max_toot_chars
|
const textlimit = data.max_toot_chars
|
||||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
store.dispatch('setInstanceOption', {
|
||||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
name: 'textlimit',
|
||||||
|
value: textlimit
|
||||||
|
})
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'accountApprovalRequired',
|
||||||
|
value: data.approval_required
|
||||||
|
})
|
||||||
// don't override cookie if set
|
// don't override cookie if set
|
||||||
if (!Cookies.get('userLanguage')) {
|
if (!Cookies.get('userLanguage')) {
|
||||||
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
|
store.dispatch('setOption', {
|
||||||
|
name: 'interfaceLanguage',
|
||||||
|
value: resolveLanguage(data.languages)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vapidPublicKey) {
|
if (vapidPublicKey) {
|
||||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'vapidPublicKey',
|
||||||
|
value: vapidPublicKey
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw res
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Could not load instance config, potentially fatal')
|
console.error('Could not load instance config, potentially fatal')
|
||||||
|
@ -97,10 +119,12 @@ const getBackendProvidedConfig = async ({ store }) => {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.pleroma_fe
|
return data.pleroma_fe
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw res
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +135,7 @@ const getStaticConfig = async () => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return res.json()
|
return res.json()
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw res
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load static/config.json, continuing without it.')
|
console.warn('Failed to load static/config.json, continuing without it.')
|
||||||
|
@ -154,16 +178,12 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
name: 'logoMask',
|
name: 'logoMask',
|
||||||
value: typeof config.logoMask === 'undefined'
|
value: typeof config.logoMask === 'undefined' ? true : config.logoMask
|
||||||
? true
|
|
||||||
: config.logoMask
|
|
||||||
})
|
})
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
name: 'logoMargin',
|
name: 'logoMargin',
|
||||||
value: typeof config.logoMargin === 'undefined'
|
value: typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin
|
||||||
? 0
|
|
||||||
: config.logoMargin
|
|
||||||
})
|
})
|
||||||
copyInstanceOption('logoLeft')
|
copyInstanceOption('logoLeft')
|
||||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||||
|
@ -191,7 +211,7 @@ const getTOS = async ({ store }) => {
|
||||||
const html = await res.text()
|
const html = await res.text()
|
||||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw res
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Can't load TOS")
|
console.warn("Can't load TOS")
|
||||||
|
@ -204,9 +224,12 @@ const getInstancePanel = async ({ store }) => {
|
||||||
const res = await preloadFetch('/instance/panel.html')
|
const res = await preloadFetch('/instance/panel.html')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const html = await res.text()
|
const html = await res.text()
|
||||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'instanceSpecificPanelContent',
|
||||||
|
value: html
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw res
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Can't load instance panel")
|
console.warn("Can't load instance panel")
|
||||||
|
@ -219,25 +242,30 @@ const getStickers = async ({ store }) => {
|
||||||
const res = await window.fetch('/static/stickers.json')
|
const res = await window.fetch('/static/stickers.json')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const values = await res.json()
|
const values = await res.json()
|
||||||
const stickers = (await Promise.all(
|
const stickers = (
|
||||||
Object.entries(values).map(async ([name, path]) => {
|
await Promise.all(
|
||||||
const resPack = await window.fetch(path + 'pack.json')
|
Object.entries(values).map(async ([name, path]) => {
|
||||||
var meta = {}
|
const resPack = await window.fetch(path + 'pack.json')
|
||||||
if (resPack.ok) {
|
var meta = {}
|
||||||
meta = await resPack.json()
|
if (resPack.ok) {
|
||||||
}
|
meta = await resPack.json()
|
||||||
return {
|
}
|
||||||
pack: name,
|
return {
|
||||||
path,
|
pack: name,
|
||||||
meta
|
path,
|
||||||
}
|
meta
|
||||||
})
|
}
|
||||||
)).sort((a, b) => {
|
})
|
||||||
|
)
|
||||||
|
).sort((a, b) => {
|
||||||
return a.meta.title.localeCompare(b.meta.title)
|
return a.meta.title.localeCompare(b.meta.title)
|
||||||
})
|
})
|
||||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'stickers',
|
||||||
|
value: stickers
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw res
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Can't load stickers")
|
console.warn("Can't load stickers")
|
||||||
|
@ -252,13 +280,19 @@ const getAppSecret = async ({ store }) => {
|
||||||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||||
.then((token) => {
|
.then((token) => {
|
||||||
commit('setAppToken', token.access_token)
|
commit('setAppToken', token.access_token)
|
||||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
commit(
|
||||||
|
'setBackendInteractor',
|
||||||
|
backendInteractorService(store.getters.getToken())
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'staffAccounts',
|
||||||
|
value: nicknames
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNodeInfo = async ({ store }) => {
|
const getNodeInfo = async ({ store }) => {
|
||||||
|
@ -268,65 +302,146 @@ const getNodeInfo = async ({ store }) => {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const metadata = data.metadata
|
const metadata = data.metadata
|
||||||
const features = metadata.features
|
const features = metadata.features
|
||||||
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
store.dispatch('setInstanceOption', {
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
name: 'name',
|
||||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
value: metadata.nodeName
|
||||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
})
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', {
|
||||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
name: 'registrationOpen',
|
||||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
value: data.openRegistrations
|
||||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
})
|
||||||
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
|
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
|
const uploadLimits = metadata.uploadLimits
|
||||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
store.dispatch('setInstanceOption', {
|
||||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
name: 'uploadlimit',
|
||||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
value: parseInt(uploadLimits.general)
|
||||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
})
|
||||||
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
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', {
|
||||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
name: 'restrictedNicknames',
|
||||||
|
value: metadata.restrictedNicknames
|
||||||
|
})
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'postFormats',
|
||||||
|
value: metadata.postFormats
|
||||||
|
})
|
||||||
|
|
||||||
const suggestions = metadata.suggestions
|
const suggestions = metadata.suggestions
|
||||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
store.dispatch('setInstanceOption', {
|
||||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
name: 'suggestionsEnabled',
|
||||||
|
value: suggestions.enabled
|
||||||
|
})
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'suggestionsWeb',
|
||||||
|
value: suggestions.web
|
||||||
|
})
|
||||||
|
|
||||||
const software = data.software
|
const software = data.software
|
||||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
store.dispatch('setInstanceOption', {
|
||||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
|
name: 'backendVersion',
|
||||||
|
value: software.version
|
||||||
|
})
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'pleromaBackend',
|
||||||
|
value: software.name === 'pleroma'
|
||||||
|
})
|
||||||
|
|
||||||
const priv = metadata.private
|
const priv = metadata.private
|
||||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||||
|
|
||||||
const frontendVersion = window.___pleromafe_commit_hash
|
const frontendVersion = window.___pleromafe_commit_hash
|
||||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'frontendVersion',
|
||||||
|
value: frontendVersion
|
||||||
|
})
|
||||||
|
|
||||||
const federation = metadata.federation
|
const federation = metadata.federation
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
name: 'tagPolicyAvailable',
|
name: 'tagPolicyAvailable',
|
||||||
value: typeof federation.mrf_policies === 'undefined'
|
value:
|
||||||
? false
|
typeof federation.mrf_policies === 'undefined'
|
||||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
? false
|
||||||
|
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||||
})
|
})
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
store.dispatch('setInstanceOption', {
|
||||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
|
name: 'federationPolicy',
|
||||||
|
value: federation
|
||||||
|
})
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'localBubbleInstances',
|
||||||
|
value: metadata.localBubbleInstances
|
||||||
|
})
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
name: 'federating',
|
name: 'federating',
|
||||||
value: typeof federation.enabled === 'undefined'
|
value:
|
||||||
? true
|
typeof federation.enabled === 'undefined' ? true : federation.enabled
|
||||||
: federation.enabled
|
})
|
||||||
|
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'publicTimelineVisibility',
|
||||||
|
value: metadata.publicTimelineVisibility
|
||||||
|
})
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'federatedTimelineAvailable',
|
||||||
|
value: metadata.federatedTimelineAvailable
|
||||||
})
|
})
|
||||||
|
|
||||||
const accountActivationRequired = metadata.accountActivationRequired
|
const accountActivationRequired = metadata.accountActivationRequired
|
||||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'accountActivationRequired',
|
||||||
|
value: accountActivationRequired
|
||||||
|
})
|
||||||
|
|
||||||
const accounts = metadata.staffAccounts
|
const accounts = metadata.staffAccounts
|
||||||
resolveStaffAccounts({ store, accounts })
|
resolveStaffAccounts({ store, accounts })
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw res
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not load nodeinfo')
|
console.warn('Could not load nodeinfo')
|
||||||
|
@ -336,11 +451,16 @@ const getNodeInfo = async ({ store }) => {
|
||||||
|
|
||||||
const setConfig = async ({ store }) => {
|
const setConfig = async ({ store }) => {
|
||||||
// apiConfig, staticConfig
|
// apiConfig, staticConfig
|
||||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
const configInfos = await Promise.all([
|
||||||
|
getBackendProvidedConfig({ store }),
|
||||||
|
getStaticConfig()
|
||||||
|
])
|
||||||
const apiConfig = configInfos[0]
|
const apiConfig = configInfos[0]
|
||||||
const staticConfig = configInfos[1]
|
const staticConfig = configInfos[1]
|
||||||
|
|
||||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
await setSettings({ store, apiConfig, staticConfig }).then(
|
||||||
|
getAppSecret({ store })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkOAuthToken = async ({ store }) => {
|
const checkOAuthToken = async ({ store }) => {
|
||||||
|
@ -363,7 +483,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
FaviconService.initFaviconService()
|
FaviconService.initFaviconService()
|
||||||
|
|
||||||
const overrides = window.___pleromafe_dev_overrides || {}
|
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 })
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
|
||||||
await setConfig({ store })
|
await setConfig({ store })
|
||||||
|
@ -373,7 +496,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
const customThemePresent = customThemeSource || customTheme
|
const customThemePresent = customThemeSource || customTheme
|
||||||
|
|
||||||
if (customThemePresent) {
|
if (customThemePresent) {
|
||||||
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
|
if (
|
||||||
|
customThemeSource &&
|
||||||
|
customThemeSource.themeEngineVersion === CURRENT_VERSION
|
||||||
|
) {
|
||||||
applyTheme(customThemeSource)
|
applyTheme(customThemeSource)
|
||||||
} else {
|
} else {
|
||||||
applyTheme(customTheme)
|
applyTheme(customTheme)
|
||||||
|
@ -394,9 +520,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
])
|
])
|
||||||
|
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
|
||||||
store.dispatch('startFetchingAnnouncements')
|
|
||||||
store.dispatch('startFetchingReports')
|
|
||||||
getTOS({ store })
|
getTOS({ store })
|
||||||
getStickers({ store })
|
getStickers({ store })
|
||||||
|
|
||||||
|
@ -404,7 +527,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: routes(store),
|
routes: routes(store),
|
||||||
scrollBehavior: (to, _from, savedPosition) => {
|
scrollBehavior: (to, _from, savedPosition) => {
|
||||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
if (to.matched.some((m) => m.meta.dontScroll)) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,51 +35,145 @@ export default (store) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let routes = [
|
let routes = [
|
||||||
{ name: 'root',
|
{
|
||||||
|
name: 'root',
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: _to => {
|
redirect: (_to) => {
|
||||||
return (store.state.users.currentUser
|
return (
|
||||||
? store.state.instance.redirectRootLogin
|
(store.state.users.currentUser
|
||||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
? 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: 'public-external-timeline',
|
||||||
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
|
path: '/main/all',
|
||||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
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: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
{ 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([^/@]+)',
|
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||||
component: RemoteUserResolver,
|
component: RemoteUserResolver,
|
||||||
beforeEnter: validateAuthenticatedRoute
|
beforeEnter: validateAuthenticatedRoute
|
||||||
},
|
},
|
||||||
{ name: 'remote-user-profile',
|
{
|
||||||
|
name: 'remote-user-profile',
|
||||||
path: '/remote-users/:hostname/:username',
|
path: '/remote-users/:hostname/:username',
|
||||||
component: RemoteUserResolver,
|
component: RemoteUserResolver,
|
||||||
beforeEnter: validateAuthenticatedRoute
|
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: 'external-user-profile',
|
||||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
path: '/users/:id',
|
||||||
|
component: UserProfile,
|
||||||
|
meta: { dontScroll: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'interactions',
|
||||||
|
path: '/users/:username/interactions',
|
||||||
|
component: Interactions,
|
||||||
|
beforeEnter: validateAuthenticatedRoute
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dms',
|
||||||
|
path: '/users/:username/dms',
|
||||||
|
component: DMs,
|
||||||
|
beforeEnter: validateAuthenticatedRoute
|
||||||
|
},
|
||||||
{ name: 'registration', path: '/registration', component: Registration },
|
{ name: 'registration', path: '/registration', component: Registration },
|
||||||
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent },
|
{
|
||||||
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation },
|
name: 'registration-request-sent',
|
||||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
path: '/registration-request-sent',
|
||||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
component: RegistrationRequestSent
|
||||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
},
|
||||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
{
|
||||||
|
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: '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: 'oauth-callback',
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
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: 'about', path: '/about', component: About },
|
||||||
{ name: 'lists', path: '/lists', component: Lists },
|
{ name: 'lists', path: '/lists', component: Lists },
|
||||||
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
|
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
|
||||||
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
|
{ 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
|
return routes
|
||||||
|
|
|
@ -15,13 +15,17 @@ const About = {
|
||||||
LocalBubblePanel
|
LocalBubblePanel
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
showFeaturesPanel() {
|
||||||
showInstanceSpecificPanel () {
|
return this.$store.state.instance.showFeaturesPanel
|
||||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
},
|
||||||
|
showInstanceSpecificPanel() {
|
||||||
|
return (
|
||||||
|
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||||
!this.$store.getters.mergedConfig.hideISP &&
|
!this.$store.getters.mergedConfig.hideISP &&
|
||||||
this.$store.state.instance.instanceSpecificPanelContent
|
this.$store.state.instance.instanceSpecificPanelContent
|
||||||
|
)
|
||||||
},
|
},
|
||||||
showLocalBubblePanel () {
|
showLocalBubblePanel() {
|
||||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./about.js" ></script>
|
<script src="./about.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss"></style>
|
||||||
</style>
|
|
||||||
|
|
|
@ -3,19 +3,13 @@ import Popover from '../popover/popover.vue'
|
||||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import {
|
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||||
faEllipsisV
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
library.add(faEllipsisV)
|
||||||
faEllipsisV
|
|
||||||
)
|
|
||||||
|
|
||||||
const AccountActions = {
|
const AccountActions = {
|
||||||
props: [
|
props: ['user', 'relationship'],
|
||||||
'user', 'relationship'
|
data() {
|
||||||
],
|
|
||||||
data () {
|
|
||||||
return {
|
return {
|
||||||
showingConfirmBlock: false
|
showingConfirmBlock: false
|
||||||
}
|
}
|
||||||
|
@ -26,56 +20,59 @@ const AccountActions = {
|
||||||
ConfirmModal
|
ConfirmModal
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
refetchRelationship () {
|
refetchRelationship() {
|
||||||
return this.$store.dispatch('fetchUserRelationship', this.user.id)
|
return this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||||
},
|
},
|
||||||
showConfirmBlock () {
|
showConfirmBlock() {
|
||||||
this.showingConfirmBlock = true
|
this.showingConfirmBlock = true
|
||||||
},
|
},
|
||||||
hideConfirmBlock () {
|
hideConfirmBlock() {
|
||||||
this.showingConfirmBlock = false
|
this.showingConfirmBlock = false
|
||||||
},
|
},
|
||||||
showRepeats () {
|
showRepeats() {
|
||||||
this.$store.dispatch('showReblogs', this.user.id)
|
this.$store.dispatch('showReblogs', this.user.id)
|
||||||
},
|
},
|
||||||
hideRepeats () {
|
hideRepeats() {
|
||||||
this.$store.dispatch('hideReblogs', this.user.id)
|
this.$store.dispatch('hideReblogs', this.user.id)
|
||||||
},
|
},
|
||||||
blockUser () {
|
blockUser() {
|
||||||
if (!this.shouldConfirmBlock) {
|
if (!this.shouldConfirmBlock) {
|
||||||
this.doBlockUser()
|
this.doBlockUser()
|
||||||
} else {
|
} else {
|
||||||
this.showConfirmBlock()
|
this.showConfirmBlock()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doBlockUser () {
|
doBlockUser() {
|
||||||
this.$store.dispatch('blockUser', this.user.id)
|
this.$store.dispatch('blockUser', this.user.id)
|
||||||
this.hideConfirmBlock()
|
this.hideConfirmBlock()
|
||||||
},
|
},
|
||||||
unblockUser () {
|
unblockUser() {
|
||||||
this.$store.dispatch('unblockUser', this.user.id)
|
this.$store.dispatch('unblockUser', this.user.id)
|
||||||
},
|
},
|
||||||
removeUserFromFollowers () {
|
removeUserFromFollowers() {
|
||||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||||
},
|
},
|
||||||
reportUser () {
|
reportUser() {
|
||||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||||
},
|
},
|
||||||
muteDomain () {
|
muteDomain() {
|
||||||
this.$store.dispatch('muteDomain', this.user.screen_name.split('@')[1])
|
this.$store
|
||||||
|
.dispatch('muteDomain', this.user.screen_name.split('@')[1])
|
||||||
.then(() => this.refetchRelationship())
|
.then(() => this.refetchRelationship())
|
||||||
},
|
},
|
||||||
unmuteDomain () {
|
unmuteDomain() {
|
||||||
this.$store.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
|
this.$store
|
||||||
|
.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
|
||||||
.then(() => this.refetchRelationship())
|
.then(() => this.refetchRelationship())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
shouldConfirmBlock () {
|
shouldConfirmBlock() {
|
||||||
return this.$store.getters.mergedConfig.modalOnBlock
|
return this.$store.getters.mergedConfig.modalOnBlock
|
||||||
},
|
},
|
||||||
...mapState({
|
...mapState({
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
pleromaChatMessagesAvailable: (state) =>
|
||||||
|
state.instance.pleromaChatMessagesAvailable
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
remove-padding
|
remove-padding
|
||||||
>
|
>
|
||||||
<template v-slot:content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<template v-if="relationship.following">
|
<template v-if="relationship.following">
|
||||||
<button
|
<button
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button class="button-unstyled ellipsis-button">
|
<button class="button-unstyled ellipsis-button">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="icon"
|
class="icon"
|
||||||
|
@ -93,10 +93,8 @@
|
||||||
keypath="user_card.block_confirm"
|
keypath="user_card.block_confirm"
|
||||||
tag="span"
|
tag="span"
|
||||||
>
|
>
|
||||||
<template v-slot:user>
|
<template #user>
|
||||||
<span
|
<span v-text="user.screen_name_ui" />
|
||||||
v-text="user.screen_name_ui"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</confirm-modal>
|
</confirm-modal>
|
||||||
|
|
|
@ -8,7 +8,7 @@ const Announcement = {
|
||||||
AnnouncementEditor,
|
AnnouncementEditor,
|
||||||
RichContent
|
RichContent
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
editing: false,
|
editing: false,
|
||||||
editedAnnouncement: {
|
editedAnnouncement: {
|
||||||
|
@ -25,78 +25,93 @@ const Announcement = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
currentUser: state => state.users.currentUser
|
currentUser: (state) => state.users.currentUser
|
||||||
}),
|
}),
|
||||||
content () {
|
content() {
|
||||||
return this.announcement.content
|
return this.announcement.content
|
||||||
},
|
},
|
||||||
isRead () {
|
isRead() {
|
||||||
return this.announcement.read
|
return this.announcement.read
|
||||||
},
|
},
|
||||||
publishedAt () {
|
publishedAt() {
|
||||||
const time = this.announcement['published_at']
|
const time = this.announcement['published_at']
|
||||||
if (!time) {
|
if (!time) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
return this.formatTimeOrDate(
|
||||||
|
time,
|
||||||
|
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
startsAt () {
|
startsAt() {
|
||||||
const time = this.announcement['starts_at']
|
const time = this.announcement['starts_at']
|
||||||
if (!time) {
|
if (!time) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
return this.formatTimeOrDate(
|
||||||
|
time,
|
||||||
|
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
endsAt () {
|
endsAt() {
|
||||||
const time = this.announcement['ends_at']
|
const time = this.announcement['ends_at']
|
||||||
if (!time) {
|
if (!time) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
return this.formatTimeOrDate(
|
||||||
|
time,
|
||||||
|
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
inactive () {
|
inactive() {
|
||||||
return this.announcement.inactive
|
return this.announcement.inactive
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
markAsRead () {
|
markAsRead() {
|
||||||
if (!this.isRead) {
|
if (!this.isRead) {
|
||||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
return this.$store.dispatch(
|
||||||
|
'markAnnouncementAsRead',
|
||||||
|
this.announcement.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteAnnouncement () {
|
deleteAnnouncement() {
|
||||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||||
},
|
},
|
||||||
formatTimeOrDate (time, locale) {
|
formatTimeOrDate(time, locale) {
|
||||||
const d = new Date(time)
|
const d = new Date(time)
|
||||||
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
return this.announcement['all_day']
|
||||||
|
? d.toLocaleDateString(locale)
|
||||||
|
: d.toLocaleString(locale)
|
||||||
},
|
},
|
||||||
enterEditMode () {
|
enterEditMode() {
|
||||||
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
|
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
|
||||||
this.editedAnnouncement.startsAt = this.announcement['starts_at']
|
this.editedAnnouncement.startsAt = this.announcement['starts_at']
|
||||||
this.editedAnnouncement.endsAt = this.announcement['ends_at']
|
this.editedAnnouncement.endsAt = this.announcement['ends_at']
|
||||||
this.editedAnnouncement.allDay = this.announcement['all_day']
|
this.editedAnnouncement.allDay = this.announcement['all_day']
|
||||||
this.editing = true
|
this.editing = true
|
||||||
},
|
},
|
||||||
submitEdit () {
|
submitEdit() {
|
||||||
this.$store.dispatch('editAnnouncement', {
|
this.$store
|
||||||
id: this.announcement.id,
|
.dispatch('editAnnouncement', {
|
||||||
...this.editedAnnouncement
|
id: this.announcement.id,
|
||||||
})
|
...this.editedAnnouncement
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.editing = false
|
this.editing = false
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.editError = error.error
|
this.editError = error.error
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
cancelEdit () {
|
cancelEdit() {
|
||||||
this.editing = false
|
this.editing = false
|
||||||
},
|
},
|
||||||
clearError () {
|
clearError() {
|
||||||
this.editError = undefined
|
this.editError = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@
|
||||||
class="times"
|
class="times"
|
||||||
>
|
>
|
||||||
<span v-if="publishedAt">
|
<span v-if="publishedAt">
|
||||||
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
{{
|
||||||
|
$t('announcements.published_time_display', { time: publishedAt })
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="startsAt">
|
<span v-if="startsAt">
|
||||||
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||||
|
@ -99,7 +101,7 @@
|
||||||
<script src="./announcement.js"></script>
|
<script src="./announcement.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "../../variables";
|
@import '../../variables';
|
||||||
|
|
||||||
.announcement {
|
.announcement {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
|
@ -108,7 +110,8 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: var(--status-margin, $status-margin);
|
padding: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
.heading, .body {
|
.heading,
|
||||||
|
.body {
|
||||||
margin-bottom: var(--status-margin, $status-margin);
|
margin-bottom: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,22 +10,26 @@
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
/>
|
/>
|
||||||
<span class="announcement-metadata">
|
<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
|
<input
|
||||||
id="announcement-start-time"
|
id="announcement-start-time"
|
||||||
v-model="announcement.startsAt"
|
v-model="announcement.startsAt"
|
||||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="announcement-metadata">
|
<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
|
<input
|
||||||
id="announcement-end-time"
|
id="announcement-end-time"
|
||||||
v-model="announcement.endsAt"
|
v-model="announcement.endsAt"
|
||||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="announcement-metadata">
|
<span class="announcement-metadata">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -33,7 +37,9 @@
|
||||||
v-model="announcement.allDay"
|
v-model="announcement.allDay"
|
||||||
:disabled="disabled"
|
: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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -7,7 +7,7 @@ const AnnouncementsPage = {
|
||||||
Announcement,
|
Announcement,
|
||||||
AnnouncementEditor
|
AnnouncementEditor
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
newAnnouncement: {
|
newAnnouncement: {
|
||||||
content: '',
|
content: '',
|
||||||
|
@ -19,34 +19,35 @@ const AnnouncementsPage = {
|
||||||
error: undefined
|
error: undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted() {
|
||||||
this.$store.dispatch('fetchAnnouncements')
|
this.$store.dispatch('fetchAnnouncements')
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
currentUser: state => state.users.currentUser
|
currentUser: (state) => state.users.currentUser
|
||||||
}),
|
}),
|
||||||
announcements () {
|
announcements() {
|
||||||
return this.$store.state.announcements.announcements
|
return this.$store.state.announcements.announcements
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
postAnnouncement () {
|
postAnnouncement() {
|
||||||
this.posting = true
|
this.posting = true
|
||||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
this.$store
|
||||||
|
.dispatch('postAnnouncement', this.newAnnouncement)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.newAnnouncement.content = ''
|
this.newAnnouncement.content = ''
|
||||||
this.startsAt = undefined
|
this.startsAt = undefined
|
||||||
this.endsAt = undefined
|
this.endsAt = undefined
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.error = error.error
|
this.error = error.error
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.posting = false
|
this.posting = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearError () {
|
clearError() {
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<section
|
<section v-if="currentUser && currentUser.role === 'admin'">
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
|
||||||
>
|
|
||||||
<div class="post-form">
|
<div class="post-form">
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||||
|
@ -50,9 +48,7 @@
|
||||||
v-for="announcement in announcements"
|
v-for="announcement in announcements"
|
||||||
:key="announcement.id"
|
:key="announcement.id"
|
||||||
>
|
>
|
||||||
<announcement
|
<announcement :announcement="announcement" />
|
||||||
:announcement="announcement"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,13 +57,14 @@
|
||||||
<script src="./announcements_page.js"></script>
|
<script src="./announcements_page.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "../../variables";
|
@import '../../variables';
|
||||||
|
|
||||||
.announcements-page {
|
.announcements-page {
|
||||||
.post-form {
|
.post-form {
|
||||||
padding: var(--status-margin, $status-margin);
|
padding: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
.heading, .body {
|
.heading,
|
||||||
|
.body {
|
||||||
margin-bottom: var(--status-margin, $status-margin);
|
margin-bottom: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
export default {
|
export default {
|
||||||
emits: ['resetAsyncComponent'],
|
emits: ['resetAsyncComponent'],
|
||||||
methods: {
|
methods: {
|
||||||
retry () {
|
retry() {
|
||||||
this.$emit('resetAsyncComponent')
|
this.$emit('resetAsyncComponent')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,8 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
.btn {
|
.btn {
|
||||||
margin: .5em;
|
margin: 0.5em;
|
||||||
padding: .5em 2em;
|
padding: 0.5em 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
faPencilAlt,
|
faPencilAlt,
|
||||||
faAlignRight
|
faAlignRight
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Blurhash from '../blurhash/Blurhash.vue'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faFile,
|
faFile,
|
||||||
|
@ -46,14 +47,16 @@ const Attachment = {
|
||||||
'shiftDn',
|
'shiftDn',
|
||||||
'edit'
|
'edit'
|
||||||
],
|
],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
localDescription: this.description || this.attachment.description,
|
localDescription: this.description || this.attachment.description,
|
||||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||||
loading: false,
|
loading: false,
|
||||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
img:
|
||||||
|
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
|
||||||
|
document.createElement('img'),
|
||||||
modalOpen: false,
|
modalOpen: false,
|
||||||
showHidden: false,
|
showHidden: false,
|
||||||
flashLoaded: false,
|
flashLoaded: false,
|
||||||
|
@ -63,10 +66,11 @@ const Attachment = {
|
||||||
components: {
|
components: {
|
||||||
Flash,
|
Flash,
|
||||||
StillImage,
|
StillImage,
|
||||||
VideoAttachment
|
VideoAttachment,
|
||||||
|
Blurhash
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
classNames () {
|
classNames() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'-loading': this.loading,
|
'-loading': this.loading,
|
||||||
|
@ -78,37 +82,40 @@ const Attachment = {
|
||||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
usePlaceholder () {
|
usePlaceholder() {
|
||||||
return this.size === 'hide'
|
return this.size === 'hide'
|
||||||
},
|
},
|
||||||
useContainFit () {
|
useContainFit() {
|
||||||
return this.$store.getters.mergedConfig.useContainFit
|
return this.$store.getters.mergedConfig.useContainFit
|
||||||
},
|
},
|
||||||
placeholderName () {
|
useBlurhash() {
|
||||||
|
return this.$store.getters.mergedConfig.useBlurhash
|
||||||
|
},
|
||||||
|
placeholderName() {
|
||||||
if (this.attachment.description === '' || !this.attachment.description) {
|
if (this.attachment.description === '' || !this.attachment.description) {
|
||||||
return this.type.toUpperCase()
|
return this.type.toUpperCase()
|
||||||
}
|
}
|
||||||
return this.attachment.description
|
return this.attachment.description
|
||||||
},
|
},
|
||||||
placeholderIconClass () {
|
placeholderIconClass() {
|
||||||
if (this.type === 'image') return 'image'
|
if (this.type === 'image') return 'image'
|
||||||
if (this.type === 'video') return 'video'
|
if (this.type === 'video') return 'video'
|
||||||
if (this.type === 'audio') return 'music'
|
if (this.type === 'audio') return 'music'
|
||||||
return 'file'
|
return 'file'
|
||||||
},
|
},
|
||||||
referrerpolicy () {
|
referrerpolicy() {
|
||||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||||
},
|
},
|
||||||
type () {
|
type() {
|
||||||
return fileTypeService.fileType(this.attachment.mimetype)
|
return fileTypeService.fileType(this.attachment.mimetype)
|
||||||
},
|
},
|
||||||
hidden () {
|
hidden() {
|
||||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||||
},
|
},
|
||||||
isEmpty () {
|
isEmpty() {
|
||||||
return (this.type === 'html' && !this.attachment.oembed)
|
return this.type === 'html' && !this.attachment.oembed
|
||||||
},
|
},
|
||||||
useModal () {
|
useModal() {
|
||||||
let modalTypes = []
|
let modalTypes = []
|
||||||
switch (this.size) {
|
switch (this.size) {
|
||||||
case 'hide':
|
case 'hide':
|
||||||
|
@ -123,29 +130,29 @@ const Attachment = {
|
||||||
}
|
}
|
||||||
return modalTypes.includes(this.type)
|
return modalTypes.includes(this.type)
|
||||||
},
|
},
|
||||||
videoTag () {
|
videoTag() {
|
||||||
return this.useModal ? 'button' : 'span'
|
return this.useModal ? 'button' : 'span'
|
||||||
},
|
},
|
||||||
statusForm () {
|
statusForm() {
|
||||||
return this.$parent.$parent
|
return this.$parent.$parent
|
||||||
},
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'attachment.description' (newVal) {
|
'attachment.description'(newVal) {
|
||||||
this.localDescription = newVal
|
this.localDescription = newVal
|
||||||
},
|
},
|
||||||
localDescription (newVal) {
|
localDescription(newVal) {
|
||||||
this.onEdit(newVal)
|
this.onEdit(newVal)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
linkClicked ({ target }) {
|
linkClicked({ target }) {
|
||||||
if (target.tagName === 'A') {
|
if (target.tagName === 'A') {
|
||||||
window.open(target.href, '_blank')
|
window.open(target.href, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModal (event) {
|
openModal(event) {
|
||||||
if (this.useModal) {
|
if (this.useModal) {
|
||||||
this.$emit('setMedia')
|
this.$emit('setMedia')
|
||||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||||
|
@ -153,34 +160,35 @@ const Attachment = {
|
||||||
window.open(this.attachment.url)
|
window.open(this.attachment.url)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openModalForce (event) {
|
openModalForce(event) {
|
||||||
this.$emit('setMedia')
|
this.$emit('setMedia')
|
||||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||||
},
|
},
|
||||||
onEdit (event) {
|
onEdit(event) {
|
||||||
this.edit && this.edit(this.attachment, event)
|
this.edit && this.edit(this.attachment, event)
|
||||||
},
|
},
|
||||||
onRemove () {
|
onRemove() {
|
||||||
this.remove && this.remove(this.attachment)
|
this.remove && this.remove(this.attachment)
|
||||||
},
|
},
|
||||||
onShiftUp () {
|
onShiftUp() {
|
||||||
this.shiftUp && this.shiftUp(this.attachment)
|
this.shiftUp && this.shiftUp(this.attachment)
|
||||||
},
|
},
|
||||||
onShiftDn () {
|
onShiftDn() {
|
||||||
this.shiftDn && this.shiftDn(this.attachment)
|
this.shiftDn && this.shiftDn(this.attachment)
|
||||||
},
|
},
|
||||||
stopFlash () {
|
stopFlash() {
|
||||||
this.$refs.flash.closePlayer()
|
this.$refs.flash.closePlayer()
|
||||||
},
|
},
|
||||||
setFlashLoaded (event) {
|
setFlashLoaded(event) {
|
||||||
this.flashLoaded = event
|
this.flashLoaded = event
|
||||||
},
|
},
|
||||||
toggleDescription () {
|
toggleDescription() {
|
||||||
this.showDescription = !this.showDescription
|
this.showDescription = !this.showDescription
|
||||||
},
|
},
|
||||||
toggleHidden (event) {
|
toggleHidden(event) {
|
||||||
if (
|
if (
|
||||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
this.mergedConfig.useOneClickNsfw &&
|
||||||
|
!this.showHidden &&
|
||||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||||
) {
|
) {
|
||||||
this.openModal(event)
|
this.openModal(event)
|
||||||
|
@ -201,14 +209,16 @@ const Attachment = {
|
||||||
this.showHidden = !this.showHidden
|
this.showHidden = !this.showHidden
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onImageLoad (image) {
|
onImageLoad(image) {
|
||||||
const width = image.naturalWidth
|
const width = image.naturalWidth
|
||||||
const height = image.naturalHeight
|
const height = image.naturalHeight
|
||||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||||
},
|
},
|
||||||
resize (e) {
|
resize(e) {
|
||||||
const target = e.target || 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.
|
// Reset to default height for empty form, nothing else to do here.
|
||||||
if (target.value === '') {
|
if (target.value === '') {
|
||||||
|
@ -219,14 +229,16 @@ const Attachment = {
|
||||||
|
|
||||||
const paddingString = getComputedStyle(target)['padding']
|
const paddingString = getComputedStyle(target)['padding']
|
||||||
// remove -px suffix
|
// 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'
|
target.style.height = 'auto'
|
||||||
const newHeight = Math.floor(target.scrollHeight - padding * 2)
|
const newHeight = Math.floor(target.scrollHeight - padding * 2)
|
||||||
target.style.height = `${newHeight}px`
|
target.style.height = `${newHeight}px`
|
||||||
this.$emit('resize', newHeight)
|
this.$emit('resize', newHeight)
|
||||||
},
|
},
|
||||||
postStatus (event) {
|
postStatus(event) {
|
||||||
this.statusForm.postStatus(event, this.statusForm.newStatus)
|
this.statusForm.postStatus(event, this.statusForm.newStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,6 @@
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.play-icon {
|
.play-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 64px;
|
font-size: 64px;
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
@click.prevent
|
@click.prevent
|
||||||
>
|
>
|
||||||
<FAIcon :icon="placeholderIconClass" />
|
<FAIcon :icon="placeholderIconClass" />
|
||||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
<b>{{ nsfw ? 'NSFW / ' : '' }}</b
|
||||||
|
>{{ edit ? '' : placeholderName }}
|
||||||
</a>
|
</a>
|
||||||
<div
|
<div
|
||||||
v-if="edit || remove"
|
v-if="edit || remove"
|
||||||
|
@ -30,7 +31,11 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
v-if="
|
||||||
|
size !== 'hide' &&
|
||||||
|
!hideDescription &&
|
||||||
|
(edit || localDescription || showDescription)
|
||||||
|
"
|
||||||
class="description-container"
|
class="description-container"
|
||||||
:class="{ '-static': !edit }"
|
:class="{ '-static': !edit }"
|
||||||
>
|
>
|
||||||
|
@ -41,7 +46,7 @@
|
||||||
class="description-field"
|
class="description-field"
|
||||||
:placeholder="$t('post_status.media_description')"
|
:placeholder="$t('post_status.media_description')"
|
||||||
@keydown.enter.prevent=""
|
@keydown.enter.prevent=""
|
||||||
>
|
/>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
{{ localDescription }}
|
{{ localDescription }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -64,11 +69,19 @@
|
||||||
:title="attachment.description"
|
:title="attachment.description"
|
||||||
@click.prevent.stop="toggleHidden"
|
@click.prevent.stop="toggleHidden"
|
||||||
>
|
>
|
||||||
|
<Blurhash
|
||||||
|
v-if="useBlurhash && attachment.blurhash"
|
||||||
|
:height="512"
|
||||||
|
:width="1024"
|
||||||
|
:hash="attachment.blurhash"
|
||||||
|
:punch="1"
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
|
v-else
|
||||||
:key="nsfwImage"
|
:key="nsfwImage"
|
||||||
class="nsfw"
|
class="nsfw"
|
||||||
:src="nsfwImage"
|
:src="nsfwImage"
|
||||||
>
|
/>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
v-if="type === 'video'"
|
v-if="type === 'video'"
|
||||||
class="play-icon"
|
class="play-icon"
|
||||||
|
@ -88,7 +101,12 @@
|
||||||
<FAIcon icon="stop" />
|
<FAIcon icon="stop" />
|
||||||
</button>
|
</button>
|
||||||
<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"
|
class="button-unstyled attachment-button"
|
||||||
:title="$t('status.show_attachment_description')"
|
:title="$t('status.show_attachment_description')"
|
||||||
@click.prevent="toggleDescription"
|
@click.prevent="toggleDescription"
|
||||||
|
@ -140,7 +158,7 @@
|
||||||
<a
|
<a
|
||||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||||
class="image-container"
|
class="image-container"
|
||||||
:class="{'-hidden': hidden && preloadImage }"
|
:class="{ '-hidden': hidden && preloadImage }"
|
||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@click.stop.prevent="openModal"
|
@click.stop.prevent="openModal"
|
||||||
|
@ -218,11 +236,13 @@
|
||||||
v-if="attachment.thumb_url"
|
v-if="attachment.thumb_url"
|
||||||
class="image"
|
class="image"
|
||||||
>
|
>
|
||||||
<img :src="attachment.thumb_url">
|
<img :src="attachment.thumb_url" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- 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" />
|
<div v-html="attachment.oembed.oembedHTML" />
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
</div>
|
</div>
|
||||||
|
@ -244,7 +264,11 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
v-if="
|
||||||
|
size !== 'hide' &&
|
||||||
|
!hideDescription &&
|
||||||
|
(edit || (localDescription && showDescription))
|
||||||
|
"
|
||||||
class="description-container"
|
class="description-container"
|
||||||
:class="{ '-static': !edit }"
|
:class="{ '-static': !edit }"
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,13 +6,17 @@ import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
const AuthForm = {
|
const AuthForm = {
|
||||||
name: 'AuthForm',
|
name: 'AuthForm',
|
||||||
render () {
|
render() {
|
||||||
return h(resolveComponent(this.authForm))
|
return h(resolveComponent(this.authForm))
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
authForm () {
|
authForm() {
|
||||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
if (this.requiredTOTP) {
|
||||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
return 'MFATOTPForm'
|
||||||
|
}
|
||||||
|
if (this.requiredRecovery) {
|
||||||
|
return 'MFARecoveryForm'
|
||||||
|
}
|
||||||
return 'LoginForm'
|
return 'LoginForm'
|
||||||
},
|
},
|
||||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||||
|
|
|
@ -2,11 +2,13 @@ const debounceMilliseconds = 500
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
query: { // function to query results and return a promise
|
query: {
|
||||||
|
// function to query results and return a promise
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
filter: { // function to filter results in real time
|
filter: {
|
||||||
|
// function to filter results in real time
|
||||||
type: Function
|
type: Function
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
|
@ -14,7 +16,7 @@ export default {
|
||||||
default: 'Search...'
|
default: 'Search...'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
term: '',
|
term: '',
|
||||||
timeout: null,
|
timeout: null,
|
||||||
|
@ -23,29 +25,31 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filtered () {
|
filtered() {
|
||||||
return this.filter ? this.filter(this.results) : this.results
|
return this.filter ? this.filter(this.results) : this.results
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
term (val) {
|
term(val) {
|
||||||
this.fetchResults(val)
|
this.fetchResults(val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchResults (term) {
|
fetchResults(term) {
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout)
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
this.results = []
|
this.results = []
|
||||||
if (term) {
|
if (term) {
|
||||||
this.query(term).then((results) => { this.results = results })
|
this.query(term).then((results) => {
|
||||||
|
this.results = results
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, debounceMilliseconds)
|
}, debounceMilliseconds)
|
||||||
},
|
},
|
||||||
onInputClick () {
|
onInputClick() {
|
||||||
this.resultsVisible = true
|
this.resultsVisible = true
|
||||||
},
|
},
|
||||||
onClickOutside () {
|
onClickOutside() {
|
||||||
this.resultsVisible = false
|
this.resultsVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
class="autosuggest-input"
|
class="autosuggest-input"
|
||||||
@click="onInputClick"
|
@click="onInputClick"
|
||||||
>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="resultsVisible && filtered.length > 0"
|
v-if="resultsVisible && filtered.length > 0"
|
||||||
class="autosuggest-results"
|
class="autosuggest-results"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
||||||
const AvatarList = {
|
const AvatarList = {
|
||||||
props: ['users'],
|
props: ['users'],
|
||||||
computed: {
|
computed: {
|
||||||
slicedUsers () {
|
slicedUsers() {
|
||||||
return this.users ? this.users.slice(0, 15) : []
|
return this.users ? this.users.slice(0, 15) : []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -12,8 +12,12 @@ const AvatarList = {
|
||||||
UserAvatar
|
UserAvatar
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
userProfileLink (user) {
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./avatar_list.js" ></script>
|
<script src="./avatar_list.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,8 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
const BasicUserCard = {
|
const BasicUserCard = {
|
||||||
props: [
|
props: ['user'],
|
||||||
'user'
|
data() {
|
||||||
],
|
|
||||||
data () {
|
|
||||||
return {
|
return {
|
||||||
userExpanded: false
|
userExpanded: false
|
||||||
}
|
}
|
||||||
|
@ -18,11 +16,15 @@ const BasicUserCard = {
|
||||||
RichContent
|
RichContent
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded() {
|
||||||
this.userExpanded = !this.userExpanded
|
this.userExpanded = !this.userExpanded
|
||||||
},
|
},
|
||||||
userProfileLink (user) {
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,19 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
|
||||||
const BlockCard = {
|
const BlockCard = {
|
||||||
props: ['userId'],
|
props: ['userId'],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
progress: false
|
progress: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user() {
|
||||||
return this.$store.getters.findUser(this.userId)
|
return this.$store.getters.findUser(this.userId)
|
||||||
},
|
},
|
||||||
relationship () {
|
relationship() {
|
||||||
return this.$store.getters.relationship(this.userId)
|
return this.$store.getters.relationship(this.userId)
|
||||||
},
|
},
|
||||||
blocked () {
|
blocked() {
|
||||||
return this.relationship.blocking
|
return this.relationship.blocking
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -22,13 +22,13 @@ const BlockCard = {
|
||||||
BasicUserCard
|
BasicUserCard
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
unblockUser () {
|
unblockUser() {
|
||||||
this.progress = true
|
this.progress = true
|
||||||
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||||
this.progress = false
|
this.progress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
blockUser () {
|
blockUser() {
|
||||||
this.progress = true
|
this.progress = true
|
||||||
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||||
this.progress = false
|
this.progress = false
|
||||||
|
|
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>
|
|
@ -2,14 +2,14 @@ import Timeline from '../timeline/timeline.vue'
|
||||||
|
|
||||||
const Bookmarks = {
|
const Bookmarks = {
|
||||||
computed: {
|
computed: {
|
||||||
timeline () {
|
timeline() {
|
||||||
return this.$store.state.statuses.timelines.bookmarks
|
return this.$store.state.statuses.timelines.bookmarks
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Timeline
|
Timeline
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted() {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,16 @@ const PublicTimeline = {
|
||||||
Timeline
|
Timeline
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
timeline() {
|
||||||
|
return this.$store.state.statuses.timelines.bubble
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created() {
|
||||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted() {
|
||||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PublicTimeline
|
export default PublicTimeline
|
||||||
|
|
|
@ -11,14 +11,17 @@ export default {
|
||||||
name: 'Timeago',
|
name: 'Timeago',
|
||||||
props: ['date'],
|
props: ['date'],
|
||||||
computed: {
|
computed: {
|
||||||
displayDate () {
|
displayDate() {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
today.setHours(0, 0, 0, 0)
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
if (this.date.getTime() === today.getTime()) {
|
if (this.date.getTime() === today.getTime()) {
|
||||||
return this.$t('display_date.today')
|
return this.$t('display_date.today')
|
||||||
} else {
|
} 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"
|
:checked="modelValue"
|
||||||
:indeterminate="indeterminate"
|
:indeterminate="indeterminate"
|
||||||
@change="$emit('update:modelValue', $event.target.checked)"
|
@change="$emit('update:modelValue', $event.target.checked)"
|
||||||
>
|
/>
|
||||||
<i class="checkbox-indicator" />
|
<i class="checkbox-indicator" />
|
||||||
<span
|
<span
|
||||||
v-if="!!$slots.default"
|
v-if="!!$slots.default"
|
||||||
|
@ -22,12 +22,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:modelValue'],
|
props: ['modelValue', 'indeterminate', 'disabled'],
|
||||||
props: [
|
emits: ['update:modelValue']
|
||||||
'modelValue',
|
|
||||||
'indeterminate',
|
|
||||||
'disabled'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -56,7 +52,7 @@ export default {
|
||||||
border-radius: $fallback--checkboxRadius;
|
border-radius: $fallback--checkboxRadius;
|
||||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||||
box-shadow: 0px 0px 2px black inset;
|
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: $fallback--fg;
|
||||||
background-color: var(--input, $fallback--fg);
|
background-color: var(--input, $fallback--fg);
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -71,7 +67,7 @@ export default {
|
||||||
&.disabled {
|
&.disabled {
|
||||||
.checkbox-indicator::before,
|
.checkbox-indicator::before,
|
||||||
.label {
|
.label {
|
||||||
opacity: .5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
|
@ -79,7 +75,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=checkbox] {
|
input[type='checkbox'] {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
&:checked + .checkbox-indicator::before {
|
&:checked + .checkbox-indicator::before {
|
||||||
|
@ -92,11 +88,10 @@ export default {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--inputText, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
margin-left: .5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
flex: 0 0 0;
|
flex: 0 0 0;
|
||||||
max-width: 9em;
|
max-width: 9em;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: .2em 8px;
|
padding: 0.2em 8px;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -40,9 +40,10 @@
|
||||||
}
|
}
|
||||||
.transparentIndicator {
|
.transparentIndicator {
|
||||||
// forgot to install counter-strike source, ooops
|
// forgot to install counter-strike source, ooops
|
||||||
background-color: #FF00FF;
|
background-color: #ff00ff;
|
||||||
position: relative;
|
position: relative;
|
||||||
&::before, &::after {
|
&::before,
|
||||||
|
&::after {
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
|
@ -64,5 +65,4 @@
|
||||||
.label {
|
.label {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,12 @@
|
||||||
:model-value="present"
|
:model-value="present"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="opt"
|
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">
|
<div class="input color-input-field">
|
||||||
<input
|
<input
|
||||||
|
@ -24,7 +29,7 @@
|
||||||
:value="modelValue || fallback"
|
:value="modelValue || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
@input="$emit('update:modelValue', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="validColor"
|
v-if="validColor"
|
||||||
:id="name"
|
:id="name"
|
||||||
|
@ -33,7 +38,7 @@
|
||||||
:value="modelValue || fallback"
|
:value="modelValue || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
@input="$emit('update:modelValue', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="transparentColor"
|
v-if="transparentColor"
|
||||||
class="transparentIndicator"
|
class="transparentIndicator"
|
||||||
|
@ -41,12 +46,11 @@
|
||||||
<div
|
<div
|
||||||
v-if="computedColor"
|
v-if="computedColor"
|
||||||
class="computedIndicator"
|
class="computedIndicator"
|
||||||
:style="{backgroundColor: fallback}"
|
:style="{ backgroundColor: fallback }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" src="./color_input.scss"></style>
|
|
||||||
<script>
|
<script>
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||||
|
@ -93,21 +97,22 @@ export default {
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
computed: {
|
computed: {
|
||||||
present () {
|
present() {
|
||||||
return typeof this.modelValue !== 'undefined'
|
return typeof this.modelValue !== 'undefined'
|
||||||
},
|
},
|
||||||
validColor () {
|
validColor() {
|
||||||
return hex2rgb(this.modelValue || this.fallback)
|
return hex2rgb(this.modelValue || this.fallback)
|
||||||
},
|
},
|
||||||
transparentColor () {
|
transparentColor() {
|
||||||
return this.modelValue === 'transparent'
|
return this.modelValue === 'transparent'
|
||||||
},
|
},
|
||||||
computedColor () {
|
computedColor() {
|
||||||
return this.modelValue && this.modelValue.startsWith('--')
|
return this.modelValue && this.modelValue.startsWith('--')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" src="./color_input.scss"></style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.color-control {
|
.color-control {
|
||||||
|
|
|
@ -22,13 +22,12 @@ const ConfirmModal = {
|
||||||
type: String
|
type: String
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {},
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onCancel () {
|
onCancel() {
|
||||||
this.$emit('cancelled')
|
this.$emit('cancelled')
|
||||||
},
|
},
|
||||||
onAccept () {
|
onAccept() {
|
||||||
this.$emit('accepted')
|
this.$emit('accepted')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
</dialog-modal>
|
</dialog-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script src="./confirm_modal.js"></script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '../../_variables';
|
@import '../../_variables';
|
||||||
|
|
||||||
|
@ -35,5 +37,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script src="./confirm_modal.js"></script>
|
|
||||||
|
|
|
@ -43,11 +43,7 @@ import {
|
||||||
faThumbsUp
|
faThumbsUp
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(faAdjust, faExclamationTriangle, faThumbsUp)
|
||||||
faAdjust,
|
|
||||||
faExclamationTriangle,
|
|
||||||
faThumbsUp
|
|
||||||
)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -65,19 +61,35 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hint () {
|
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 level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||||
const context = this.$t('settings.style.common.contrast.context.text')
|
const context = this.$t('settings.style.common.contrast.context.text')
|
||||||
const ratio = this.contrast.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 () {
|
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 level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||||
const context = this.$t('settings.style.common.contrast.context.18pt')
|
const context = this.$t('settings.style.common.contrast.context.18pt')
|
||||||
const ratio = this.contrast.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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ const conversationPage = {
|
||||||
Conversation
|
Conversation
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
statusId () {
|
statusId() {
|
||||||
return this.$route.params.id
|
return this.$route.params.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,7 @@ import {
|
||||||
faChevronLeft
|
faChevronLeft
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
|
||||||
faAngleDoubleDown,
|
|
||||||
faAngleDoubleLeft,
|
|
||||||
faChevronLeft
|
|
||||||
)
|
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
const sortById = (a, b) => {
|
||||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||||
|
@ -39,16 +35,17 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
||||||
if (statusoid.type === 'retweet') {
|
if (statusoid.type === 'retweet') {
|
||||||
conversation = filter(
|
conversation = filter(
|
||||||
conversation,
|
conversation,
|
||||||
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
|
(status) =>
|
||||||
|
status.type === 'retweet' || status.id !== statusoid.retweeted_status.id
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||||
}
|
}
|
||||||
return conversation.filter(_ => _).sort(sortById)
|
return conversation.filter((_) => _).sort(sortById)
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = {
|
const conversation = {
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
|
@ -66,74 +63,78 @@ const conversation = {
|
||||||
'profileUserId',
|
'profileUserId',
|
||||||
'virtualHidden'
|
'virtualHidden'
|
||||||
],
|
],
|
||||||
created () {
|
created() {
|
||||||
if (this.isPage) {
|
if (this.isPage) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
maxDepthToShowByDefault () {
|
maxDepthToShowByDefault() {
|
||||||
// maxDepthInThread = max number of depths that is *visible*
|
// maxDepthInThread = max number of depths that is *visible*
|
||||||
// since our depth starts with 0 and "showing" means "showing children"
|
// since our depth starts with 0 and "showing" means "showing children"
|
||||||
// there is a -2 here
|
// there is a -2 here
|
||||||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||||
return maxDepth >= 1 ? maxDepth : 1
|
return maxDepth >= 1 ? maxDepth : 1
|
||||||
},
|
},
|
||||||
streamingEnabled () {
|
streamingEnabled() {
|
||||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
return (
|
||||||
|
this.mergedConfig.useStreamingApi &&
|
||||||
|
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||||
|
)
|
||||||
},
|
},
|
||||||
displayStyle () {
|
displayStyle() {
|
||||||
return this.$store.getters.mergedConfig.conversationDisplay
|
return this.$store.getters.mergedConfig.conversationDisplay
|
||||||
},
|
},
|
||||||
isTreeView () {
|
isTreeView() {
|
||||||
return !this.isLinearView
|
return !this.isLinearView
|
||||||
},
|
},
|
||||||
treeViewIsSimple () {
|
treeViewIsSimple() {
|
||||||
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||||
},
|
},
|
||||||
isLinearView () {
|
isLinearView() {
|
||||||
return this.displayStyle === 'linear'
|
return this.displayStyle === 'linear'
|
||||||
},
|
},
|
||||||
shouldFadeAncestors () {
|
shouldFadeAncestors() {
|
||||||
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||||
},
|
},
|
||||||
otherRepliesButtonPosition () {
|
otherRepliesButtonPosition() {
|
||||||
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||||
},
|
},
|
||||||
showOtherRepliesButtonBelowStatus () {
|
showOtherRepliesButtonBelowStatus() {
|
||||||
return this.otherRepliesButtonPosition === 'below'
|
return this.otherRepliesButtonPosition === 'below'
|
||||||
},
|
},
|
||||||
showOtherRepliesButtonInsideStatus () {
|
showOtherRepliesButtonInsideStatus() {
|
||||||
return this.otherRepliesButtonPosition === 'inside'
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
},
|
},
|
||||||
suspendable () {
|
suspendable() {
|
||||||
if (this.isTreeView) {
|
if (this.isTreeView) {
|
||||||
return Object.entries(this.statusContentProperties)
|
return Object.entries(this.statusContentProperties).every(
|
||||||
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (this.$refs.statusComponent && this.$refs.statusComponent[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 {
|
} else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideStatus () {
|
hideStatus() {
|
||||||
return this.virtualHidden && this.suspendable
|
return this.virtualHidden && this.suspendable
|
||||||
},
|
},
|
||||||
status () {
|
status() {
|
||||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||||
},
|
},
|
||||||
originalStatusId () {
|
originalStatusId() {
|
||||||
if (this.status.retweeted_status) {
|
if (this.status.retweeted_status) {
|
||||||
return this.status.retweeted_status.id
|
return this.status.retweeted_status.id
|
||||||
} else {
|
} else {
|
||||||
return this.statusId
|
return this.statusId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
conversationId () {
|
conversationId() {
|
||||||
return this.getConversationId(this.statusId)
|
return this.getConversationId(this.statusId)
|
||||||
},
|
},
|
||||||
conversation () {
|
conversation() {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -142,155 +143,203 @@ const conversation = {
|
||||||
return [this.status]
|
return [this.status]
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
|
const conversation = clone(
|
||||||
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
|
this.$store.state.statuses.conversationsObject[this.conversationId]
|
||||||
|
)
|
||||||
|
const statusIndex = findIndex(conversation, {
|
||||||
|
id: this.originalStatusId
|
||||||
|
})
|
||||||
if (statusIndex !== -1) {
|
if (statusIndex !== -1) {
|
||||||
conversation[statusIndex] = this.status
|
conversation[statusIndex] = this.status
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortAndFilterConversation(conversation, this.status)
|
return sortAndFilterConversation(conversation, this.status)
|
||||||
},
|
},
|
||||||
statusMap () {
|
statusMap() {
|
||||||
return this.conversation.reduce((res, s) => {
|
return this.conversation.reduce((res, s) => {
|
||||||
res[s.id] = s
|
res[s.id] = s
|
||||||
return res
|
return res
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
threadTree () {
|
threadTree() {
|
||||||
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
const reverseLookupTable = this.conversation.reduce(
|
||||||
table[status.id] = index
|
(table, status, index) => {
|
||||||
return table
|
table[status.id] = index
|
||||||
}, {})
|
return table
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
const threads = this.conversation.reduce((a, cur) => {
|
const threads = this.conversation.reduce(
|
||||||
const id = cur.id
|
(a, cur) => {
|
||||||
a.forest[id] = this.getReplies(id)
|
const id = cur.id
|
||||||
.map(s => s.id)
|
a.forest[id] = this.getReplies(id).map((s) => s.id)
|
||||||
|
|
||||||
return a
|
return a
|
||||||
}, {
|
},
|
||||||
forest: {}
|
{
|
||||||
})
|
forest: {}
|
||||||
|
|
||||||
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
|
||||||
if (processed[id]) {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
processed[id] = true
|
const walk = (forest, topLevel, depth = 0, processed = {}) =>
|
||||||
return [{
|
topLevel
|
||||||
status: this.conversation[reverseLookupTable[id]],
|
.map((id) => {
|
||||||
id,
|
if (processed[id]) {
|
||||||
depth
|
return []
|
||||||
}, 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))
|
processed[id] = true
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
status: this.conversation[reverseLookupTable[id]],
|
||||||
|
id,
|
||||||
|
depth
|
||||||
|
},
|
||||||
|
walk(forest, forest[id], depth + 1, processed)
|
||||||
|
].reduce((a, b) => a.concat(b), [])
|
||||||
|
})
|
||||||
|
.reduce((a, b) => a.concat(b), [])
|
||||||
|
|
||||||
|
const linearized = walk(
|
||||||
|
threads.forest,
|
||||||
|
this.topLevel.map((k) => k.id)
|
||||||
|
)
|
||||||
|
|
||||||
return linearized
|
return linearized
|
||||||
},
|
},
|
||||||
replyIds () {
|
replyIds() {
|
||||||
return this.conversation.map(k => k.id)
|
return this.conversation
|
||||||
|
.map((k) => k.id)
|
||||||
.reduce((res, id) => {
|
.reduce((res, id) => {
|
||||||
res[id] = (this.replies[id] || []).map(k => k.id)
|
res[id] = (this.replies[id] || []).map((k) => k.id)
|
||||||
return res
|
return res
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
totalReplyCount () {
|
totalReplyCount() {
|
||||||
const sizes = {}
|
const sizes = {}
|
||||||
const subTreeSizeFor = (id) => {
|
const subTreeSizeFor = (id) => {
|
||||||
if (sizes[id]) {
|
if (sizes[id]) {
|
||||||
return 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]
|
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) => {
|
return Object.keys(sizes).reduce((res, id) => {
|
||||||
res[id] = sizes[id] - 1 // exclude itself
|
res[id] = sizes[id] - 1 // exclude itself
|
||||||
return res
|
return res
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
totalReplyDepth () {
|
totalReplyDepth() {
|
||||||
const depths = {}
|
const depths = {}
|
||||||
const subTreeDepthFor = (id) => {
|
const subTreeDepthFor = (id) => {
|
||||||
if (depths[id]) {
|
if (depths[id]) {
|
||||||
return 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]
|
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) => {
|
return Object.keys(depths).reduce((res, id) => {
|
||||||
res[id] = depths[id] - 1 // exclude itself
|
res[id] = depths[id] - 1 // exclude itself
|
||||||
return res
|
return res
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
depths () {
|
depths() {
|
||||||
return this.threadTree.reduce((a, k) => {
|
return this.threadTree.reduce((a, k) => {
|
||||||
a[k.id] = k.depth
|
a[k.id] = k.depth
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
topLevel () {
|
topLevel() {
|
||||||
const topLevel = this.conversation.reduce((tl, cur) =>
|
const topLevel = this.conversation.reduce(
|
||||||
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
|
(tl, cur) =>
|
||||||
|
tl.filter(
|
||||||
|
(k) =>
|
||||||
|
this.getReplies(cur.id)
|
||||||
|
.map((v) => v.id)
|
||||||
|
.indexOf(k.id) === -1
|
||||||
|
),
|
||||||
|
this.conversation
|
||||||
|
)
|
||||||
return topLevel
|
return topLevel
|
||||||
},
|
},
|
||||||
otherTopLevelCount () {
|
otherTopLevelCount() {
|
||||||
return this.topLevel.length - 1
|
return this.topLevel.length - 1
|
||||||
},
|
},
|
||||||
showingTopLevel () {
|
showingTopLevel() {
|
||||||
if (this.canDive && this.diveRoot) {
|
if (this.canDive && this.diveRoot) {
|
||||||
return [this.statusMap[this.diveRoot]]
|
return [this.statusMap[this.diveRoot]]
|
||||||
}
|
}
|
||||||
return this.topLevel
|
return this.topLevel
|
||||||
},
|
},
|
||||||
diveRoot () {
|
diveRoot() {
|
||||||
const statusId = this.inlineDivePosition || this.statusId
|
const statusId = this.inlineDivePosition || this.statusId
|
||||||
const isTopLevel = !this.parentOf(statusId)
|
const isTopLevel = !this.parentOf(statusId)
|
||||||
return isTopLevel ? null : statusId
|
return isTopLevel ? null : statusId
|
||||||
},
|
},
|
||||||
diveDepth () {
|
diveDepth() {
|
||||||
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||||
},
|
},
|
||||||
diveMode () {
|
diveMode() {
|
||||||
return this.canDive && !!this.diveRoot
|
return this.canDive && !!this.diveRoot
|
||||||
},
|
},
|
||||||
shouldShowAllConversationButton () {
|
shouldShowAllConversationButton() {
|
||||||
// The "show all conversation" button tells the user that there exist
|
// 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
|
// 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 () {
|
shouldShowAncestors() {
|
||||||
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
return (
|
||||||
|
this.isTreeView &&
|
||||||
|
this.isExpanded &&
|
||||||
|
this.ancestorsOf(this.diveRoot).length
|
||||||
|
)
|
||||||
},
|
},
|
||||||
replies () {
|
replies() {
|
||||||
let i = 1
|
let i = 1
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
|
return reduce(
|
||||||
/* eslint-disable camelcase */
|
this.conversation,
|
||||||
const irid = in_reply_to_status_id
|
(result, { id, in_reply_to_status_id }) => {
|
||||||
/* eslint-enable camelcase */
|
/* eslint-disable camelcase */
|
||||||
if (irid) {
|
const irid = in_reply_to_status_id
|
||||||
result[irid] = result[irid] || []
|
/* eslint-enable camelcase */
|
||||||
result[irid].push({
|
if (irid) {
|
||||||
name: `#${i}`,
|
result[irid] = result[irid] || []
|
||||||
id: id
|
result[irid].push({
|
||||||
})
|
name: `#${i}`,
|
||||||
}
|
id: id
|
||||||
i++
|
})
|
||||||
return result
|
}
|
||||||
}, {})
|
i++
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
isExpanded () {
|
isExpanded() {
|
||||||
return !!(this.expanded || this.isPage)
|
return !!(this.expanded || this.isPage)
|
||||||
},
|
},
|
||||||
hiddenStyle () {
|
hiddenStyle() {
|
||||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||||
return this.virtualHidden ? { height } : {}
|
return this.virtualHidden ? { height } : {}
|
||||||
},
|
},
|
||||||
threadDisplayStatus () {
|
threadDisplayStatus() {
|
||||||
return this.conversation.reduce((a, k) => {
|
return this.conversation.reduce((a, k) => {
|
||||||
const id = k.id
|
const id = k.id
|
||||||
const depth = this.depths[id]
|
const depth = this.depths[id]
|
||||||
|
@ -298,7 +347,7 @@ const conversation = {
|
||||||
if (this.threadDisplayStatusObject[id]) {
|
if (this.threadDisplayStatusObject[id]) {
|
||||||
return this.threadDisplayStatusObject[id]
|
return this.threadDisplayStatusObject[id]
|
||||||
}
|
}
|
||||||
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
|
||||||
return 'showing'
|
return 'showing'
|
||||||
} else {
|
} else {
|
||||||
return 'hidden'
|
return 'hidden'
|
||||||
|
@ -309,7 +358,7 @@ const conversation = {
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
statusContentProperties () {
|
statusContentProperties() {
|
||||||
return this.conversation.reduce((a, k) => {
|
return this.conversation.reduce((a, k) => {
|
||||||
const id = k.id
|
const id = k.id
|
||||||
const props = (() => {
|
const props = (() => {
|
||||||
|
@ -334,20 +383,20 @@ const conversation = {
|
||||||
return a
|
return a
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
canDive () {
|
canDive() {
|
||||||
return this.isTreeView && this.isExpanded
|
return this.isTreeView && this.isExpanded
|
||||||
},
|
},
|
||||||
focused () {
|
focused() {
|
||||||
return (id) => {
|
return (id) => {
|
||||||
return (this.isExpanded) && id === this.highlight
|
return this.isExpanded && id === this.highlight
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeHighlight () {
|
maybeHighlight() {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -355,53 +404,59 @@ const conversation = {
|
||||||
ThreadTree
|
ThreadTree
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
statusId (newVal, oldVal) {
|
statusId(newVal, oldVal) {
|
||||||
const newConversationId = this.getConversationId(newVal)
|
const newConversationId = this.getConversationId(newVal)
|
||||||
const oldConversationId = this.getConversationId(oldVal)
|
const oldConversationId = this.getConversationId(oldVal)
|
||||||
if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
|
if (
|
||||||
|
newConversationId &&
|
||||||
|
oldConversationId &&
|
||||||
|
newConversationId === oldConversationId
|
||||||
|
) {
|
||||||
this.setHighlight(this.originalStatusId)
|
this.setHighlight(this.originalStatusId)
|
||||||
} else {
|
} else {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expanded (value) {
|
expanded(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
} else {
|
} else {
|
||||||
this.resetDisplayState()
|
this.resetDisplayState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
virtualHidden (value) {
|
virtualHidden(value) {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch('setVirtualHeight', {
|
||||||
'setVirtualHeight',
|
statusId: this.statusId,
|
||||||
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
|
height: `${this.$el.clientHeight}px`
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchConversation () {
|
fetchConversation() {
|
||||||
if (this.status) {
|
if (this.status) {
|
||||||
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
|
this.$store.state.api.backendInteractor
|
||||||
|
.fetchConversation({ id: this.statusId })
|
||||||
.then(({ ancestors, descendants }) => {
|
.then(({ ancestors, descendants }) => {
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||||
this.setHighlight(this.originalStatusId)
|
this.setHighlight(this.originalStatusId)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
|
this.$store.state.api.backendInteractor
|
||||||
|
.fetchStatus({ id: this.statusId })
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getReplies (id) {
|
getReplies(id) {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
getHighlight () {
|
getHighlight() {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight(id) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
|
|
||||||
|
@ -412,32 +467,38 @@ const conversation = {
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
toggleExpanded () {
|
toggleExpanded() {
|
||||||
this.expanded = !this.expanded
|
this.expanded = !this.expanded
|
||||||
},
|
},
|
||||||
getConversationId (statusId) {
|
getConversationId(statusId) {
|
||||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
return get(
|
||||||
|
status,
|
||||||
|
'retweeted_status.statusnet_conversation_id',
|
||||||
|
get(status, 'statusnet_conversation_id')
|
||||||
|
)
|
||||||
},
|
},
|
||||||
setThreadDisplay (id, nextStatus) {
|
setThreadDisplay(id, nextStatus) {
|
||||||
this.threadDisplayStatusObject = {
|
this.threadDisplayStatusObject = {
|
||||||
...this.threadDisplayStatusObject,
|
...this.threadDisplayStatusObject,
|
||||||
[id]: nextStatus
|
[id]: nextStatus
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleThreadDisplay (id) {
|
toggleThreadDisplay(id) {
|
||||||
const curStatus = this.threadDisplayStatus[id]
|
const curStatus = this.threadDisplayStatus[id]
|
||||||
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||||
this.setThreadDisplay(id, nextStatus)
|
this.setThreadDisplay(id, nextStatus)
|
||||||
},
|
},
|
||||||
setThreadDisplayRecursively (id, nextStatus) {
|
setThreadDisplayRecursively(id, nextStatus) {
|
||||||
this.setThreadDisplay(id, nextStatus)
|
this.setThreadDisplay(id, nextStatus)
|
||||||
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
|
this.getReplies(id)
|
||||||
|
.map((k) => k.id)
|
||||||
|
.map((id) => this.setThreadDisplayRecursively(id, nextStatus))
|
||||||
},
|
},
|
||||||
showThreadRecursively (id) {
|
showThreadRecursively(id) {
|
||||||
this.setThreadDisplayRecursively(id, 'showing')
|
this.setThreadDisplayRecursively(id, 'showing')
|
||||||
},
|
},
|
||||||
setStatusContentProperty (id, name, value) {
|
setStatusContentProperty(id, name, value) {
|
||||||
this.statusContentPropertiesObject = {
|
this.statusContentPropertiesObject = {
|
||||||
...this.statusContentPropertiesObject,
|
...this.statusContentPropertiesObject,
|
||||||
[id]: {
|
[id]: {
|
||||||
|
@ -446,10 +507,14 @@ const conversation = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleStatusContentProperty (id, name) {
|
toggleStatusContentProperty(id, name) {
|
||||||
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
this.setStatusContentProperty(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
!this.statusContentProperties[id][name]
|
||||||
|
)
|
||||||
},
|
},
|
||||||
leastVisibleAncestor (id) {
|
leastVisibleAncestor(id) {
|
||||||
let cur = id
|
let cur = id
|
||||||
let parent = this.parentOf(cur)
|
let parent = this.parentOf(cur)
|
||||||
while (cur) {
|
while (cur) {
|
||||||
|
@ -463,18 +528,20 @@ const conversation = {
|
||||||
// nothing found, fall back to toplevel
|
// nothing found, fall back to toplevel
|
||||||
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||||
},
|
},
|
||||||
diveIntoStatus (id, preventScroll) {
|
diveIntoStatus(id, preventScroll) {
|
||||||
this.tryScrollTo(id)
|
this.tryScrollTo(id)
|
||||||
},
|
},
|
||||||
diveToTopLevel () {
|
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
|
// only used when we are not on a page
|
||||||
undive () {
|
undive() {
|
||||||
this.inlineDivePosition = null
|
this.inlineDivePosition = null
|
||||||
this.setHighlight(this.statusId)
|
this.setHighlight(this.statusId)
|
||||||
},
|
},
|
||||||
tryScrollTo (id) {
|
tryScrollTo(id) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -503,13 +570,13 @@ const conversation = {
|
||||||
this.setHighlight(id)
|
this.setHighlight(id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
goToCurrent () {
|
goToCurrent() {
|
||||||
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||||
},
|
},
|
||||||
statusById (id) {
|
statusById(id) {
|
||||||
return this.statusMap[id]
|
return this.statusMap[id]
|
||||||
},
|
},
|
||||||
parentOf (id) {
|
parentOf(id) {
|
||||||
const status = this.statusById(id)
|
const status = this.statusById(id)
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -520,11 +587,11 @@ const conversation = {
|
||||||
}
|
}
|
||||||
return parentId
|
return parentId
|
||||||
},
|
},
|
||||||
parentOrSelf (id) {
|
parentOrSelf(id) {
|
||||||
return this.parentOf(id) || id
|
return this.parentOf(id) || id
|
||||||
},
|
},
|
||||||
// Ancestors of some status, from top to bottom
|
// Ancestors of some status, from top to bottom
|
||||||
ancestorsOf (id) {
|
ancestorsOf(id) {
|
||||||
const ancestors = []
|
const ancestors = []
|
||||||
let cur = this.parentOf(id)
|
let cur = this.parentOf(id)
|
||||||
while (cur) {
|
while (cur) {
|
||||||
|
@ -533,7 +600,7 @@ const conversation = {
|
||||||
}
|
}
|
||||||
return ancestors
|
return ancestors
|
||||||
},
|
},
|
||||||
topLevelAncestorOrSelfId (id) {
|
topLevelAncestorOrSelfId(id) {
|
||||||
let cur = id
|
let cur = id
|
||||||
let parent = this.parentOf(id)
|
let parent = this.parentOf(id)
|
||||||
while (parent) {
|
while (parent) {
|
||||||
|
@ -542,7 +609,7 @@ const conversation = {
|
||||||
}
|
}
|
||||||
return cur
|
return cur
|
||||||
},
|
},
|
||||||
resetDisplayState () {
|
resetDisplayState() {
|
||||||
this.undive()
|
this.undive()
|
||||||
this.threadDisplayStatusObject = {}
|
this.threadDisplayStatusObject = {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
v-if="!hideStatus"
|
v-if="!hideStatus"
|
||||||
:style="hiddenStyle"
|
:style="hiddenStyle"
|
||||||
class="Conversation"
|
class="Conversation"
|
||||||
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
|
:class="{ '-expanded': isExpanded, panel: isExpanded }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isExpanded"
|
v-if="isExpanded"
|
||||||
|
@ -35,13 +35,15 @@
|
||||||
@click.prevent="diveToTopLevel"
|
@click.prevent="diveToTopLevel"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<FAIcon
|
<FAIcon icon="angle-double-left" />
|
||||||
icon="angle-double-left"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #text>
|
<template #text>
|
||||||
<span>
|
<span>
|
||||||
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
{{
|
||||||
|
$tc('status.show_all_conversation', otherTopLevelCount, {
|
||||||
|
numStatus: otherTopLevelCount
|
||||||
|
})
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
@ -54,14 +56,20 @@
|
||||||
v-for="status in ancestorsOf(diveRoot)"
|
v-for="status in ancestorsOf(diveRoot)"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
class="thread-ancestor"
|
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
|
<status
|
||||||
ref="statusComponent"
|
ref="statusComponent"
|
||||||
:inline-expanded="collapsable && isExpanded"
|
:inline-expanded="collapsable && isExpanded"
|
||||||
:statusoid="status"
|
:statusoid="status"
|
||||||
:expandable="!isExpanded"
|
:expandable="!isExpanded"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
:show-pinned="
|
||||||
|
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||||
|
"
|
||||||
:focused="focused(status.id)"
|
:focused="focused(status.id)"
|
||||||
:in-conversation="isExpanded"
|
:in-conversation="isExpanded"
|
||||||
:highlight="getHighlight()"
|
:highlight="getHighlight()"
|
||||||
|
@ -69,7 +77,6 @@
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
class="conversation-status status-fadein panel-body"
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
:simple-tree="treeViewIsSimple"
|
:simple-tree="treeViewIsSimple"
|
||||||
:toggle-thread-display="toggleThreadDisplay"
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
@ -78,28 +85,47 @@
|
||||||
:total-reply-depth="totalReplyDepth"
|
:total-reply-depth="totalReplyDepth"
|
||||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
:dive="() => diveIntoStatus(status.id)"
|
:dive="() => diveIntoStatus(status.id)"
|
||||||
|
:controlled-showing-tall="
|
||||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
statusContentProperties[status.id].showingTall
|
||||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
"
|
||||||
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
:controlled-expanding-subject="
|
||||||
|
statusContentProperties[status.id].expandingSubject
|
||||||
|
"
|
||||||
|
:controlled-showing-long-subject="
|
||||||
|
statusContentProperties[status.id].showingLongSubject
|
||||||
|
"
|
||||||
:controlled-replying="statusContentProperties[status.id].replying"
|
:controlled-replying="statusContentProperties[status.id].replying"
|
||||||
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
:controlled-media-playing="
|
||||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
statusContentProperties[status.id].mediaPlaying
|
||||||
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
"
|
||||||
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
:controlled-toggle-showing-tall="
|
||||||
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
() => toggleStatusContentProperty(status.id, 'showingTall')
|
||||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
"
|
||||||
|
: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"
|
@goto="setHighlight"
|
||||||
@toggleExpanded="toggleExpanded"
|
@toggleExpanded="toggleExpanded"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
v-if="
|
||||||
|
showOtherRepliesButtonBelowStatus &&
|
||||||
|
getReplies(status.id).length > 1
|
||||||
|
"
|
||||||
class="thread-ancestor-dive-box"
|
class="thread-ancestor-dive-box"
|
||||||
>
|
>
|
||||||
<div
|
<div class="thread-ancestor-dive-box-inner">
|
||||||
class="thread-ancestor-dive-box-inner"
|
|
||||||
>
|
|
||||||
<i18n-t
|
<i18n-t
|
||||||
tag="button"
|
tag="button"
|
||||||
scope="global"
|
scope="global"
|
||||||
|
@ -108,13 +134,17 @@
|
||||||
@click.prevent="diveIntoStatus(status.id)"
|
@click.prevent="diveIntoStatus(status.id)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<FAIcon
|
<FAIcon icon="angle-double-right" />
|
||||||
icon="angle-double-right"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #text>
|
<template #text>
|
||||||
<span>
|
<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>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
|
@ -127,7 +157,6 @@
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
ref="statusComponent"
|
ref="statusComponent"
|
||||||
:depth="0"
|
:depth="0"
|
||||||
|
|
||||||
:status="status"
|
:status="status"
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:conversation="conversation"
|
:conversation="conversation"
|
||||||
|
@ -135,13 +164,11 @@
|
||||||
:is-expanded="isExpanded"
|
:is-expanded="isExpanded"
|
||||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
:focused="focused"
|
:focused="focused"
|
||||||
:get-replies="getReplies"
|
:get-replies="getReplies"
|
||||||
:highlight="maybeHighlight"
|
:highlight="maybeHighlight"
|
||||||
:set-highlight="setHighlight"
|
:set-highlight="setHighlight"
|
||||||
:toggle-expanded="toggleExpanded"
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
:simple="treeViewIsSimple"
|
:simple="treeViewIsSimple"
|
||||||
:toggle-thread-display="toggleThreadDisplay"
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
@ -165,7 +192,9 @@
|
||||||
:inline-expanded="collapsable && isExpanded"
|
:inline-expanded="collapsable && isExpanded"
|
||||||
:statusoid="status"
|
:statusoid="status"
|
||||||
:expandable="!isExpanded"
|
:expandable="!isExpanded"
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
:show-pinned="
|
||||||
|
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||||
|
"
|
||||||
:focused="focused(status.id)"
|
:focused="focused(status.id)"
|
||||||
:in-conversation="isExpanded"
|
:in-conversation="isExpanded"
|
||||||
:highlight="getHighlight()"
|
:highlight="getHighlight()"
|
||||||
|
@ -173,7 +202,6 @@
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
class="conversation-status status-fadein panel-body"
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
:toggle-thread-display="toggleThreadDisplay"
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
:thread-display-status="threadDisplayStatus"
|
:thread-display-status="threadDisplayStatus"
|
||||||
:show-thread-recursively="showThreadRecursively"
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
@ -182,7 +210,6 @@
|
||||||
:status-content-properties="statusContentProperties"
|
:status-content-properties="statusContentProperties"
|
||||||
:set-status-content-property="setStatusContentProperty"
|
:set-status-content-property="setStatusContentProperty"
|
||||||
:toggle-status-content-property="toggleStatusContentProperty"
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
|
||||||
@goto="setHighlight"
|
@goto="setHighlight"
|
||||||
@toggleExpanded="toggleExpanded"
|
@toggleExpanded="toggleExpanded"
|
||||||
/>
|
/>
|
||||||
|
@ -233,7 +260,8 @@
|
||||||
border-bottom-color: var(--border, $fallback--border);
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
/* Make the button stretch along the whole row */
|
/* Make the button stretch along the whole row */
|
||||||
&, &-inner {
|
&,
|
||||||
|
&-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -253,8 +281,7 @@
|
||||||
.thread-ancestor-has-other-replies .conversation-status,
|
.thread-ancestor-has-other-replies .conversation-status,
|
||||||
.thread-ancestor:last-child .conversation-status,
|
.thread-ancestor:last-child .conversation-status,
|
||||||
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||||
&:last-child .conversation-status,
|
&:last-child .conversation-status {
|
||||||
&.-expanded .thread-tree .conversation-status {
|
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +298,8 @@
|
||||||
border-left-color: $fallback--cRed;
|
border-left-color: $fallback--cRed;
|
||||||
border-left-color: var(--cRed, $fallback--cRed);
|
border-left-color: var(--cRed, $fallback--cRed);
|
||||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
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);
|
border-bottom: 1px solid var(--border, $fallback--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import SearchBar from 'components/search_bar/search_bar.vue'
|
import SearchBar from 'components/search_bar/search_bar.vue'
|
||||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
publicTimelineVisible,
|
||||||
|
federatedTimelineVisible,
|
||||||
|
bubbleTimelineVisible
|
||||||
|
} from '../../lib/timeline_visibility'
|
||||||
import {
|
import {
|
||||||
faSignInAlt,
|
faSignInAlt,
|
||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
|
@ -19,6 +24,7 @@ import {
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faUserTie
|
faUserTie
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faSignInAlt,
|
faSignInAlt,
|
||||||
|
@ -46,76 +52,103 @@ export default {
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
searchBarHidden: true,
|
searchBarHidden: true,
|
||||||
supportsMask: window.CSS && window.CSS.supports && (
|
supportsMask:
|
||||||
window.CSS.supports('mask-size', 'contain') ||
|
window.CSS &&
|
||||||
|
window.CSS.supports &&
|
||||||
|
(window.CSS.supports('mask-size', 'contain') ||
|
||||||
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
||||||
window.CSS.supports('-moz-mask-size', 'contain') ||
|
window.CSS.supports('-moz-mask-size', 'contain') ||
|
||||||
window.CSS.supports('-ms-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
|
showingConfirmLogout: false
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
enableMask() {
|
||||||
logoStyle () {
|
return this.supportsMask && this.$store.state.instance.logoMask
|
||||||
|
},
|
||||||
|
logoStyle() {
|
||||||
return {
|
return {
|
||||||
'visibility': this.enableMask ? 'hidden' : 'visible'
|
visibility: this.enableMask ? 'hidden' : 'visible'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logoMaskStyle () {
|
logoMaskStyle() {
|
||||||
return this.enableMask ? {
|
return this.enableMask
|
||||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
? {
|
||||||
} : {
|
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||||
'background-color': this.enableMask ? '' : 'transparent'
|
}
|
||||||
}
|
: {
|
||||||
|
'background-color': this.enableMask ? '' : 'transparent'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
logoBgStyle () {
|
logoBgStyle() {
|
||||||
return Object.assign({
|
return Object.assign(
|
||||||
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
{
|
||||||
opacity: this.searchBarHidden ? 1 : 0
|
margin: `${this.$store.state.instance.logoMargin} 0`,
|
||||||
}, this.enableMask ? {} : {
|
opacity: this.searchBarHidden ? 1 : 0
|
||||||
'background-color': this.enableMask ? '' : 'transparent'
|
},
|
||||||
})
|
this.enableMask
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
'background-color': this.enableMask ? '' : 'transparent'
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
logo () { return this.$store.state.instance.logo },
|
logo() {
|
||||||
mergedConfig () {
|
return this.$store.state.instance.logo
|
||||||
|
},
|
||||||
|
mergedConfig() {
|
||||||
return this.$store.getters.mergedConfig
|
return this.$store.getters.mergedConfig
|
||||||
},
|
},
|
||||||
sitename () { return this.$store.state.instance.name },
|
sitename() {
|
||||||
showNavShortcuts () {
|
return this.$store.state.instance.name
|
||||||
|
},
|
||||||
|
showNavShortcuts() {
|
||||||
return this.mergedConfig.showNavShortcuts
|
return this.mergedConfig.showNavShortcuts
|
||||||
},
|
},
|
||||||
showWiderShortcuts () {
|
showWiderShortcuts() {
|
||||||
return this.mergedConfig.showWiderShortcuts
|
return this.mergedConfig.showWiderShortcuts
|
||||||
},
|
},
|
||||||
hideSiteFavicon () {
|
hideSiteFavicon() {
|
||||||
return this.mergedConfig.hideSiteFavicon
|
return this.mergedConfig.hideSiteFavicon
|
||||||
},
|
},
|
||||||
hideSiteName () {
|
hideSiteName() {
|
||||||
return this.mergedConfig.hideSiteName
|
return this.mergedConfig.hideSiteName
|
||||||
},
|
},
|
||||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
hideSitename() {
|
||||||
logoLeft () { return this.$store.state.instance.logoLeft },
|
return this.$store.state.instance.hideSitename
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
},
|
||||||
privateMode () { return this.$store.state.instance.private },
|
logoLeft() {
|
||||||
shouldConfirmLogout () {
|
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
|
return this.$store.getters.mergedConfig.modalOnLogout
|
||||||
},
|
},
|
||||||
showBubbleTimeline () {
|
showBubbleTimeline() {
|
||||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||||
}
|
},
|
||||||
|
...mapState({
|
||||||
|
publicTimelineVisible,
|
||||||
|
federatedTimelineVisible,
|
||||||
|
bubbleTimelineVisible
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToTop () {
|
scrollToTop() {
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
},
|
},
|
||||||
onSearchBarToggled (hidden) {
|
onSearchBarToggled(hidden) {
|
||||||
this.searchBarHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal() {
|
||||||
this.$store.dispatch('openSettingsModal')
|
this.$store.dispatch('openSettingsModal')
|
||||||
},
|
},
|
||||||
openModModal () {
|
openModModal() {
|
||||||
this.$store.dispatch('openModModal')
|
this.$store.dispatch('openModModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: var(--navbar-height);
|
grid-template-rows: var(--navbar-height);
|
||||||
grid-template-columns: 2fr auto 2fr;
|
grid-template-columns: 2fr auto 2fr;
|
||||||
grid-template-areas: "nav-left logo actions";
|
grid-template-areas: 'nav-left logo actions';
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0 1.2em;
|
padding: 0 1.2em;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -24,11 +24,12 @@
|
||||||
|
|
||||||
&.-logoLeft .inner-nav {
|
&.-logoLeft .inner-nav {
|
||||||
grid-template-columns: auto 2fr 2fr;
|
grid-template-columns: auto 2fr 2fr;
|
||||||
grid-template-areas: "logo nav-left actions";
|
grid-template-areas: 'logo nav-left actions';
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-default {
|
.button-default {
|
||||||
&, svg {
|
&,
|
||||||
|
svg {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnTopBarText, $fallback--text);
|
color: var(--btnTopBarText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnToggledTopBarText, $fallback--text);
|
color: var(--btnToggledTopBarText, $fallback--text);
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
background-color: var(--btnToggledTopBar, $fallback--fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,13 +89,25 @@
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
&.router-link-active {
|
&.router-link-active {
|
||||||
font-size: 1.2em;
|
// box-shadow: 0 -2px 0 var(--selectedMenuText, $fallback--text) inset;
|
||||||
margin-top: 0.05em;
|
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 {
|
.svg-inline--fa {
|
||||||
font-weight: bolder;
|
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--selectedMenuText, $fallback--text);
|
color: var(--selectedMenuText, $fallback--text);
|
||||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
<img
|
<img
|
||||||
v-if="!hideSiteFavicon"
|
v-if="!hideSiteFavicon"
|
||||||
class="favicon"
|
class="favicon"
|
||||||
src="/favicon.png"
|
src="/favicon.svg"
|
||||||
>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="!hideSiteName"
|
v-if="!hideSiteName"
|
||||||
class="site-name"
|
class="site-name"
|
||||||
|
@ -44,6 +44,7 @@
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="publicTimelineVisible"
|
||||||
:to="{ name: 'public-timeline' }"
|
:to="{ name: 'public-timeline' }"
|
||||||
class="nav-icon"
|
class="nav-icon"
|
||||||
>
|
>
|
||||||
|
@ -55,7 +56,7 @@
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="currentUser && showBubbleTimeline"
|
v-if="bubbleTimelineVisible"
|
||||||
:to="{ name: 'bubble-timeline' }"
|
:to="{ name: 'bubble-timeline' }"
|
||||||
class="nav-icon"
|
class="nav-icon"
|
||||||
>
|
>
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="federatedTimelineVisible"
|
||||||
:to="{ name: 'public-external-timeline' }"
|
:to="{ name: 'public-external-timeline' }"
|
||||||
class="nav-icon"
|
class="nav-icon"
|
||||||
>
|
>
|
||||||
|
@ -91,7 +93,7 @@
|
||||||
<img
|
<img
|
||||||
:src="logo"
|
:src="logo"
|
||||||
:style="logoStyle"
|
:style="logoStyle"
|
||||||
>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="item right actions">
|
<div class="item right actions">
|
||||||
<search-bar
|
<search-bar
|
||||||
|
@ -106,7 +108,10 @@
|
||||||
<router-link
|
<router-link
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
class="nav-icon"
|
class="nav-icon"
|
||||||
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
|
:to="{
|
||||||
|
name: 'interactions',
|
||||||
|
params: { username: currentUser.screen_name }
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
|
@ -152,7 +157,10 @@
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<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"
|
class="button-unstyled nav-icon"
|
||||||
@click.stop="openModModal"
|
@click.stop="openModModal"
|
||||||
>
|
>
|
||||||
|
|
|
@ -31,14 +31,14 @@
|
||||||
.dark-overlay {
|
.dark-overlay {
|
||||||
&::before {
|
&::before {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
content: " ";
|
content: ' ';
|
||||||
display: block;
|
display: block;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
left: 0;
|
left: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
background: rgba(27,31,35,.5);
|
background: rgba(27, 31, 35, 0.5);
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
|
|
||||||
.dialog-modal-footer {
|
.dialog-modal-footer {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: .5em .5em;
|
padding: 0.5em 0.5em;
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--bg, $fallback--bg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
border-top: 1px solid $fallback--border;
|
border-top: 1px solid $fallback--border;
|
||||||
|
@ -84,9 +84,8 @@
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin-left: .5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Timeline from '../timeline/timeline.vue'
|
||||||
|
|
||||||
const DMs = {
|
const DMs = {
|
||||||
computed: {
|
computed: {
|
||||||
timeline () {
|
timeline() {
|
||||||
return this.$store.state.statuses.timelines.dms
|
return this.$store.state.statuses.timelines.dms
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,18 +6,18 @@ const DomainMuteCard = {
|
||||||
ProgressButton
|
ProgressButton
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user() {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
muted () {
|
muted() {
|
||||||
return this.user.domainMutes.includes(this.domain)
|
return this.user.domainMutes.includes(this.domain)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
unmuteDomain () {
|
unmuteDomain() {
|
||||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||||
},
|
},
|
||||||
muteDomain () {
|
muteDomain() {
|
||||||
return this.$store.dispatch('muteDomain', this.domain)
|
return this.$store.dispatch('muteDomain', this.domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
>
|
>
|
||||||
{{ $t('domain_mute_card.unmute') }}
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('domain_mute_card.unmute_progress') }}
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
>
|
>
|
||||||
{{ $t('domain_mute_card.mute') }}
|
{{ $t('domain_mute_card.mute') }}
|
||||||
<template v-slot:progress>
|
<template #progress>
|
||||||
{{ $t('domain_mute_card.mute_progress') }}
|
{{ $t('domain_mute_card.mute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
|
|
@ -8,27 +8,27 @@ const EditStatusModal = {
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
Modal
|
Modal
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
resettingForm: false
|
resettingForm: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isLoggedIn () {
|
isLoggedIn() {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
modalActivated () {
|
modalActivated() {
|
||||||
return this.$store.state.editStatus.modalActivated
|
return this.$store.state.editStatus.modalActivated
|
||||||
},
|
},
|
||||||
isFormVisible () {
|
isFormVisible() {
|
||||||
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||||
},
|
},
|
||||||
params () {
|
params() {
|
||||||
return this.$store.state.editStatus.params || {}
|
return this.$store.state.editStatus.params || {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
params (newVal, oldVal) {
|
params(newVal, oldVal) {
|
||||||
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||||
this.resettingForm = true
|
this.resettingForm = true
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -36,14 +36,16 @@ const EditStatusModal = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isFormVisible (val) {
|
isFormVisible(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
this.$nextTick(
|
||||||
|
() => this.$el && this.$el.querySelector('textarea').focus()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||||
const params = {
|
const params = {
|
||||||
store: this.$store,
|
store: this.$store,
|
||||||
statusId: this.$store.state.editStatus.params.statusId,
|
statusId: this.$store.state.editStatus.params.statusId,
|
||||||
|
@ -55,7 +57,8 @@ const EditStatusModal = {
|
||||||
contentType
|
contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusPosterService.editStatus(params)
|
return statusPosterService
|
||||||
|
.editStatus(params)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
@ -66,7 +69,7 @@ const EditStatusModal = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
closeModal () {
|
closeModal() {
|
||||||
this.$store.dispatch('closeEditStatusModal')
|
this.$store.dispatch('closeEditStatusModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
<PostStatusForm
|
<PostStatusForm
|
||||||
class="panel-body"
|
class="panel-body"
|
||||||
v-bind="params"
|
v-bind="params"
|
||||||
@posted="closeModal"
|
:disable-polls="true"
|
||||||
:disablePolls="true"
|
:disable-visibility-selector="true"
|
||||||
:disableVisibilitySelector="true"
|
|
||||||
:post-handler="doEditStatus"
|
:post-handler="doEditStatus"
|
||||||
|
@posted="closeModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</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 { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||||
faSmileBeam
|
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
library.add(faSmileBeam)
|
||||||
faSmileBeam
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||||
|
@ -105,7 +101,7 @@ const EmojiInput = {
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
input: undefined,
|
input: undefined,
|
||||||
highlighted: 0,
|
highlighted: 0,
|
||||||
|
@ -123,29 +119,34 @@ const EmojiInput = {
|
||||||
EmojiPicker
|
EmojiPicker
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
padEmoji () {
|
padEmoji() {
|
||||||
return this.$store.getters.mergedConfig.padEmoji
|
return this.$store.getters.mergedConfig.padEmoji
|
||||||
},
|
},
|
||||||
showSuggestions () {
|
showSuggestions() {
|
||||||
return this.focused &&
|
return (
|
||||||
|
this.focused &&
|
||||||
this.suggestions &&
|
this.suggestions &&
|
||||||
this.suggestions.length > 0 &&
|
this.suggestions.length > 0 &&
|
||||||
!this.showPicker &&
|
!this.showPicker &&
|
||||||
!this.temporarilyHideSuggestions
|
!this.temporarilyHideSuggestions
|
||||||
|
)
|
||||||
},
|
},
|
||||||
textAtCaret () {
|
textAtCaret() {
|
||||||
return (this.wordAtCaret || {}).word || ''
|
return (this.wordAtCaret || {}).word || ''
|
||||||
},
|
},
|
||||||
wordAtCaret () {
|
wordAtCaret() {
|
||||||
if (this.modelValue && this.caret) {
|
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
|
return word
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted() {
|
||||||
const { root } = this.$refs
|
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
|
if (!input) return
|
||||||
this.input = input
|
this.input = input
|
||||||
this.resize()
|
this.resize()
|
||||||
|
@ -158,7 +159,7 @@ const EmojiInput = {
|
||||||
input.addEventListener('transitionend', this.onTransition)
|
input.addEventListener('transitionend', this.onTransition)
|
||||||
input.addEventListener('input', this.onInput)
|
input.addEventListener('input', this.onInput)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted() {
|
||||||
const { input } = this
|
const { input } = this
|
||||||
if (input) {
|
if (input) {
|
||||||
input.removeEventListener('blur', this.onBlur)
|
input.removeEventListener('blur', this.onBlur)
|
||||||
|
@ -183,29 +184,29 @@ const EmojiInput = {
|
||||||
// Async: cancel if textAtCaret has changed during wait
|
// Async: cancel if textAtCaret has changed during wait
|
||||||
if (this.textAtCaret !== newWord) return
|
if (this.textAtCaret !== newWord) return
|
||||||
if (matchedSuggestions.length <= 0) return
|
if (matchedSuggestions.length <= 0) return
|
||||||
this.suggestions = take(matchedSuggestions, 5)
|
this.suggestions = take(matchedSuggestions, 5).map(
|
||||||
.map(({ imageUrl, ...rest }) => ({
|
({ imageUrl, ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
img: imageUrl || ''
|
img: imageUrl || ''
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
},
|
},
|
||||||
suggestions: {
|
suggestions: {
|
||||||
handler (newValue) {
|
handler(newValue) {
|
||||||
this.$nextTick(this.resize)
|
this.$nextTick(this.resize)
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focusPickerInput () {
|
focusPickerInput() {
|
||||||
const pickerEl = this.$refs.picker.$el
|
const pickerEl = this.$refs.picker.$el
|
||||||
if (!pickerEl) return
|
if (!pickerEl) return
|
||||||
const pickerInput = pickerEl.querySelector('input')
|
const pickerInput = pickerEl.querySelector('input')
|
||||||
if (pickerInput) pickerInput.focus()
|
if (pickerInput) pickerInput.focus()
|
||||||
},
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker() {
|
||||||
this.showPicker = true
|
this.showPicker = true
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.focusPickerInput()
|
this.focusPickerInput()
|
||||||
|
@ -218,21 +219,24 @@ const EmojiInput = {
|
||||||
this.disableClickOutside = false
|
this.disableClickOutside = false
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
togglePicker () {
|
togglePicker() {
|
||||||
this.input.focus()
|
this.input.focus()
|
||||||
this.showPicker = !this.showPicker
|
this.showPicker = !this.showPicker
|
||||||
if (this.showPicker) {
|
if (this.showPicker) {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(this.focusPickerInput)
|
this.$nextTick(this.focusPickerInput)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
replace (replacement) {
|
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.$emit('update:modelValue', newValue)
|
||||||
this.caret = 0
|
this.caret = 0
|
||||||
},
|
},
|
||||||
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
insert({ insertion, keepOpen, surroundingSpace = true }) {
|
||||||
const before = this.modelValue.substring(0, this.caret) || ''
|
const before = this.modelValue.substring(0, this.caret) || ''
|
||||||
const after = this.modelValue.substring(this.caret) || ''
|
const after = this.modelValue.substring(this.caret) || ''
|
||||||
|
|
||||||
|
@ -251,19 +255,25 @@ const EmojiInput = {
|
||||||
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
||||||
*/
|
*/
|
||||||
const isSpaceRegex = /\s/
|
const isSpaceRegex = /\s/
|
||||||
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
|
const spaceBefore =
|
||||||
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
|
surroundingSpace &&
|
||||||
|
!isSpaceRegex.exec(before.slice(-1)) &&
|
||||||
|
before.length &&
|
||||||
|
this.padEmoji > 0
|
||||||
|
? ' '
|
||||||
|
: ''
|
||||||
|
const spaceAfter =
|
||||||
|
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
|
||||||
|
? ' '
|
||||||
|
: ''
|
||||||
|
|
||||||
const newValue = [
|
const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
|
||||||
before,
|
''
|
||||||
spaceBefore,
|
)
|
||||||
insertion,
|
|
||||||
spaceAfter,
|
|
||||||
after
|
|
||||||
].join('')
|
|
||||||
this.keepOpen = keepOpen
|
this.keepOpen = keepOpen
|
||||||
this.$emit('update:modelValue', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
const position =
|
||||||
|
this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
this.input.focus()
|
this.input.focus()
|
||||||
}
|
}
|
||||||
|
@ -275,12 +285,17 @@ const EmojiInput = {
|
||||||
this.caret = position
|
this.caret = position
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
replaceText (e, suggestion) {
|
replaceText(e, suggestion) {
|
||||||
const len = this.suggestions.length || 0
|
const len = this.suggestions.length || 0
|
||||||
if (len > 0 || suggestion) {
|
if (len > 0 || suggestion) {
|
||||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
const chosenSuggestion =
|
||||||
|
suggestion || this.suggestions[this.highlighted]
|
||||||
const replacement = chosenSuggestion.replacement
|
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.$emit('update:modelValue', newValue)
|
||||||
this.highlighted = 0
|
this.highlighted = 0
|
||||||
const position = this.wordAtCaret.start + replacement.length
|
const position = this.wordAtCaret.start + replacement.length
|
||||||
|
@ -295,7 +310,7 @@ const EmojiInput = {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cycleBackward (e) {
|
cycleBackward(e) {
|
||||||
const len = this.suggestions.length || 0
|
const len = this.suggestions.length || 0
|
||||||
if (len > 1) {
|
if (len > 1) {
|
||||||
this.highlighted -= 1
|
this.highlighted -= 1
|
||||||
|
@ -307,7 +322,7 @@ const EmojiInput = {
|
||||||
this.highlighted = 0
|
this.highlighted = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cycleForward (e) {
|
cycleForward(e) {
|
||||||
const len = this.suggestions.length || 0
|
const len = this.suggestions.length || 0
|
||||||
if (len > 1) {
|
if (len > 1) {
|
||||||
this.highlighted += 1
|
this.highlighted += 1
|
||||||
|
@ -319,26 +334,28 @@ const EmojiInput = {
|
||||||
this.highlighted = 0
|
this.highlighted = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollIntoView () {
|
scrollIntoView() {
|
||||||
const rootRef = this.$refs['picker'].$el
|
const rootRef = this.$refs['picker'].$el
|
||||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||||
* replies in notifs) or mobile post form. Note that getting and setting
|
* replies in notifs) or mobile post form. Note that getting and setting
|
||||||
* scroll is different for `Window` and `Element`s
|
* scroll is different for `Window` and `Element`s
|
||||||
*/
|
*/
|
||||||
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
const scrollerRef =
|
||||||
this.$el.closest('.post-form-modal-view') ||
|
this.$el.closest('.sidebar-scroller') ||
|
||||||
window
|
this.$el.closest('.post-form-modal-view') ||
|
||||||
const currentScroll = scrollerRef === window
|
window
|
||||||
? scrollerRef.scrollY
|
const currentScroll =
|
||||||
: scrollerRef.scrollTop
|
scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
|
||||||
const scrollerHeight = scrollerRef === window
|
const scrollerHeight =
|
||||||
? scrollerRef.innerHeight
|
scrollerRef === window
|
||||||
: scrollerRef.offsetHeight
|
? scrollerRef.innerHeight
|
||||||
|
: scrollerRef.offsetHeight
|
||||||
|
|
||||||
const scrollerBottomBorder = currentScroll + scrollerHeight
|
const scrollerBottomBorder = currentScroll + scrollerHeight
|
||||||
// We check where the bottom border of root element is, this uses findOffset
|
// We check where the bottom border of root element is, this uses findOffset
|
||||||
// to find offset relative to scrollable container (scroller)
|
// 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)
|
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
|
||||||
// could also check top delta but there's no case for it
|
// could also check top delta but there's no case for it
|
||||||
|
@ -360,10 +377,10 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onTransition (e) {
|
onTransition(e) {
|
||||||
this.resize()
|
this.resize()
|
||||||
},
|
},
|
||||||
onBlur (e) {
|
onBlur(e) {
|
||||||
// Clicking on any suggestion removes focus from autocomplete,
|
// Clicking on any suggestion removes focus from autocomplete,
|
||||||
// preventing click handler ever executing.
|
// preventing click handler ever executing.
|
||||||
this.blurTimeout = setTimeout(() => {
|
this.blurTimeout = setTimeout(() => {
|
||||||
|
@ -372,10 +389,10 @@ const EmojiInput = {
|
||||||
this.resize()
|
this.resize()
|
||||||
}, 200)
|
}, 200)
|
||||||
},
|
},
|
||||||
onClick (e, suggestion) {
|
onClick(e, suggestion) {
|
||||||
this.replaceText(e, suggestion)
|
this.replaceText(e, suggestion)
|
||||||
},
|
},
|
||||||
onFocus (e) {
|
onFocus(e) {
|
||||||
if (this.blurTimeout) {
|
if (this.blurTimeout) {
|
||||||
clearTimeout(this.blurTimeout)
|
clearTimeout(this.blurTimeout)
|
||||||
this.blurTimeout = null
|
this.blurTimeout = null
|
||||||
|
@ -389,7 +406,7 @@ const EmojiInput = {
|
||||||
this.resize()
|
this.resize()
|
||||||
this.temporarilyHideSuggestions = false
|
this.temporarilyHideSuggestions = false
|
||||||
},
|
},
|
||||||
onKeyUp (e) {
|
onKeyUp(e) {
|
||||||
const { key } = e
|
const { key } = e
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
this.resize()
|
||||||
|
@ -402,11 +419,11 @@ const EmojiInput = {
|
||||||
this.temporarilyHideSuggestions = false
|
this.temporarilyHideSuggestions = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPaste (e) {
|
onPaste(e) {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
this.resize()
|
||||||
},
|
},
|
||||||
onKeyDown (e) {
|
onKeyDown(e) {
|
||||||
const { ctrlKey, shiftKey, key } = e
|
const { ctrlKey, shiftKey, key } = e
|
||||||
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
||||||
this.insert({ insertion: '\n', surroundingSpace: false })
|
this.insert({ insertion: '\n', surroundingSpace: false })
|
||||||
|
@ -453,31 +470,31 @@ const EmojiInput = {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
this.resize()
|
this.resize()
|
||||||
},
|
},
|
||||||
onInput (e) {
|
onInput(e) {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
this.resize()
|
||||||
this.$emit('update:modelValue', e.target.value)
|
this.$emit('update:modelValue', e.target.value)
|
||||||
},
|
},
|
||||||
onClickInput (e) {
|
onClickInput(e) {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
},
|
},
|
||||||
onClickOutside (e) {
|
onClickOutside(e) {
|
||||||
if (this.disableClickOutside) return
|
if (this.disableClickOutside) return
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
},
|
},
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded(e) {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
onStickerUploadFailed (e) {
|
onStickerUploadFailed(e) {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
this.$emit('sticker-upload-Failed', e)
|
this.$emit('sticker-upload-Failed', e)
|
||||||
},
|
},
|
||||||
setCaret ({ target: { selectionStart } }) {
|
setCaret({ target: { selectionStart } }) {
|
||||||
this.caret = selectionStart
|
this.caret = selectionStart
|
||||||
},
|
},
|
||||||
resize () {
|
resize() {
|
||||||
const panel = this.$refs.panel
|
const panel = this.$refs.panel
|
||||||
if (!panel) return
|
if (!panel) return
|
||||||
const picker = this.$refs.picker.$el
|
const picker = this.$refs.picker.$el
|
||||||
|
@ -488,9 +505,12 @@ const EmojiInput = {
|
||||||
this.setPlacement(panelBody, panel, offsetBottom)
|
this.setPlacement(panelBody, panel, offsetBottom)
|
||||||
this.setPlacement(picker, picker, offsetBottom)
|
this.setPlacement(picker, picker, offsetBottom)
|
||||||
},
|
},
|
||||||
setPlacement (container, target, offsetBottom) {
|
setPlacement(container, target, offsetBottom) {
|
||||||
if (!container || !target) return
|
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.top = offsetBottom + 'px'
|
||||||
target.style.bottom = 'auto'
|
target.style.bottom = 'auto'
|
||||||
} else {
|
} else {
|
||||||
|
@ -498,7 +518,7 @@ const EmojiInput = {
|
||||||
target.style.bottom = this.input.offsetHeight + 'px'
|
target.style.bottom = this.input.offsetHeight + 'px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
overflowsBottom (el) {
|
overflowsBottom(el) {
|
||||||
return el.getBoundingClientRect().bottom > window.innerHeight
|
return el.getBoundingClientRect().bottom > window.innerHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
v-if="enableEmojiPicker"
|
v-if="enableEmojiPicker"
|
||||||
ref="picker"
|
ref="picker"
|
||||||
|
show-keep-open
|
||||||
:class="{ hide: !showPicker }"
|
:class="{ hide: !showPicker }"
|
||||||
:enable-sticker-picker="enableStickerPicker"
|
:enable-sticker-picker="enableStickerPicker"
|
||||||
class="emoji-picker-panel"
|
class="emoji-picker-panel"
|
||||||
|
@ -42,11 +43,14 @@
|
||||||
:class="{ highlighted: index === highlighted }"
|
:class="{ highlighted: index === highlighted }"
|
||||||
@click.stop.prevent="onClick($event, suggestion)"
|
@click.stop.prevent="onClick($event, suggestion)"
|
||||||
>
|
>
|
||||||
<span v-if="!suggestion.mfm" class="image">
|
<span
|
||||||
|
v-if="!suggestion.mfm"
|
||||||
|
class="image"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="suggestion.img"
|
v-if="suggestion.img"
|
||||||
:src="suggestion.img"
|
:src="suggestion.img"
|
||||||
>
|
/>
|
||||||
<span v-else>{{ suggestion.replacement }}</span>
|
<span v-else>{{ suggestion.replacement }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
|
@ -77,7 +81,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: .2em .25em;
|
margin: 0.2em 0.25em;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
@ -93,7 +97,7 @@
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|
||||||
&.hide {
|
&.hide {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +108,7 @@
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|
||||||
&.hide {
|
&.hide {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-body {
|
&-body {
|
||||||
|
@ -178,7 +182,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea {
|
input,
|
||||||
|
textarea {
|
||||||
flex: 1 0 auto;
|
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']
|
const MFM_TAGS = [
|
||||||
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
|
'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
|
* 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.
|
* doesn't support user linking you can just provide only emoji.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default data => {
|
export default (data) => {
|
||||||
const emojiCurry = suggestEmoji(data.emoji)
|
const emojiCurry = suggestEmoji(data.emoji)
|
||||||
const usersCurry = data.store && suggestUsers(data.store)
|
const usersCurry = data.store && suggestUsers(data.store)
|
||||||
return input => {
|
return (input) => {
|
||||||
const firstChar = input[0]
|
const firstChar = input[0]
|
||||||
if (firstChar === ':' && data.emoji) {
|
if (firstChar === ':' && data.emoji) {
|
||||||
return emojiCurry(input)
|
return emojiCurry(input)
|
||||||
|
@ -25,14 +46,15 @@ export default data => {
|
||||||
return usersCurry(input)
|
return usersCurry(input)
|
||||||
}
|
}
|
||||||
if (firstChar === '$') {
|
if (firstChar === '$') {
|
||||||
return MFM_TAGS
|
return MFM_TAGS.filter(
|
||||||
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
|
({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const suggestEmoji = emojis => input => {
|
export const suggestEmoji = (emojis) => (input) => {
|
||||||
const noPrefix = input.toLowerCase().substr(1)
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
return emojis
|
return emojis
|
||||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
.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)
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
if (previousQuery === noPrefix) return suggestions
|
if (previousQuery === noPrefix) return suggestions
|
||||||
|
|
||||||
|
@ -99,36 +121,47 @@ export const suggestUsers = ({ dispatch, state }) => {
|
||||||
await debounceUserSearch(noPrefix)
|
await debounceUserSearch(noPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSuggestions = state.users.users.filter(
|
const newSuggestions = state.users.users
|
||||||
user =>
|
.filter(
|
||||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
(user) =>
|
||||||
user.name.toLowerCase().startsWith(noPrefix)
|
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||||
).slice(0, 20).sort((a, b) => {
|
user.name.toLowerCase().startsWith(noPrefix)
|
||||||
let aScore = 0
|
)
|
||||||
let bScore = 0
|
.slice(0, 20)
|
||||||
|
.sort((a, b) => {
|
||||||
|
let aScore = 0
|
||||||
|
let bScore = 0
|
||||||
|
|
||||||
// Matches on screen name (i.e. user@instance) makes a priority
|
// Matches on screen name (i.e. user@instance) makes a priority
|
||||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
|
|
||||||
// Matches on name takes second priority
|
// Matches on name takes second priority
|
||||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
|
||||||
const diff = (bScore - aScore) * 10
|
const diff = (bScore - aScore) * 10
|
||||||
|
|
||||||
// Then sort alphabetically
|
// Then sort alphabetically
|
||||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||||
|
|
||||||
return diff + nameAlphabetically + screenNameAlphabetically
|
return diff + nameAlphabetically + screenNameAlphabetically
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
})
|
||||||
displayText: screen_name_ui,
|
.map(
|
||||||
detailText: name,
|
({
|
||||||
imageUrl: profile_image_url_original,
|
screen_name,
|
||||||
replacement: '@' + 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 */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
suggestions = newSuggestions || []
|
suggestions = newSuggestions || []
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
|
@ -8,18 +9,7 @@ import {
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { trim, escapeRegExp, startCase } from 'lodash'
|
import { trim, escapeRegExp, startCase } from 'lodash'
|
||||||
|
|
||||||
library.add(
|
library.add(faBoxOpen, faStickyNote, faSmileBeam)
|
||||||
faBoxOpen,
|
|
||||||
faStickyNote,
|
|
||||||
faSmileBeam
|
|
||||||
)
|
|
||||||
|
|
||||||
// At widest, approximately 20 emoji are visible in a row,
|
|
||||||
// loading 3 rows, could be overkill for narrow picker
|
|
||||||
const LOAD_EMOJI_BY = 60
|
|
||||||
|
|
||||||
// When to start loading new batch emoji, in pixels
|
|
||||||
const LOAD_EMOJI_MARGIN = 64
|
|
||||||
|
|
||||||
const EmojiPicker = {
|
const EmojiPicker = {
|
||||||
props: {
|
props: {
|
||||||
|
@ -27,151 +17,88 @@ const EmojiPicker = {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
showKeepOpen: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
keyword: '',
|
keyword: '',
|
||||||
activeGroup: 'standard',
|
activeGroup: 'standard',
|
||||||
showingStickers: false,
|
showingStickers: false,
|
||||||
groupsScrolledClass: 'scrolled-top',
|
keepOpen: false
|
||||||
keepOpen: false,
|
|
||||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
|
||||||
customEmojiTimeout: null,
|
|
||||||
customEmojiLoadAllConfirmed: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
StickerPicker: defineAsyncComponent(() =>
|
||||||
Checkbox
|
import('../sticker_picker/sticker_picker.vue')
|
||||||
|
),
|
||||||
|
Checkbox,
|
||||||
|
EmojiGrid
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded(e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
onStickerUploadFailed (e) {
|
onStickerUploadFailed(e) {
|
||||||
this.$emit('sticker-upload-failed', e)
|
this.$emit('sticker-upload-failed', e)
|
||||||
},
|
},
|
||||||
onEmoji (emoji) {
|
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.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||||
|
this.$store.commit('emojiUsed', emoji)
|
||||||
},
|
},
|
||||||
onScroll (e) {
|
onWheel(e) {
|
||||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
|
||||||
this.updateScrolledClass(target)
|
|
||||||
this.scrolledGroup(target)
|
|
||||||
this.triggerLoadMore(target)
|
|
||||||
},
|
|
||||||
onWheel (e) {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||||
},
|
},
|
||||||
highlight (key) {
|
highlight(key) {
|
||||||
this.setShowStickers(false)
|
this.setShowStickers(false)
|
||||||
this.activeGroup = key
|
this.activeGroup = key
|
||||||
},
|
if (this.keyword.length) {
|
||||||
updateScrolledClass (target) {
|
this.$refs.emojiGrid.scrollToItem(key)
|
||||||
if (target.scrollTop <= 5) {
|
|
||||||
this.groupsScrolledClass = 'scrolled-top'
|
|
||||||
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
|
||||||
this.groupsScrolledClass = 'scrolled-bottom'
|
|
||||||
} else {
|
|
||||||
this.groupsScrolledClass = 'scrolled-middle'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
triggerLoadMore (target) {
|
onActiveGroup(group) {
|
||||||
const ref = this.$refs['group-end-custom']
|
this.activeGroup = group
|
||||||
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) {
|
toggleStickers() {
|
||||||
const top = target.scrollTop + 5
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.emojisView.forEach(group => {
|
|
||||||
const ref = this.$refs['group-' + group.id]
|
|
||||||
if (ref.offsetTop <= top) {
|
|
||||||
this.activeGroup = group.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadEmoji () {
|
|
||||||
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
|
|
||||||
|
|
||||||
if (allLoaded) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.customEmojiBufferSlice += LOAD_EMOJI_BY
|
|
||||||
},
|
|
||||||
startEmojiLoad (forceUpdate = false) {
|
|
||||||
if (!forceUpdate) {
|
|
||||||
this.keyword = ''
|
|
||||||
}
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs['emoji-groups'].scrollTop = 0
|
|
||||||
})
|
|
||||||
const bufferSize = this.customEmojiBuffer.length
|
|
||||||
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
|
|
||||||
if (bufferPrefilledAll && !forceUpdate) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.customEmojiBufferSlice = LOAD_EMOJI_BY
|
|
||||||
},
|
|
||||||
toggleStickers () {
|
|
||||||
this.showingStickers = !this.showingStickers
|
this.showingStickers = !this.showingStickers
|
||||||
},
|
},
|
||||||
setShowStickers (value) {
|
setShowStickers(value) {
|
||||||
this.showingStickers = value
|
this.showingStickers = value
|
||||||
},
|
},
|
||||||
filterByKeyword (list) {
|
filterByKeyword(list) {
|
||||||
if (this.keyword === '') return list
|
if (this.keyword === '') return list
|
||||||
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
|
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
|
||||||
return list.filter(emoji => {
|
return list.filter((emoji) => {
|
||||||
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
|
return (
|
||||||
|
regex.test(emoji.displayText) ||
|
||||||
|
(!emoji.imageUrl && emoji.replacement === this.keyword)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
keyword () {
|
|
||||||
this.customEmojiLoadAllConfirmed = false
|
|
||||||
this.onScroll()
|
|
||||||
this.startEmojiLoad(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
activeGroupView () {
|
activeGroupView() {
|
||||||
return this.showingStickers ? '' : this.activeGroup
|
return this.showingStickers ? '' : this.activeGroup
|
||||||
},
|
},
|
||||||
stickersAvailable () {
|
stickersAvailable() {
|
||||||
if (this.$store.state.instance.stickers) {
|
if (this.$store.state.instance.stickers) {
|
||||||
return this.$store.state.instance.stickers.length > 0
|
return this.$store.state.instance.stickers.length > 0
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
filteredEmoji () {
|
filteredEmoji() {
|
||||||
return this.filterByKeyword(
|
return this.filterByKeyword(this.$store.state.instance.customEmoji || [])
|
||||||
this.$store.state.instance.customEmoji || []
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
customEmojiBuffer () {
|
emojis() {
|
||||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
const recentEmojis = this.$store.getters.recentEmojis
|
||||||
},
|
|
||||||
emojis () {
|
|
||||||
const standardEmojis = this.$store.state.instance.emoji || []
|
const standardEmojis = this.$store.state.instance.emoji || []
|
||||||
const customEmojis = this.sortedEmoji
|
const customEmojis = this.sortedEmoji
|
||||||
const emojiPacks = []
|
const emojiPacks = []
|
||||||
|
@ -184,6 +111,15 @@ const EmojiPicker = {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
id: 'recent',
|
||||||
|
text: this.$t('emoji.recent'),
|
||||||
|
first: {
|
||||||
|
imageUrl: '',
|
||||||
|
replacement: '🕒'
|
||||||
|
},
|
||||||
|
emojis: this.filterByKeyword(recentEmojis)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'standard',
|
id: 'standard',
|
||||||
text: this.$t('emoji.unicode'),
|
text: this.$t('emoji.unicode'),
|
||||||
|
@ -195,7 +131,7 @@ const EmojiPicker = {
|
||||||
}
|
}
|
||||||
].concat(emojiPacks)
|
].concat(emojiPacks)
|
||||||
},
|
},
|
||||||
sortedEmoji () {
|
sortedEmoji() {
|
||||||
const customEmojis = this.$store.state.instance.customEmoji || []
|
const customEmojis = this.$store.state.instance.customEmoji || []
|
||||||
const sortedEmojiGroups = new Map()
|
const sortedEmojiGroups = new Map()
|
||||||
customEmojis.forEach((emoji) => {
|
customEmojis.forEach((emoji) => {
|
||||||
|
@ -207,19 +143,22 @@ const EmojiPicker = {
|
||||||
})
|
})
|
||||||
return new Map([...sortedEmojiGroups.entries()].sort())
|
return new Map([...sortedEmojiGroups.entries()].sort())
|
||||||
},
|
},
|
||||||
emojisView () {
|
emojisView() {
|
||||||
if (this.keyword === '') {
|
if (this.keyword === '') {
|
||||||
return this.emojis.filter(pack => {
|
return this.emojis.filter((pack) => {
|
||||||
return pack.id === this.activeGroup
|
return pack.id === this.activeGroup
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return this.emojis.filter(pack => {
|
return this.emojis.filter((pack) => {
|
||||||
return pack.emojis.length > 0
|
return pack.emojis.length > 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stickerPickerEnabled () {
|
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';
|
@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 {
|
.Notification {
|
||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
min-width: 160%;
|
min-width: 160%;
|
||||||
|
@ -7,7 +25,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
left: -70%;
|
left: -70%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@media (min-width: 800px) and (max-width: 1300px) {
|
@media (min-width: 800px) and (max-width: 1280px) {
|
||||||
left: -50%;
|
left: -50%;
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
max-width: 130%;
|
max-width: 130%;
|
||||||
|
@ -18,6 +36,10 @@
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
max-width: 130%;
|
max-width: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Status > .emoji-picker {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
|
@ -70,10 +92,6 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-groups {
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.additional-tabs {
|
.additional-tabs {
|
||||||
border-left: 1px solid;
|
border-left: 1px solid;
|
||||||
border-left-color: $fallback--icon;
|
border-left-color: $fallback--icon;
|
||||||
|
@ -100,7 +118,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: .4em;
|
padding: 0.4em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -133,7 +151,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticker-picker {
|
.sticker-picker {
|
||||||
flex: 1 1 auto
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stickers,
|
.stickers,
|
||||||
|
@ -152,14 +170,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji-search {
|
||||||
&-search {
|
padding: 5px;
|
||||||
padding: 5px;
|
flex: 0 0 auto;
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-groups {
|
&-groups {
|
||||||
|
@ -168,8 +184,8 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
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 bottom, white 0, transparent 100%) top no-repeat,
|
||||||
linear-gradient(to top, white, white);
|
linear-gradient(to top, white, white);
|
||||||
transition: mask-size 150ms;
|
transition: mask-size 150ms;
|
||||||
mask-size: 100% 20px, 100% 20px, auto;
|
mask-size: 100% 20px, 100% 20px, auto;
|
||||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||||
|
@ -221,7 +237,5 @@
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="emoji-picker panel panel-default panel-body">
|
<div class="emoji-picker panel panel-default panel-body">
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<span
|
<span
|
||||||
|
ref="emoji-tabs"
|
||||||
class="emoji-tabs"
|
class="emoji-tabs"
|
||||||
@wheel="onWheel"
|
@wheel="onWheel"
|
||||||
ref="emoji-tabs"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="group in emojis"
|
v-for="group in emojis"
|
||||||
|
@ -17,16 +17,18 @@
|
||||||
:title="group.text"
|
:title="group.text"
|
||||||
@click.prevent="highlight(group.id)"
|
@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
|
<img
|
||||||
v-else
|
v-else
|
||||||
:src="group.first.imageUrl"
|
:src="group.first.imageUrl"
|
||||||
>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="stickerPickerEnabled"
|
v-if="stickerPickerEnabled"
|
||||||
class="stickers-tab-icon emoji-tabs-item"
|
class="stickers-tab-icon emoji-tabs-item"
|
||||||
:class="{active: showingStickers}"
|
:class="{ active: showingStickers }"
|
||||||
:title="$t('emoji.stickers')"
|
:title="$t('emoji.stickers')"
|
||||||
@click.prevent="toggleStickers"
|
@click.prevent="toggleStickers"
|
||||||
>
|
>
|
||||||
|
@ -40,7 +42,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div
|
<div
|
||||||
class="emoji-content"
|
class="emoji-content"
|
||||||
:class="{hidden: showingStickers}"
|
:class="{ hidden: showingStickers }"
|
||||||
>
|
>
|
||||||
<div class="emoji-search">
|
<div class="emoji-search">
|
||||||
<input
|
<input
|
||||||
|
@ -49,13 +51,17 @@
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:placeholder="$t('emoji.search_emoji')"
|
:placeholder="$t('emoji.search_emoji')"
|
||||||
@input="$event.target.composing = false"
|
@input="$event.target.composing = false"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<EmojiGrid
|
||||||
|
ref="emojiGrid"
|
||||||
|
:groups="emojisView"
|
||||||
|
@emoji="onEmoji"
|
||||||
|
@active-group="onActiveGroup"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
ref="emoji-groups"
|
v-if="showKeepOpen"
|
||||||
class="emoji-groups"
|
class="keep-open"
|
||||||
:class="groupsScrolledClass"
|
|
||||||
@scroll="onScroll"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="group in emojisView"
|
v-for="group in emojisView"
|
||||||
|
@ -79,7 +85,7 @@
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
:src="emoji.imageUrl"
|
:src="emoji.imageUrl"
|
||||||
>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span :ref="'group-end-' + group.id" />
|
<span :ref="'group-end-' + group.id" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,11 @@ import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||||
|
|
||||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
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 = {
|
const EmojiReactions = {
|
||||||
name: 'EmojiReactions',
|
name: 'EmojiReactions',
|
||||||
components: {
|
components: {
|
||||||
|
@ -14,18 +19,20 @@ const EmojiReactions = {
|
||||||
showAll: false
|
showAll: false
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
tooManyReactions () {
|
tooManyReactions() {
|
||||||
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
||||||
},
|
},
|
||||||
emojiReactions () {
|
emojiReactions() {
|
||||||
return this.showAll
|
return this.showAll
|
||||||
? this.status.emoji_reactions
|
? this.status.emoji_reactions
|
||||||
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
||||||
},
|
},
|
||||||
showMoreString () {
|
showMoreString() {
|
||||||
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
return `+${
|
||||||
|
this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF
|
||||||
|
}`
|
||||||
},
|
},
|
||||||
accountsForEmoji () {
|
accountsForEmoji() {
|
||||||
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||||
if (reaction.url) {
|
if (reaction.url) {
|
||||||
acc[reaction.url] = reaction.accounts || []
|
acc[reaction.url] = reaction.accounts || []
|
||||||
|
@ -35,30 +42,32 @@ const EmojiReactions = {
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
loggedIn () {
|
loggedIn() {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleShowAll () {
|
toggleShowAll() {
|
||||||
this.showAll = !this.showAll
|
this.showAll = !this.showAll
|
||||||
},
|
},
|
||||||
reactedWith (emoji) {
|
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 () {
|
fetchEmojiReactionsByIfMissing() {
|
||||||
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
|
||||||
if (hasNoAccounts) {
|
if (hasNoAccounts) {
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reactWith (emoji) {
|
reactWith(emoji) {
|
||||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||||
|
const emojiObject = findEmojiByReplacement(this.$store.state, emoji)
|
||||||
|
this.$store.commit('emojiUsed', emojiObject)
|
||||||
},
|
},
|
||||||
unreact (emoji) {
|
unreact(emoji) {
|
||||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||||
},
|
},
|
||||||
emojiOnClick (emoji, event) {
|
emojiOnClick(emoji, event) {
|
||||||
if (!this.loggedIn) return
|
if (!this.loggedIn) return
|
||||||
|
|
||||||
if (this.reactedWith(emoji)) {
|
if (this.reactedWith(emoji)) {
|
||||||
|
|
|
@ -1,28 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="emoji-reactions">
|
<div class="emoji-reactions">
|
||||||
<UserListPopover
|
<UserListPopover
|
||||||
v-for="(reaction) in emojiReactions"
|
v-for="reaction in emojiReactions"
|
||||||
:key="reaction.url || reaction.name"
|
:key="reaction.url || reaction.name"
|
||||||
:users="accountsForEmoji[reaction.url || reaction.name]"
|
:users="accountsForEmoji[reaction.url || reaction.name]"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="emoji-reaction btn button-default"
|
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)"
|
@click="emojiOnClick(reaction.name, $event)"
|
||||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="reaction.url !== null"
|
v-if="reaction.url !== null"
|
||||||
|
class="emoji-button-inner"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="reaction.url"
|
:src="reaction.url"
|
||||||
:title="reaction.name"
|
:title="reaction.name"
|
||||||
class="reaction-emoji"
|
class="reaction-emoji"
|
||||||
width="2.55em"
|
width="2.55em"
|
||||||
>
|
/>
|
||||||
{{ reaction.count }}
|
{{ reaction.count }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span
|
||||||
|
v-else
|
||||||
|
class="emoji-button-inner"
|
||||||
|
>
|
||||||
<span class="reaction-emoji unicode-emoji">
|
<span class="reaction-emoji unicode-emoji">
|
||||||
{{ reaction.name }}
|
{{ reaction.name }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -41,7 +48,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./emoji_reactions.js" ></script>
|
<script src="./emoji_reactions.js"></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
@ -49,10 +56,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unicode-emoji {
|
.unicode-emoji {
|
||||||
font-size: 210%;
|
font-size: 128%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-reaction {
|
.emoji-reaction {
|
||||||
|
@ -60,13 +68,20 @@
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 28px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
.reaction-emoji {
|
.reaction-emoji {
|
||||||
width: 2.55em !important;
|
width: auto;
|
||||||
|
max-width: 96cqw;
|
||||||
|
height: 2.55em !important;
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
img.reaction-emoji {
|
||||||
|
width: 1.55em !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
@ -93,9 +108,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-default.picked-reaction {
|
.button-default.picked-reaction {
|
||||||
border: 1px solid var(--accent, $fallback--link);
|
background: none;
|
||||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
padding: 1px 0.5em;
|
||||||
margin-right: calc(0.5em - 1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
.emoji-button-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
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(
|
library.add(faCircleNotch)
|
||||||
faCircleNotch
|
|
||||||
)
|
|
||||||
|
|
||||||
const Exporter = {
|
const Exporter = {
|
||||||
props: {
|
props: {
|
||||||
|
@ -18,26 +16,30 @@ const Exporter = {
|
||||||
exportButtonLabel: { type: String },
|
exportButtonLabel: { type: String },
|
||||||
processingMessage: { type: String }
|
processingMessage: { type: String }
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false
|
processing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
process () {
|
process() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.getContent()
|
this.getContent().then((content) => {
|
||||||
.then((content) => {
|
const fileToDownload = document.createElement('a')
|
||||||
const fileToDownload = document.createElement('a')
|
fileToDownload.setAttribute(
|
||||||
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
|
'href',
|
||||||
fileToDownload.setAttribute('download', this.filename)
|
'data:text/plain;charset=utf-8,' + encodeURIComponent(content)
|
||||||
fileToDownload.style.display = 'none'
|
)
|
||||||
document.body.appendChild(fileToDownload)
|
fileToDownload.setAttribute('download', this.filename)
|
||||||
fileToDownload.click()
|
fileToDownload.style.display = 'none'
|
||||||
document.body.removeChild(fileToDownload)
|
document.body.appendChild(fileToDownload)
|
||||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
fileToDownload.click()
|
||||||
setTimeout(() => { this.processing = false }, 2000)
|
document.body.removeChild(fileToDownload)
|
||||||
})
|
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||||
|
setTimeout(() => {
|
||||||
|
this.processing = false
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
faBookmark as faBookmarkReg,
|
faBookmark as faBookmarkReg,
|
||||||
faFlag
|
faFlag
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
|
@ -35,7 +36,7 @@ const ExtraButtons = {
|
||||||
Popover,
|
Popover,
|
||||||
ConfirmModal
|
ConfirmModal
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
showingDeleteDialog: false,
|
showingDeleteDialog: false,
|
||||||
|
@ -43,154 +44,206 @@ const ExtraButtons = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
deleteStatus () {
|
deleteStatus() {
|
||||||
if (this.shouldConfirmDelete) {
|
if (this.shouldConfirmDelete) {
|
||||||
this.showDeleteStatusConfirmDialog()
|
this.showDeleteStatusConfirmDialog()
|
||||||
} else {
|
} else {
|
||||||
this.doDeleteStatus()
|
this.doDeleteStatus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doDeleteStatus () {
|
doDeleteStatus() {
|
||||||
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
||||||
this.hideDeleteStatusConfirmDialog()
|
this.hideDeleteStatusConfirmDialog()
|
||||||
},
|
},
|
||||||
showDeleteStatusConfirmDialog () {
|
showDeleteStatusConfirmDialog() {
|
||||||
this.showingDeleteDialog = true
|
this.showingDeleteDialog = true
|
||||||
},
|
},
|
||||||
hideDeleteStatusConfirmDialog () {
|
hideDeleteStatusConfirmDialog() {
|
||||||
this.showingDeleteDialog = false
|
this.showingDeleteDialog = false
|
||||||
},
|
},
|
||||||
translateStatus () {
|
translateStatus() {
|
||||||
if (this.noTranslationTargetSet) {
|
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
|
const translateTo =
|
||||||
this.$store.dispatch('translateStatus', { id: this.status.id, language: 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'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
pinStatus () {
|
pinStatus() {
|
||||||
this.$store.dispatch('pinStatus', this.status.id)
|
this.$store
|
||||||
|
.dispatch('pinStatus', this.status.id)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
unpinStatus () {
|
unpinStatus() {
|
||||||
this.$store.dispatch('unpinStatus', this.status.id)
|
this.$store
|
||||||
|
.dispatch('unpinStatus', this.status.id)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
muteConversation () {
|
muteConversation() {
|
||||||
this.$store.dispatch('muteConversation', this.status.id)
|
this.$store
|
||||||
|
.dispatch('muteConversation', this.status.id)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
unmuteConversation () {
|
unmuteConversation() {
|
||||||
this.$store.dispatch('unmuteConversation', this.status.id)
|
this.$store
|
||||||
|
.dispatch('unmuteConversation', this.status.id)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
copyLink () {
|
copyLink() {
|
||||||
navigator.clipboard.writeText(this.statusLink)
|
navigator.clipboard
|
||||||
|
.writeText(this.statusLink)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
bookmarkStatus () {
|
bookmarkStatus() {
|
||||||
this.$store.dispatch('bookmark', { id: this.status.id })
|
this.$store
|
||||||
|
.dispatch('bookmark', { id: this.status.id })
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
unbookmarkStatus () {
|
unbookmarkStatus() {
|
||||||
this.$store.dispatch('unbookmark', { id: this.status.id })
|
this.$store
|
||||||
|
.dispatch('unbookmark', { id: this.status.id })
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch((err) => this.$emit('onError', err.error.error))
|
||||||
},
|
},
|
||||||
reportStatus () {
|
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 () {
|
editStatus() {
|
||||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
this.$store
|
||||||
.then(data => this.$store.dispatch('openEditStatusModal', {
|
.dispatch('fetchStatusSource', { id: this.status.id })
|
||||||
statusId: this.status.id,
|
.then((data) =>
|
||||||
subject: data.spoiler_text,
|
this.$store.dispatch('openEditStatusModal', {
|
||||||
statusText: data.text,
|
statusId: this.status.id,
|
||||||
statusIsSensitive: this.status.nsfw,
|
subject: data.spoiler_text,
|
||||||
statusPoll: this.status.poll,
|
statusText: data.text,
|
||||||
statusFiles: [...this.status.attachments],
|
statusIsSensitive: this.status.nsfw,
|
||||||
visibility: this.status.visibility,
|
statusPoll: this.status.poll,
|
||||||
statusContentType: data.content_type
|
statusFiles: [...this.status.attachments],
|
||||||
}))
|
visibility: this.status.visibility,
|
||||||
|
statusContentType: data.content_type
|
||||||
|
})
|
||||||
|
)
|
||||||
},
|
},
|
||||||
showStatusHistory () {
|
showStatusHistory() {
|
||||||
const originalStatus = { ...this.status }
|
const originalStatus = { ...this.status }
|
||||||
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
|
const stripFieldsList = [
|
||||||
stripFieldsList.forEach(p => delete originalStatus[p])
|
'attachments',
|
||||||
|
'created_at',
|
||||||
|
'emojis',
|
||||||
|
'text',
|
||||||
|
'raw_html',
|
||||||
|
'nsfw',
|
||||||
|
'poll',
|
||||||
|
'summary',
|
||||||
|
'summary_raw_html'
|
||||||
|
]
|
||||||
|
stripFieldsList.forEach((p) => delete originalStatus[p])
|
||||||
this.$store.dispatch('openStatusHistoryModal', originalStatus)
|
this.$store.dispatch('openStatusHistoryModal', originalStatus)
|
||||||
},
|
},
|
||||||
redraftStatus () {
|
redraftStatus() {
|
||||||
if (this.shouldConfirmDelete) {
|
if (this.shouldConfirmDelete) {
|
||||||
this.showRedraftStatusConfirmDialog()
|
this.showRedraftStatusConfirmDialog()
|
||||||
} else {
|
} else {
|
||||||
this.doRedraftStatus()
|
this.doRedraftStatus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doRedraftStatus () {
|
doRedraftStatus() {
|
||||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
this.$store
|
||||||
.then(data => this.$store.dispatch('openPostStatusModal', {
|
.dispatch('fetchStatusSource', { id: this.status.id })
|
||||||
isRedraft: true,
|
.then((data) =>
|
||||||
statusId: this.status.id,
|
this.$store.dispatch('openPostStatusModal', {
|
||||||
subject: data.spoiler_text,
|
isRedraft: true,
|
||||||
statusText: data.text,
|
statusId: this.status.id,
|
||||||
statusIsSensitive: this.status.nsfw,
|
subject: data.spoiler_text,
|
||||||
statusPoll: this.status.poll,
|
statusText: data.text,
|
||||||
statusFiles: [...this.status.attachments],
|
statusIsSensitive: this.status.nsfw,
|
||||||
statusScope: this.status.visibility,
|
statusPoll: this.status.poll,
|
||||||
statusContentType: data.content_type
|
statusFiles: [...this.status.attachments],
|
||||||
}))
|
statusScope: this.status.visibility,
|
||||||
|
statusLanguage: this.status.language,
|
||||||
|
statusContentType: data.content_type
|
||||||
|
})
|
||||||
|
)
|
||||||
this.doDeleteStatus()
|
this.doDeleteStatus()
|
||||||
},
|
},
|
||||||
showRedraftStatusConfirmDialog () {
|
showRedraftStatusConfirmDialog() {
|
||||||
this.showingRedraftDialog = true
|
this.showingRedraftDialog = true
|
||||||
},
|
},
|
||||||
hideRedraftStatusConfirmDialog () {
|
hideRedraftStatusConfirmDialog() {
|
||||||
this.showingRedraftDialog = false
|
this.showingRedraftDialog = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser() {
|
||||||
canDelete () {
|
return this.$store.state.users.currentUser
|
||||||
if (!this.currentUser) { return }
|
},
|
||||||
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
canDelete() {
|
||||||
|
if (!this.currentUser) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const superuser =
|
||||||
|
this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||||
return superuser || this.status.user.id === this.currentUser.id
|
return superuser || this.status.user.id === this.currentUser.id
|
||||||
},
|
},
|
||||||
ownStatus () {
|
ownStatus() {
|
||||||
return this.status.user.id === this.currentUser.id
|
return this.status.user.id === this.currentUser.id
|
||||||
},
|
},
|
||||||
canPin () {
|
canPin() {
|
||||||
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
|
return (
|
||||||
|
this.ownStatus &&
|
||||||
|
(this.status.visibility === 'public' ||
|
||||||
|
this.status.visibility === 'unlisted')
|
||||||
|
)
|
||||||
},
|
},
|
||||||
canMute () {
|
canMute() {
|
||||||
return !!this.currentUser
|
return !!this.currentUser
|
||||||
},
|
},
|
||||||
canTranslate () {
|
canTranslate() {
|
||||||
return this.$store.state.instance.translationEnabled === true
|
return this.$store.state.instance.translationEnabled === true
|
||||||
},
|
},
|
||||||
noTranslationTargetSet () {
|
noTranslationTargetSet() {
|
||||||
return this.$store.getters.mergedConfig.translationLanguage === undefined
|
return this.$store.getters.mergedConfig.translationLanguage === undefined
|
||||||
},
|
},
|
||||||
statusLink () {
|
statusLink() {
|
||||||
if (this.status.is_local) {
|
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 {
|
} else {
|
||||||
return this.status.external_url
|
return this.status.external_url
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shouldConfirmDelete () {
|
shouldConfirmDelete() {
|
||||||
return this.$store.getters.mergedConfig.modalOnDelete
|
return this.$store.getters.mergedConfig.modalOnDelete
|
||||||
},
|
},
|
||||||
isEdited () {
|
isEdited() {
|
||||||
return this.status.edited_at !== null
|
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' }"
|
:bound-to="{ x: 'container' }"
|
||||||
remove-padding
|
remove-padding
|
||||||
>
|
>
|
||||||
<template v-slot:content="{close}">
|
<template #content="{ close }">
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button
|
<button
|
||||||
v-if="canMute && !status.thread_muted"
|
v-if="canMute && !status.thread_muted"
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="eye-slash"
|
icon="eye-slash"
|
||||||
/><span>{{ $t("status.mute_conversation") }}</span>
|
/><span>{{ $t('status.mute_conversation') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canMute && status.thread_muted"
|
v-if="canMute && status.thread_muted"
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="eye-slash"
|
icon="eye-slash"
|
||||||
/><span>{{ $t("status.unmute_conversation") }}</span>
|
/><span>{{ $t('status.unmute_conversation') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!status.pinned && canPin"
|
v-if="!status.pinned && canPin"
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="thumbtack"
|
icon="thumbtack"
|
||||||
/><span>{{ $t("status.pin") }}</span>
|
/><span>{{ $t('status.pin') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="status.pinned && canPin"
|
v-if="status.pinned && canPin"
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="thumbtack"
|
icon="thumbtack"
|
||||||
/><span>{{ $t("status.unpin") }}</span>
|
/><span>{{ $t('status.unpin') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!status.bookmarked"
|
v-if="!status.bookmarked"
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
:icon="['far', 'bookmark']"
|
:icon="['far', 'bookmark']"
|
||||||
/><span>{{ $t("status.bookmark") }}</span>
|
/><span>{{ $t('status.bookmark') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="status.bookmarked"
|
v-if="status.bookmarked"
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="bookmark"
|
icon="bookmark"
|
||||||
/><span>{{ $t("status.unbookmark") }}</span>
|
/><span>{{ $t('status.unbookmark') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="ownStatus && editingAvailable"
|
v-if="ownStatus && editingAvailable"
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="pen"
|
icon="pen"
|
||||||
/><span>{{ $t("status.edit") }}</span>
|
/><span>{{ $t('status.edit') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="isEdited && editingAvailable"
|
v-if="isEdited && editingAvailable"
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="history"
|
icon="history"
|
||||||
/><span>{{ $t("status.edit_history") }}</span>
|
/><span>{{ $t('status.edit_history') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="ownStatus"
|
v-if="ownStatus"
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="file-pen"
|
icon="file-pen"
|
||||||
/><span>{{ $t("status.redraft") }}</span>
|
/><span>{{ $t('status.redraft') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDelete"
|
v-if="canDelete"
|
||||||
|
@ -115,7 +115,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="times"
|
icon="times"
|
||||||
/><span>{{ $t("status.delete") }}</span>
|
/><span>{{ $t('status.delete') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="share-alt"
|
icon="share-alt"
|
||||||
/><span>{{ $t("status.copy_link") }}</span>
|
/><span>{{ $t('status.copy_link') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
v-if="!status.is_local"
|
v-if="!status.is_local"
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="external-link-alt"
|
icon="external-link-alt"
|
||||||
/><span>{{ $t("status.external_source") }}</span>
|
/><span>{{ $t('status.external_source') }}</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@ -147,7 +147,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
:icon="['far', 'flag']"
|
:icon="['far', 'flag']"
|
||||||
/><span>{{ $t("user_card.report") }}</span>
|
/><span>{{ $t('user_card.report') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canTranslate"
|
v-if="canTranslate"
|
||||||
|
@ -158,7 +158,7 @@
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
icon="globe"
|
icon="globe"
|
||||||
/><span>{{ $t("status.translate") }}</span>
|
/><span>{{ $t('status.translate') }}</span>
|
||||||
|
|
||||||
<template v-if="noTranslationTargetSet">
|
<template v-if="noTranslationTargetSet">
|
||||||
<span class="dropdown-item-icon__badge warning">
|
<span class="dropdown-item-icon__badge warning">
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<button class="button-unstyled popover-trigger">
|
<button class="button-unstyled popover-trigger">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
@ -205,7 +205,7 @@
|
||||||
</Popover>
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./extra_buttons.js" ></script>
|
<script src="./extra_buttons.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faStar } from '@fortawesome/free-solid-svg-icons'
|
import { faStar } from '@fortawesome/free-solid-svg-icons'
|
||||||
import {
|
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons'
|
||||||
faStar as faStarRegular
|
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
library.add(faStar, faStarRegular)
|
||||||
faStar,
|
|
||||||
faStarRegular
|
|
||||||
)
|
|
||||||
|
|
||||||
const FavoriteButton = {
|
const FavoriteButton = {
|
||||||
props: ['status', 'loggedIn'],
|
props: ['status', 'loggedIn'],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
animated: false
|
animated: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
favorite () {
|
favorite() {
|
||||||
if (!this.status.favorited) {
|
if (!this.status.favorited) {
|
||||||
this.$store.dispatch('favorite', { id: this.status.id })
|
this.$store.dispatch('favorite', { id: this.status.id })
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,8 +27,10 @@ const FavoriteButton = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
remoteInteractionLink () {
|
remoteInteractionLink() {
|
||||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
return this.$store.getters.remoteInteractionLink({
|
||||||
|
statusId: this.status.id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./favorite_button.js" ></script>
|
<script src="./favorite_button.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
@ -56,6 +56,7 @@
|
||||||
.interactive {
|
.interactive {
|
||||||
.svg-inline--fa {
|
.svg-inline--fa {
|
||||||
animation-duration: 0.6s;
|
animation-duration: 0.6s;
|
||||||
|
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .svg-inline--fa,
|
&:hover .svg-inline--fa,
|
||||||
|
|
|
@ -2,10 +2,20 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
||||||
|
|
||||||
const FeaturesPanel = {
|
const FeaturesPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
whoToFollow: function () {
|
||||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
return this.$store.state.instance.suggestionsEnabled
|
||||||
textlimit: function () { return this.$store.state.instance.textlimit },
|
},
|
||||||
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
|
mediaProxy: function () {
|
||||||
|
return this.$store.state.instance.mediaProxyAvailable
|
||||||
|
},
|
||||||
|
textlimit: function () {
|
||||||
|
return this.$store.state.instance.textlimit
|
||||||
|
},
|
||||||
|
uploadlimit: function () {
|
||||||
|
return fileSizeFormatService.fileSizeFormat(
|
||||||
|
this.$store.state.instance.uploadlimit
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,17 +16,20 @@
|
||||||
</li>
|
</li>
|
||||||
<li>{{ $t('features_panel.scope_options') }}</li>
|
<li>{{ $t('features_panel.scope_options') }}</li>
|
||||||
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./features_panel.js" ></script>
|
<script src="./features_panel.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.features-panel li {
|
.features-panel li {
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,14 +5,11 @@ import {
|
||||||
faExclamationTriangle
|
faExclamationTriangle
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(faStop, faExclamationTriangle)
|
||||||
faStop,
|
|
||||||
faExclamationTriangle
|
|
||||||
)
|
|
||||||
|
|
||||||
const Flash = {
|
const Flash = {
|
||||||
props: [ 'src' ],
|
props: ['src'],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
player: false, // can be true, "hidden", false. hidden = element exists
|
player: false, // can be true, "hidden", false. hidden = element exists
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
@ -20,7 +17,7 @@ const Flash = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openPlayer () {
|
openPlayer() {
|
||||||
if (this.player) return // prevent double-loading, or re-loading on failure
|
if (this.player) return // prevent double-loading, or re-loading on failure
|
||||||
this.player = 'hidden'
|
this.player = 'hidden'
|
||||||
RuffleService.getRuffle().then((ruffle) => {
|
RuffleService.getRuffle().then((ruffle) => {
|
||||||
|
@ -32,17 +29,20 @@ const Flash = {
|
||||||
container.appendChild(player)
|
container.appendChild(player)
|
||||||
player.style.width = '100%'
|
player.style.width = '100%'
|
||||||
player.style.height = '100%'
|
player.style.height = '100%'
|
||||||
player.load(this.src).then(() => {
|
player
|
||||||
this.player = true
|
.load(this.src)
|
||||||
}).catch((e) => {
|
.then(() => {
|
||||||
console.error('Error loading ruffle', e)
|
this.player = true
|
||||||
this.player = 'error'
|
})
|
||||||
})
|
.catch((e) => {
|
||||||
|
console.error('Error loading ruffle', e)
|
||||||
|
this.player = 'error'
|
||||||
|
})
|
||||||
this.ruffleInstance = player
|
this.ruffleInstance = player
|
||||||
this.$emit('playerOpened')
|
this.$emit('playerOpened')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
closePlayer () {
|
closePlayer() {
|
||||||
this.ruffleInstance && this.ruffleInstance.remove()
|
this.ruffleInstance && this.ruffleInstance.remove()
|
||||||
this.player = false
|
this.player = false
|
||||||
this.$emit('playerClosed')
|
this.$emit('playerClosed')
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
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 {
|
export default {
|
||||||
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||||
components: {
|
components: {
|
||||||
ConfirmModal
|
ConfirmModal
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
inProgress: false,
|
inProgress: false,
|
||||||
showingConfirmUnfollow: false
|
showingConfirmUnfollow: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
shouldConfirmUnfollow () {
|
shouldConfirmUnfollow() {
|
||||||
return this.$store.getters.mergedConfig.modalOnUnfollow
|
return this.$store.getters.mergedConfig.modalOnUnfollow
|
||||||
},
|
},
|
||||||
isPressed () {
|
isPressed() {
|
||||||
return this.inProgress || this.relationship.following
|
return this.inProgress || this.relationship.following
|
||||||
},
|
},
|
||||||
title () {
|
title() {
|
||||||
if (this.inProgress || this.relationship.following) {
|
if (this.inProgress || this.relationship.following) {
|
||||||
return this.$t('user_card.follow_unfollow')
|
return this.$t('user_card.follow_unfollow')
|
||||||
} else if (this.relationship.requested) {
|
} else if (this.relationship.requested) {
|
||||||
|
@ -27,7 +30,7 @@ export default {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label () {
|
label() {
|
||||||
if (this.inProgress) {
|
if (this.inProgress) {
|
||||||
return this.$t('user_card.follow_progress')
|
return this.$t('user_card.follow_progress')
|
||||||
} else if (this.relationship.following) {
|
} else if (this.relationship.following) {
|
||||||
|
@ -38,39 +41,44 @@ export default {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
disabled () {
|
disabled() {
|
||||||
return this.inProgress || this.user.deactivated
|
return this.inProgress || this.user.deactivated
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showConfirmUnfollow () {
|
showConfirmUnfollow() {
|
||||||
this.showingConfirmUnfollow = true
|
this.showingConfirmUnfollow = true
|
||||||
},
|
},
|
||||||
hideConfirmUnfollow () {
|
hideConfirmUnfollow() {
|
||||||
this.showingConfirmUnfollow = false
|
this.showingConfirmUnfollow = false
|
||||||
},
|
},
|
||||||
onClick () {
|
onClick() {
|
||||||
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
|
this.relationship.following || this.relationship.requested
|
||||||
|
? this.unfollow()
|
||||||
|
: this.follow()
|
||||||
},
|
},
|
||||||
follow () {
|
follow() {
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
requestFollow(this.relationship.id, this.$store).then(() => {
|
requestFollow(this.relationship.id, this.$store).then(() => {
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unfollow () {
|
unfollow() {
|
||||||
if (this.shouldConfirmUnfollow) {
|
if (this.shouldConfirmUnfollow) {
|
||||||
this.showConfirmUnfollow()
|
this.showConfirmUnfollow()
|
||||||
} else {
|
} else {
|
||||||
this.doUnfollow()
|
this.doUnfollow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doUnfollow () {
|
doUnfollow() {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
requestUnfollow(this.relationship.id, store).then(() => {
|
requestUnfollow(this.relationship.id, store).then(() => {
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
store.commit('removeStatus', {
|
||||||
|
timeline: 'friends',
|
||||||
|
userId: this.relationship.id
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.hideConfirmUnfollow()
|
this.hideConfirmUnfollow()
|
||||||
|
|
|
@ -21,9 +21,7 @@
|
||||||
tag="span"
|
tag="span"
|
||||||
>
|
>
|
||||||
<template #user>
|
<template #user>
|
||||||
<span
|
<span v-text="user.screen_name_ui" />
|
||||||
v-text="user.screen_name_ui"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</confirm-modal>
|
</confirm-modal>
|
||||||
|
|
|
@ -4,10 +4,7 @@ import FollowButton from '../follow_button/follow_button.vue'
|
||||||
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||||
|
|
||||||
const FollowCard = {
|
const FollowCard = {
|
||||||
props: [
|
props: ['user', 'noFollowsYou'],
|
||||||
'user',
|
|
||||||
'noFollowsYou'
|
|
||||||
],
|
|
||||||
components: {
|
components: {
|
||||||
BasicUserCard,
|
BasicUserCard,
|
||||||
RemoteFollow,
|
RemoteFollow,
|
||||||
|
@ -15,13 +12,13 @@ const FollowCard = {
|
||||||
RemoveFollowerButton
|
RemoveFollowerButton
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMe () {
|
isMe() {
|
||||||
return this.$store.state.users.currentUser.id === this.user.id
|
return this.$store.state.users.currentUser.id === this.user.id
|
||||||
},
|
},
|
||||||
loggedIn () {
|
loggedIn() {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
relationship () {
|
relationship() {
|
||||||
return this.$store.getters.relationship(this.user.id)
|
return this.$store.getters.relationship(this.user.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,78 +8,90 @@ const FollowRequestCard = {
|
||||||
BasicUserCard,
|
BasicUserCard,
|
||||||
ConfirmModal
|
ConfirmModal
|
||||||
},
|
},
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
showingApproveConfirmDialog: false,
|
showingApproveConfirmDialog: false,
|
||||||
showingDenyConfirmDialog: false
|
showingDenyConfirmDialog: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
findFollowRequestNotificationId () {
|
findFollowRequestNotificationId() {
|
||||||
const notif = notificationsFromStore(this.$store).find(
|
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
|
return notif && notif.id
|
||||||
},
|
},
|
||||||
showApproveConfirmDialog () {
|
showApproveConfirmDialog() {
|
||||||
this.showingApproveConfirmDialog = true
|
this.showingApproveConfirmDialog = true
|
||||||
},
|
},
|
||||||
hideApproveConfirmDialog () {
|
hideApproveConfirmDialog() {
|
||||||
this.showingApproveConfirmDialog = false
|
this.showingApproveConfirmDialog = false
|
||||||
},
|
},
|
||||||
showDenyConfirmDialog () {
|
showDenyConfirmDialog() {
|
||||||
this.showingDenyConfirmDialog = true
|
this.showingDenyConfirmDialog = true
|
||||||
},
|
},
|
||||||
hideDenyConfirmDialog () {
|
hideDenyConfirmDialog() {
|
||||||
this.showingDenyConfirmDialog = false
|
this.showingDenyConfirmDialog = false
|
||||||
},
|
},
|
||||||
approveUser () {
|
approveUser() {
|
||||||
if (this.shouldConfirmApprove) {
|
if (this.shouldConfirmApprove) {
|
||||||
this.showApproveConfirmDialog()
|
this.showApproveConfirmDialog()
|
||||||
} else {
|
} else {
|
||||||
this.doApprove()
|
this.doApprove()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doApprove () {
|
doApprove() {
|
||||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||||
this.$store.dispatch('removeFollowRequest', this.user)
|
this.$store.dispatch('removeFollowRequest', this.user)
|
||||||
|
this.$store.dispatch('decrementFollowRequestsCount')
|
||||||
|
|
||||||
const notifId = this.findFollowRequestNotificationId()
|
const notifId = this.findFollowRequestNotificationId()
|
||||||
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
||||||
this.$store.dispatch('updateNotification', {
|
this.$store.dispatch('updateNotification', {
|
||||||
id: notifId,
|
id: notifId,
|
||||||
updater: notification => {
|
updater: (notification) => {
|
||||||
notification.type = 'follow'
|
notification.type = 'follow'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.hideApproveConfirmDialog()
|
this.hideApproveConfirmDialog()
|
||||||
},
|
},
|
||||||
denyUser () {
|
denyUser() {
|
||||||
if (this.shouldConfirmDeny) {
|
if (this.shouldConfirmDeny) {
|
||||||
this.showDenyConfirmDialog()
|
this.showDenyConfirmDialog()
|
||||||
} else {
|
} else {
|
||||||
this.doDeny()
|
this.doDeny()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doDeny () {
|
doDeny() {
|
||||||
const notifId = this.findFollowRequestNotificationId()
|
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(() => {
|
.then(() => {
|
||||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||||
|
this.$store.dispatch('decrementFollowRequestsCount')
|
||||||
this.$store.dispatch('removeFollowRequest', this.user)
|
this.$store.dispatch('removeFollowRequest', this.user)
|
||||||
})
|
})
|
||||||
this.hideDenyConfirmDialog()
|
this.hideDenyConfirmDialog()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
mergedConfig () {
|
mergedConfig() {
|
||||||
return this.$store.getters.mergedConfig
|
return this.$store.getters.mergedConfig
|
||||||
},
|
},
|
||||||
shouldConfirmApprove () {
|
shouldConfirmApprove() {
|
||||||
return this.mergedConfig.modalOnApproveFollow
|
return this.mergedConfig.modalOnApproveFollow
|
||||||
},
|
},
|
||||||
shouldConfirmDeny () {
|
shouldConfirmDeny() {
|
||||||
return this.mergedConfig.modalOnDenyFollow
|
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>
|
<template>
|
||||||
<basic-user-card :user="user">
|
<basic-user-card
|
||||||
|
v-if="show"
|
||||||
|
:user="user"
|
||||||
|
>
|
||||||
<div class="follow-request-card-content-container">
|
<div class="follow-request-card-content-container">
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
|
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 = {
|
const FollowRequests = {
|
||||||
components: {
|
components: {
|
||||||
FollowRequestCard
|
FollowRequestCard,
|
||||||
|
FollowRequestList
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
requests () {
|
userId() {
|
||||||
|
return this.$store.state.users.currentUser.id
|
||||||
|
},
|
||||||
|
requests() {
|
||||||
return this.$store.state.api.followRequests
|
return this.$store.state.api.followRequests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FollowRequestCard
|
<FollowRequestList :user-id="userId">
|
||||||
v-for="request in requests"
|
<template #item="{ item }">
|
||||||
:key="request.id"
|
<FollowRequestCard :user="item" />
|
||||||
:user="request"
|
</template>
|
||||||
class="list-item"
|
</FollowRequestList>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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,11 +5,9 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Select
|
Select
|
||||||
},
|
},
|
||||||
props: [
|
props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
|
||||||
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
|
|
||||||
],
|
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
lValue: this.modelValue,
|
lValue: this.modelValue,
|
||||||
availableOptions: [
|
availableOptions: [
|
||||||
|
@ -19,43 +17,45 @@ export default {
|
||||||
'serif',
|
'serif',
|
||||||
'monospace',
|
'monospace',
|
||||||
'sans-serif'
|
'sans-serif'
|
||||||
].filter(_ => _)
|
].filter((_) => _)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate() {
|
||||||
this.lValue = this.modelValue
|
this.lValue = this.modelValue
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
present () {
|
present() {
|
||||||
return typeof this.lValue !== 'undefined'
|
return typeof this.lValue !== 'undefined'
|
||||||
},
|
},
|
||||||
dValue () {
|
dValue() {
|
||||||
return this.lValue || this.fallback || {}
|
return this.lValue || this.fallback || {}
|
||||||
},
|
},
|
||||||
family: {
|
family: {
|
||||||
get () {
|
get() {
|
||||||
return this.dValue.family
|
return this.dValue.family
|
||||||
},
|
},
|
||||||
set (v) {
|
set(v) {
|
||||||
set(this.lValue, 'family', v)
|
set(this.lValue, 'family', v)
|
||||||
this.$emit('update:modelValue', this.lValue)
|
this.$emit('update:modelValue', this.lValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isCustom () {
|
isCustom() {
|
||||||
return this.preset === 'custom'
|
return this.preset === 'custom'
|
||||||
},
|
},
|
||||||
preset: {
|
preset: {
|
||||||
get () {
|
get() {
|
||||||
if (this.family === 'serif' ||
|
if (
|
||||||
this.family === 'sans-serif' ||
|
this.family === 'serif' ||
|
||||||
this.family === 'monospace' ||
|
this.family === 'sans-serif' ||
|
||||||
this.family === 'inherit') {
|
this.family === 'monospace' ||
|
||||||
|
this.family === 'inherit'
|
||||||
|
) {
|
||||||
return this.family
|
return this.family
|
||||||
} else {
|
} else {
|
||||||
return 'custom'
|
return 'custom'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set (v) {
|
set(v) {
|
||||||
this.family = v === 'custom' ? '' : v
|
this.family = v === 'custom' ? '' : v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,13 @@
|
||||||
class="opt exlcude-disabled"
|
class="opt exlcude-disabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="present"
|
:checked="present"
|
||||||
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
@change="
|
||||||
>
|
$emit(
|
||||||
|
'update:modelValue',
|
||||||
|
typeof modelValue === 'undefined' ? fallback : undefined
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
<label
|
<label
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
class="opt-l"
|
class="opt-l"
|
||||||
|
@ -43,11 +48,11 @@
|
||||||
v-model="family"
|
v-model="family"
|
||||||
class="custom-font"
|
class="custom-font"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./font_control.js" ></script>
|
<script src="./font_control.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
|
@ -4,7 +4,9 @@ const FriendsTimeline = {
|
||||||
Timeline
|
Timeline
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeline () { return this.$store.state.statuses.timelines.friends }
|
timeline() {
|
||||||
|
return this.$store.state.statuses.timelines.friends
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ const Gallery = {
|
||||||
'editAttachment',
|
'editAttachment',
|
||||||
'grid'
|
'grid'
|
||||||
],
|
],
|
||||||
data () {
|
data() {
|
||||||
return {
|
return {
|
||||||
sizes: {},
|
sizes: {},
|
||||||
hidingLong: true
|
hidingLong: true
|
||||||
|
@ -25,42 +25,61 @@ const Gallery = {
|
||||||
},
|
},
|
||||||
components: { Attachment },
|
components: { Attachment },
|
||||||
computed: {
|
computed: {
|
||||||
rows () {
|
rows() {
|
||||||
if (!this.attachments) {
|
if (!this.attachments) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const attachments = this.limit > 0
|
const attachments =
|
||||||
? this.attachments.slice(0, this.limit)
|
this.limit > 0
|
||||||
: this.attachments
|
? this.attachments.slice(0, this.limit)
|
||||||
|
: this.attachments
|
||||||
if (this.size === 'hide') {
|
if (this.size === 'hide') {
|
||||||
return attachments.map(item => ({ minimal: true, items: [item] }))
|
return attachments.map((item) => ({ minimal: true, items: [item] }))
|
||||||
}
|
}
|
||||||
const rows = this.grid
|
const rows = this.grid
|
||||||
? [{ grid: true, items: attachments }]
|
? [{ grid: true, items: attachments }]
|
||||||
: attachments.reduce((acc, attachment, i) => {
|
: attachments
|
||||||
if (attachment.mimetype.includes('audio')) {
|
.reduce(
|
||||||
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
|
(acc, attachment, i) => {
|
||||||
}
|
if (attachment.mimetype.includes('audio')) {
|
||||||
if (!(
|
return [
|
||||||
attachment.mimetype.includes('image') ||
|
...acc,
|
||||||
attachment.mimetype.includes('video') ||
|
{ audio: true, items: [attachment] },
|
||||||
attachment.mimetype.includes('flash')
|
{ items: [] }
|
||||||
)) {
|
]
|
||||||
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
|
}
|
||||||
}
|
if (
|
||||||
const maxPerRow = 3
|
!(
|
||||||
const attachmentsRemaining = this.attachments.length - i + 1
|
attachment.mimetype.includes('image') ||
|
||||||
const currentRow = acc[acc.length - 1].items
|
attachment.mimetype.includes('video') ||
|
||||||
currentRow.push(attachment)
|
attachment.mimetype.includes('flash')
|
||||||
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
|
)
|
||||||
return [...acc, { items: [] }]
|
) {
|
||||||
} else {
|
return [
|
||||||
return acc
|
...acc,
|
||||||
}
|
{ minimal: true, items: [attachment] },
|
||||||
}, [{ items: [] }]).filter(_ => _.items.length > 0)
|
{ items: [] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const maxPerRow = 3
|
||||||
|
const attachmentsRemaining = this.attachments.length - i + 1
|
||||||
|
const currentRow = acc[acc.length - 1].items
|
||||||
|
currentRow.push(attachment)
|
||||||
|
if (
|
||||||
|
currentRow.length >= maxPerRow &&
|
||||||
|
attachmentsRemaining > maxPerRow
|
||||||
|
) {
|
||||||
|
return [...acc, { items: [] }]
|
||||||
|
} else {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[{ items: [] }]
|
||||||
|
)
|
||||||
|
.filter((_) => _.items.length > 0)
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
attachmentsDimensionalScore () {
|
attachmentsDimensionalScore() {
|
||||||
return this.rows.reduce((acc, row) => {
|
return this.rows.reduce((acc, row) => {
|
||||||
let size = 0
|
let size = 0
|
||||||
if (row.minimal) {
|
if (row.minimal) {
|
||||||
|
@ -73,7 +92,7 @@ const Gallery = {
|
||||||
return acc + size
|
return acc + size
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
tooManyAttachments () {
|
tooManyAttachments() {
|
||||||
if (this.editable || this.size === 'small') {
|
if (this.editable || this.size === 'small') {
|
||||||
return false
|
return false
|
||||||
} else if (this.size === 'hide') {
|
} else if (this.size === 'hide') {
|
||||||
|
@ -84,32 +103,32 @@ const Gallery = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNaturalSizeLoad ({ id, width, height }) {
|
onNaturalSizeLoad({ id, width, height }) {
|
||||||
set(this.sizes, id, { width, height })
|
set(this.sizes, id, { width, height })
|
||||||
},
|
},
|
||||||
rowStyle (row) {
|
rowStyle(row) {
|
||||||
if (row.audio) {
|
if (row.audio) {
|
||||||
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||||
} else if (!row.minimal && !row.grid) {
|
} 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) {
|
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%` }
|
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
|
||||||
},
|
},
|
||||||
getAspectRatio (id) {
|
getAspectRatio(id) {
|
||||||
const size = this.sizes[id]
|
const size = this.sizes[id]
|
||||||
return size ? size.width / size.height : 1
|
return size ? size.width / size.height : 1
|
||||||
},
|
},
|
||||||
toggleHidingLong (event) {
|
toggleHidingLong(event) {
|
||||||
this.hidingLong = event
|
this.hidingLong = event
|
||||||
},
|
},
|
||||||
openGallery () {
|
openGallery() {
|
||||||
this.$store.dispatch('setMedia', this.attachments)
|
this.$store.dispatch('setMedia', this.attachments)
|
||||||
this.$store.dispatch('setCurrentMedia', this.attachments[0])
|
this.$store.dispatch('setCurrentMedia', this.attachments[0])
|
||||||
},
|
},
|
||||||
onMedia () {
|
onMedia() {
|
||||||
this.$store.dispatch('setMedia', this.attachments)
|
this.$store.dispatch('setMedia', this.attachments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,20 @@
|
||||||
:size="size"
|
:size="size"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:remove="removeAttachment"
|
:remove="removeAttachment"
|
||||||
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
|
:shift-up="
|
||||||
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
|
!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment
|
||||||
|
"
|
||||||
|
:shift-dn="
|
||||||
|
!(
|
||||||
|
attachmentIndex === row.items.length - 1 &&
|
||||||
|
rowIndex === rows.length - 1
|
||||||
|
) && shiftDnAttachment
|
||||||
|
"
|
||||||
:edit="editAttachment"
|
:edit="editAttachment"
|
||||||
:description="descriptions && descriptions[attachment.id]"
|
:description="descriptions && descriptions[attachment.id]"
|
||||||
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
|
:hide-description="
|
||||||
|
size === 'small' || (tooManyAttachments && hidingLong)
|
||||||
|
"
|
||||||
:style="itemStyle(attachment.id, row.items)"
|
:style="itemStyle(attachment.id, row.items)"
|
||||||
@setMedia="onMedia"
|
@setMedia="onMedia"
|
||||||
@naturalSizeLoad="onNaturalSizeLoad"
|
@naturalSizeLoad="onNaturalSizeLoad"
|
||||||
|
@ -42,7 +51,7 @@
|
||||||
class="many-attachments"
|
class="many-attachments"
|
||||||
>
|
>
|
||||||
<div class="many-attachments-text">
|
<div class="many-attachments-text">
|
||||||
{{ $t("status.many_attachments", { number: attachments.length }) }}
|
{{ $t('status.many_attachments', { number: attachments.length }) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="many-attachments-buttons">
|
<div class="many-attachments-buttons">
|
||||||
<span
|
<span
|
||||||
|
@ -53,7 +62,7 @@
|
||||||
class="button-unstyled -link"
|
class="button-unstyled -link"
|
||||||
@click="toggleHidingLong(true)"
|
@click="toggleHidingLong(true)"
|
||||||
>
|
>
|
||||||
{{ $t("status.collapse_attachments") }}
|
{{ $t('status.collapse_attachments') }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -64,7 +73,7 @@
|
||||||
class="button-unstyled -link"
|
class="button-unstyled -link"
|
||||||
@click="toggleHidingLong(false)"
|
@click="toggleHidingLong(false)"
|
||||||
>
|
>
|
||||||
{{ $t("status.show_all_attachments") }}
|
{{ $t('status.show_all_attachments') }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -75,7 +84,7 @@
|
||||||
class="button-unstyled -link"
|
class="button-unstyled -link"
|
||||||
@click="openGallery"
|
@click="openGallery"
|
||||||
>
|
>
|
||||||
{{ $t("status.open_gallery") }}
|
{{ $t('status.open_gallery') }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,7 +92,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src='./gallery.js'></script>
|
<script src="./gallery.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
@ -109,8 +118,8 @@
|
||||||
.gallery-rows {
|
.gallery-rows {
|
||||||
max-height: 25em;
|
max-height: 25em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
mask:
|
mask: linear-gradient(to top, white, transparent) bottom/100% 70px
|
||||||
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
no-repeat,
|
||||||
linear-gradient(to top, white, white);
|
linear-gradient(to top, white, white);
|
||||||
|
|
||||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
faTimes
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
library.add(
|
library.add(faTimes)
|
||||||
faTimes
|
|
||||||
)
|
|
||||||
|
|
||||||
const GlobalNoticeList = {
|
const GlobalNoticeList = {
|
||||||
computed: {
|
computed: {
|
||||||
notices () {
|
notices() {
|
||||||
return this.$store.state.interface.globalNotices
|
return this.$store.state.interface.globalNotices
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeNotice (notice) {
|
closeNotice(notice) {
|
||||||
this.$store.dispatch('removeGlobalNotice', notice)
|
this.$store.dispatch('removeGlobalNotice', notice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue