Merge remote-tracking branch 'upstream/develop' into docs
* upstream/develop: (374 commits) fix typo rename mutations according to actual property names fix fix fix logged out post-update fix user banner fix AMERICA comments No longer sending extra data, renamed some properties Revert "add TOTP/Recovery Form for mobile version" Apply suggestion to src/services/entity_normalizer/entity_normalizer.service.js i18n/Update Japanese translation render modal at the root level using portal install portal vue Small improve of the who to follow panel layout Fix/Small fix in the who to follow page remove console spam i18n wire up user.description with masto api data ...
This commit is contained in:
commit
b00da17788
32
.eslintrc.js
32
.eslintrc.js
|
@ -1,14 +1,17 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: 'babel-eslint',
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint',
|
||||||
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: 'standard',
|
extends: [
|
||||||
|
'standard',
|
||||||
|
'plugin:vue/recommended'
|
||||||
|
],
|
||||||
// required to lint *.vue files
|
// required to lint *.vue files
|
||||||
plugins: [
|
plugins: [
|
||||||
'html'
|
'vue'
|
||||||
],
|
],
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -17,6 +20,27 @@ module.exports = {
|
||||||
// allow async-await
|
// allow async-await
|
||||||
'generator-star-spacing': 0,
|
'generator-star-spacing': 0,
|
||||||
// allow debugger during development
|
// allow debugger during development
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||||
|
// Webpack 4 update commit, most of these probably should be fixed and removed in a separate MR
|
||||||
|
// A lot of errors come from .vue files that are now properly linted
|
||||||
|
'vue/valid-v-if': 1,
|
||||||
|
'vue/use-v-on-exact': 1,
|
||||||
|
'vue/no-parsing-error': 1,
|
||||||
|
'vue/require-v-for-key': 1,
|
||||||
|
'vue/valid-v-for': 1,
|
||||||
|
'vue/require-prop-types': 1,
|
||||||
|
'vue/no-use-v-if-with-v-for': 1,
|
||||||
|
'indent': 1,
|
||||||
|
'import/first': 1,
|
||||||
|
'object-curly-spacing': 1,
|
||||||
|
'prefer-promise-reject-errors': 1,
|
||||||
|
'eol-last': 1,
|
||||||
|
'no-return-await': 1,
|
||||||
|
'no-multi-spaces': 1,
|
||||||
|
'no-trailing-spaces': 1,
|
||||||
|
'no-unused-expressions': 1,
|
||||||
|
'no-mixed-operators': 1,
|
||||||
|
'camelcase': 1,
|
||||||
|
'no-multiple-empty-lines': 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# This file is a template, and might need editing before it works on your project.
|
# This file is a template, and might need editing before it works on your project.
|
||||||
# Official framework image. Look for the different tagged releases at:
|
# Official framework image. Look for the different tagged releases at:
|
||||||
# https://hub.docker.com/r/library/node/tags/
|
# https://hub.docker.com/r/library/node/tags/
|
||||||
image: node:7
|
image: node:8
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- lint
|
- lint
|
||||||
|
@ -16,7 +16,12 @@ lint:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
|
variables:
|
||||||
|
APT_CACHE_DIR: apt-cache
|
||||||
script:
|
script:
|
||||||
|
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||||
|
- apt install firefox-esr -y --no-install-recommends
|
||||||
|
- firefox --version
|
||||||
- yarn
|
- yarn
|
||||||
- npm run unit
|
- npm run unit
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
# v1.0
|
||||||
|
## Removed features/radically changed behavior
|
||||||
|
### minimalScopesMode
|
||||||
|
As of !633, `scopeOptions` is no longer available and instead is changed for `minimalScopesMode` (default: `false`)
|
||||||
|
|
||||||
|
Reasoning is that scopeOptions option originally existed mostly as a backwards-compatibility with GNU Social which only had `public` scope available and using scope selector would''t work. Since at some point we dropped GNU Social support, this option was mostly a nuisance (being default `false`'), however some people think scopes are an annoyance to a certain degree and want as less of that feature as possible.
|
||||||
|
|
||||||
|
Solution - to only show minimal set among: *Direct*, *User default* and *Scope of post replying to*. This also makes it impossible to reply to a DM with a non-DM post from UI.
|
||||||
|
|
||||||
|
*This setting is admin-default, user-configurable. Admin can choose different default for their instance but user can override it.*
|
|
@ -41,7 +41,7 @@ FE Build process also leaves current commit hash in global variable `___pleromaf
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings.
|
Edit config.json for configuration.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
var sass = require('sass')
|
||||||
|
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'
|
||||||
|
@ -11,51 +12,51 @@ exports.assetsPath = function (_path) {
|
||||||
|
|
||||||
exports.cssLoaders = function (options) {
|
exports.cssLoaders = function (options) {
|
||||||
options = options || {}
|
options = options || {}
|
||||||
// generate loader string to be used with extract text plugin
|
|
||||||
function generateLoaders (loaders) {
|
|
||||||
var sourceLoader = loaders.map(function (loader) {
|
|
||||||
var extraParamChar
|
|
||||||
if (/\?/.test(loader)) {
|
|
||||||
loader = loader.replace(/\?/, '-loader?')
|
|
||||||
extraParamChar = '&'
|
|
||||||
} else {
|
|
||||||
loader = loader + '-loader'
|
|
||||||
extraParamChar = '?'
|
|
||||||
}
|
|
||||||
return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
|
|
||||||
}).join('!')
|
|
||||||
|
|
||||||
|
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) {
|
||||||
return ExtractTextPlugin.extract('vue-style-loader', sourceLoader)
|
return [MiniCssExtractPlugin.loader].concat(loaders)
|
||||||
} else {
|
} else {
|
||||||
return ['vue-style-loader', sourceLoader].join('!')
|
return ['vue-style-loader'].concat(loaders)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
|
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
|
||||||
return {
|
return [
|
||||||
css: generateLoaders(['css']),
|
{
|
||||||
postcss: generateLoaders(['css']),
|
test: /\.(post)?css$/,
|
||||||
less: generateLoaders(['css', 'less']),
|
use: generateLoaders(['css-loader']),
|
||||||
sass: generateLoaders(['css', 'sass?indentedSyntax']),
|
},
|
||||||
scss: generateLoaders(['css', 'sass']),
|
{
|
||||||
stylus: generateLoaders(['css', 'stylus']),
|
test: /\.less$/,
|
||||||
styl: generateLoaders(['css', 'stylus'])
|
use: generateLoaders(['css-loader', 'less-loader']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.sass$/,
|
||||||
|
use: generateLoaders([
|
||||||
|
'css-loader',
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
indentedSyntax: true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: generateLoaders(['css-loader', 'sass-loader'])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.styl(us)?$/,
|
||||||
|
use: generateLoaders(['css-loader', 'stylus-loader']),
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate loaders for standalone style files (outside of .vue)
|
// Generate loaders for standalone style files (outside of .vue)
|
||||||
exports.styleLoaders = function (options) {
|
exports.styleLoaders = function (options) {
|
||||||
var output = []
|
return exports.cssLoaders(options)
|
||||||
var loaders = exports.cssLoaders(options)
|
|
||||||
for (var extension in loaders) {
|
|
||||||
var loader = loaders[extension]
|
|
||||||
output.push({
|
|
||||||
test: new RegExp('\\.' + extension + '$'),
|
|
||||||
loader: loader
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,16 @@ module.exports = {
|
||||||
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: {
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all'
|
||||||
|
}
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['', '.js', '.vue'],
|
extensions: ['.js', '.vue'],
|
||||||
fallback: [path.join(__dirname, '../node_modules')],
|
modules: [
|
||||||
|
path.join(__dirname, '../node_modules')
|
||||||
|
],
|
||||||
alias: {
|
alias: {
|
||||||
'vue$': 'vue/dist/vue.runtime.common',
|
'vue$': 'vue/dist/vue.runtime.common',
|
||||||
'src': path.resolve(__dirname, '../src'),
|
'src': path.resolve(__dirname, '../src'),
|
||||||
|
@ -30,67 +37,53 @@ module.exports = {
|
||||||
'components': path.resolve(__dirname, '../src/components')
|
'components': path.resolve(__dirname, '../src/components')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolveLoader: {
|
|
||||||
fallback: [path.join(__dirname, '../node_modules')]
|
|
||||||
},
|
|
||||||
module: {
|
module: {
|
||||||
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
||||||
preLoaders: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
enforce: 'pre',
|
||||||
loader: 'eslint',
|
test: /\.(js|vue)$/,
|
||||||
include: projectRoot,
|
include: projectRoot,
|
||||||
exclude: /node_modules/
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: 'eslint-loader',
|
||||||
|
options: {
|
||||||
|
formatter: require('eslint-friendly-formatter'),
|
||||||
|
sourceMap: config.build.productionSourceMap,
|
||||||
|
extract: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
loader: 'eslint',
|
|
||||||
include: projectRoot,
|
|
||||||
exclude: /node_modules/
|
|
||||||
}
|
|
||||||
],
|
|
||||||
loaders: [
|
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
loader: 'vue'
|
use: 'vue-loader'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
loader: 'babel',
|
|
||||||
include: projectRoot,
|
include: projectRoot,
|
||||||
exclude: /node_modules\/(?!tributejs)/
|
exclude: /node_modules\/(?!tributejs)/,
|
||||||
},
|
use: 'babel-loader'
|
||||||
{
|
|
||||||
test: /\.json$/,
|
|
||||||
loader: 'json'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||||
loader: 'url',
|
use: {
|
||||||
query: {
|
loader: 'url-loader',
|
||||||
|
options: {
|
||||||
limit: 10000,
|
limit: 10000,
|
||||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||||
loader: 'url',
|
use: {
|
||||||
query: {
|
loader: 'url-loader',
|
||||||
|
options: {
|
||||||
limit: 10000,
|
limit: 10000,
|
||||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
eslint: {
|
|
||||||
formatter: require('eslint-friendly-formatter')
|
|
||||||
},
|
|
||||||
vue: {
|
|
||||||
loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }),
|
|
||||||
postcss: [
|
|
||||||
require('autoprefixer')({
|
|
||||||
browsers: ['last 2 versions']
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
@ -12,8 +12,9 @@ Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||||
|
|
||||||
module.exports = merge(baseWebpackConfig, {
|
module.exports = merge(baseWebpackConfig, {
|
||||||
module: {
|
module: {
|
||||||
loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||||
},
|
},
|
||||||
|
mode: 'development',
|
||||||
// eval-source-map is faster for development
|
// eval-source-map is faster for development
|
||||||
devtool: '#eval-source-map',
|
devtool: '#eval-source-map',
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -23,9 +24,7 @@ module.exports = merge(baseWebpackConfig, {
|
||||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
|
'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
|
||||||
}),
|
}),
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
new webpack.NoErrorsPlugin(),
|
|
||||||
// https://github.com/ampedandwired/html-webpack-plugin
|
// https://github.com/ampedandwired/html-webpack-plugin
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
|
|
|
@ -4,7 +4,7 @@ var utils = require('./utils')
|
||||||
var webpack = require('webpack')
|
var webpack = require('webpack')
|
||||||
var merge = require('webpack-merge')
|
var merge = require('webpack-merge')
|
||||||
var baseWebpackConfig = require('./webpack.base.conf')
|
var baseWebpackConfig = require('./webpack.base.conf')
|
||||||
var ExtractTextPlugin = require('extract-text-webpack-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')
|
||||||
|
@ -13,23 +13,23 @@ var env = process.env.NODE_ENV === 'testing'
|
||||||
let commitHash = require('child_process')
|
let commitHash = require('child_process')
|
||||||
.execSync('git rev-parse --short HEAD')
|
.execSync('git rev-parse --short HEAD')
|
||||||
.toString();
|
.toString();
|
||||||
console.log(commitHash)
|
|
||||||
|
|
||||||
var webpackConfig = merge(baseWebpackConfig, {
|
var webpackConfig = merge(baseWebpackConfig, {
|
||||||
|
mode: 'production',
|
||||||
module: {
|
module: {
|
||||||
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
|
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||||
},
|
},
|
||||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all'
|
||||||
|
}
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
path: config.build.assetsRoot,
|
path: config.build.assetsRoot,
|
||||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||||
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
|
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
|
||||||
},
|
|
||||||
vue: {
|
|
||||||
loaders: utils.cssLoaders({
|
|
||||||
sourceMap: config.build.productionSourceMap,
|
|
||||||
extract: true
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||||
|
@ -38,14 +38,10 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||||
'DEV_OVERRIDES': JSON.stringify(undefined)
|
'DEV_OVERRIDES': JSON.stringify(undefined)
|
||||||
}),
|
}),
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
|
||||||
compress: {
|
|
||||||
warnings: false
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
|
||||||
// extract css into its own file
|
// extract css into its own file
|
||||||
new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
|
new MiniCssExtractPlugin({
|
||||||
|
filename: utils.assetsPath('css/[name].[contenthash].css')
|
||||||
|
}),
|
||||||
// generate dist index.html with correct asset hash for caching.
|
// generate dist index.html with correct asset hash for caching.
|
||||||
// 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
|
||||||
|
@ -67,25 +63,11 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
chunksSortMode: 'dependency'
|
chunksSortMode: 'dependency'
|
||||||
}),
|
}),
|
||||||
// split vendor js into its own file
|
// split vendor js into its own file
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
|
||||||
name: 'vendor',
|
|
||||||
minChunks: function (module, count) {
|
|
||||||
// any required modules inside node_modules are extracted to vendor
|
|
||||||
return (
|
|
||||||
module.resource &&
|
|
||||||
/\.js$/.test(module.resource) &&
|
|
||||||
module.resource.indexOf(
|
|
||||||
path.join(__dirname, '../node_modules')
|
|
||||||
) === 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// 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
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
// new webpack.optimize.SplitChunksPlugin({
|
||||||
name: 'manifest',
|
// name: ['app', 'vendor']
|
||||||
chunks: ['vendor']
|
// }),
|
||||||
})
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||||
<title>Pleroma</title>
|
<title>Pleroma</title>
|
||||||
<!--server-generated-meta-->
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
|
|
60
package.json
60
package.json
|
@ -11,9 +11,11 @@
|
||||||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||||
"e2e": "node test/e2e/runner.js",
|
"e2e": "node test/e2e/runner.js",
|
||||||
"test": "npm run unit && npm run e2e",
|
"test": "npm run unit && npm run e2e",
|
||||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
|
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||||
|
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chenfengyuan/vue-qrcode": "^1.0.0",
|
||||||
"babel-plugin-add-module-exports": "^0.2.1",
|
"babel-plugin-add-module-exports": "^0.2.1",
|
||||||
"babel-plugin-lodash": "^3.2.11",
|
"babel-plugin-lodash": "^3.2.11",
|
||||||
"chromatism": "^3.0.0",
|
"chromatism": "^3.0.0",
|
||||||
|
@ -21,15 +23,16 @@
|
||||||
"diff": "^3.0.1",
|
"diff": "^3.0.1",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-mocha-reporter": "^2.2.1",
|
||||||
"localforage": "^1.5.0",
|
"localforage": "^1.5.0",
|
||||||
"node-sass": "^3.10.1",
|
|
||||||
"object-path": "^0.11.3",
|
"object-path": "^0.11.3",
|
||||||
"phoenix": "^1.3.0",
|
"phoenix": "^1.3.0",
|
||||||
|
"popper.js": "^1.14.7",
|
||||||
|
"portal-vue": "^2.1.4",
|
||||||
"sanitize-html": "^1.13.0",
|
"sanitize-html": "^1.13.0",
|
||||||
"sass-loader": "^4.0.2",
|
"v-click-outside": "^2.1.1",
|
||||||
"vue": "^2.5.13",
|
"vue": "^2.5.13",
|
||||||
"vue-chat-scroll": "^1.2.1",
|
"vue-chat-scroll": "^1.2.1",
|
||||||
"vue-compose": "^0.7.1",
|
|
||||||
"vue-i18n": "^7.3.2",
|
"vue-i18n": "^7.3.2",
|
||||||
|
"vue-popperjs": "^2.0.3",
|
||||||
"vue-router": "^3.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vue-template-compiler": "^2.3.4",
|
"vue-template-compiler": "^2.3.4",
|
||||||
"vue-timeago": "^3.1.2",
|
"vue-timeago": "^3.1.2",
|
||||||
|
@ -44,7 +47,7 @@
|
||||||
"babel-core": "^6.0.0",
|
"babel-core": "^6.0.0",
|
||||||
"babel-eslint": "^7.0.0",
|
"babel-eslint": "^7.0.0",
|
||||||
"babel-helper-vue-jsx-merge-props": "^2.0.3",
|
"babel-helper-vue-jsx-merge-props": "^2.0.3",
|
||||||
"babel-loader": "^6.0.0",
|
"babel-loader": "^7.0.0",
|
||||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||||
"babel-plugin-transform-runtime": "^6.0.0",
|
"babel-plugin-transform-runtime": "^6.0.0",
|
||||||
"babel-plugin-transform-vue-jsx": "3",
|
"babel-plugin-transform-vue-jsx": "3",
|
||||||
|
@ -57,52 +60,55 @@
|
||||||
"chromedriver": "^2.21.2",
|
"chromedriver": "^2.21.2",
|
||||||
"connect-history-api-fallback": "^1.1.0",
|
"connect-history-api-fallback": "^1.1.0",
|
||||||
"cross-spawn": "^4.0.2",
|
"cross-spawn": "^4.0.2",
|
||||||
"css-loader": "^0.25.0",
|
"css-loader": "^0.28.0",
|
||||||
"eslint": "^3.7.1",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-standard": "^6.1.0",
|
"eslint-config-standard": "^12.0.0",
|
||||||
"eslint-friendly-formatter": "^2.0.5",
|
"eslint-friendly-formatter": "^2.0.5",
|
||||||
"eslint-loader": "^1.5.0",
|
"eslint-loader": "^2.1.0",
|
||||||
"eslint-plugin-html": "^1.5.5",
|
"eslint-plugin-import": "^2.13.0",
|
||||||
"eslint-plugin-promise": "^2.0.1",
|
"eslint-plugin-node": "^7.0.0",
|
||||||
"eslint-plugin-standard": "^2.0.1",
|
"eslint-plugin-promise": "^4.0.0",
|
||||||
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
|
"eslint-plugin-vue": "^5.2.2",
|
||||||
"eventsource-polyfill": "^0.9.6",
|
"eventsource-polyfill": "^0.9.6",
|
||||||
"express": "^4.13.3",
|
"express": "^4.13.3",
|
||||||
"extract-text-webpack-plugin": "^1.0.1",
|
"file-loader": "^3.0.1",
|
||||||
"file-loader": "^0.9.0",
|
|
||||||
"function-bind": "^1.0.2",
|
"function-bind": "^1.0.2",
|
||||||
"html-webpack-plugin": "^2.8.1",
|
"html-webpack-plugin": "^3.0.0",
|
||||||
"http-proxy-middleware": "^0.17.2",
|
"http-proxy-middleware": "^0.17.2",
|
||||||
"inject-loader": "^2.0.1",
|
"inject-loader": "^2.0.1",
|
||||||
"iso-639-1": "^2.0.3",
|
"iso-639-1": "^2.0.3",
|
||||||
"isparta-loader": "^2.0.0",
|
"isparta-loader": "^2.0.0",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"karma": "^1.3.0",
|
"karma": "^3.0.0",
|
||||||
"karma-coverage": "^1.1.1",
|
"karma-coverage": "^1.1.1",
|
||||||
"karma-mocha": "^1.2.0",
|
"karma-mocha": "^1.2.0",
|
||||||
"karma-phantomjs-launcher": "^1.0.0",
|
"karma-firefox-launcher": "^1.1.0",
|
||||||
"karma-sinon-chai": "^1.2.0",
|
"karma-sinon-chai": "^2.0.2",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
"karma-spec-reporter": "0.0.26",
|
"karma-spec-reporter": "0.0.26",
|
||||||
"karma-webpack": "^1.7.0",
|
"karma-webpack": "^4.0.0-rc.3",
|
||||||
"lodash": "^4.16.4",
|
"lodash": "^4.16.4",
|
||||||
"lolex": "^1.4.0",
|
"lolex": "^1.4.0",
|
||||||
|
"mini-css-extract-plugin": "^0.5.0",
|
||||||
"mocha": "^3.1.0",
|
"mocha": "^3.1.0",
|
||||||
"nightwatch": "^0.9.8",
|
"nightwatch": "^0.9.8",
|
||||||
"opn": "^4.0.2",
|
"opn": "^4.0.2",
|
||||||
"ora": "^0.3.0",
|
"ora": "^0.3.0",
|
||||||
"phantomjs-prebuilt": "^2.1.3",
|
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
|
"sass": "^1.17.3",
|
||||||
|
"sass-loader": "git://github.com/webpack-contrib/sass-loader",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
"serviceworker-webpack-plugin": "0.2.3",
|
"serviceworker-webpack-plugin": "^1.0.0",
|
||||||
"shelljs": "^0.7.4",
|
"shelljs": "^0.7.4",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^2.1.0",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^2.8.0",
|
||||||
"url-loader": "^0.5.7",
|
"url-loader": "^1.1.2",
|
||||||
"vue-loader": "^11.1.0",
|
"vue-loader": "^14.0.0",
|
||||||
"vue-style-loader": "^2.0.0",
|
"vue-style-loader": "^4.0.0",
|
||||||
"webpack": "^1.13.2",
|
"webpack": "^4.0.0",
|
||||||
"webpack-dev-middleware": "^1.8.3",
|
"webpack-dev-middleware": "^3.6.0",
|
||||||
"webpack-hot-middleware": "^2.12.2",
|
"webpack-hot-middleware": "^2.12.2",
|
||||||
"webpack-merge": "^0.14.1"
|
"webpack-merge": "^0.14.1"
|
||||||
},
|
},
|
||||||
|
|
29
src/App.js
29
src/App.js
|
@ -9,7 +9,9 @@ import ChatPanel from './components/chat_panel/chat_panel.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MediaModal from './components/media_modal/media_modal.vue'
|
||||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue'
|
import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue'
|
||||||
import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
|
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||||
|
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||||
|
import { windowWidth } from './services/window_utils/window_utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -24,7 +26,9 @@ export default {
|
||||||
ChatPanel,
|
ChatPanel,
|
||||||
MediaModal,
|
MediaModal,
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
MobilePostStatusModal
|
MobilePostStatusModal,
|
||||||
|
MobileNav,
|
||||||
|
UserReportingModal
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline',
|
mobileActivePanel: 'timeline',
|
||||||
|
@ -40,6 +44,10 @@ export default {
|
||||||
created () {
|
created () {
|
||||||
// Load the locale from the storage
|
// Load the locale from the storage
|
||||||
this.$i18n.locale = this.$store.state.config.interfaceLanguage
|
this.$i18n.locale = this.$store.state.config.interfaceLanguage
|
||||||
|
window.addEventListener('resize', this.updateMobileState)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
window.removeEventListener('resize', this.updateMobileState)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
|
@ -82,13 +90,8 @@ export default {
|
||||||
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
||||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel },
|
showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||||
unseenNotifications () {
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||||
return unseenNotificationsFromStore(this.$store)
|
isMobileLayout () { return this.$store.state.interface.mobileLayout }
|
||||||
},
|
|
||||||
unseenNotificationsCount () {
|
|
||||||
return this.unseenNotifications.length
|
|
||||||
},
|
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToTop () {
|
scrollToTop () {
|
||||||
|
@ -101,8 +104,12 @@ export default {
|
||||||
onFinderToggled (hidden) {
|
onFinderToggled (hidden) {
|
||||||
this.finderHidden = hidden
|
this.finderHidden = hidden
|
||||||
},
|
},
|
||||||
toggleMobileSidebar () {
|
updateMobileState () {
|
||||||
this.$refs.sideDrawer.toggleDrawer()
|
const mobileLayout = windowWidth() <= 800
|
||||||
|
const changed = mobileLayout !== this.isMobileLayout
|
||||||
|
if (changed) {
|
||||||
|
this.$store.dispatch('setMobileLayout', mobileLayout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
133
src/App.scss
133
src/App.scss
|
@ -101,6 +101,14 @@ button {
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--bg, $fallback--bg)
|
background-color: var(--bg, $fallback--bg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
// TODO: add better color variable
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--alertErrorPanelText, $fallback--text);
|
||||||
|
background-color: $fallback--alertError;
|
||||||
|
background-color: var(--alertError, $fallback--alertError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label.select {
|
label.select {
|
||||||
|
@ -371,6 +379,7 @@ main-router {
|
||||||
|
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: none;
|
||||||
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
|
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
|
||||||
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
|
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
@ -484,24 +493,6 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-button {
|
|
||||||
display: none;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-dot {
|
|
||||||
border-radius: 100%;
|
|
||||||
height: 8px;
|
|
||||||
width: 8px;
|
|
||||||
position: absolute;
|
|
||||||
left: calc(50% - 4px);
|
|
||||||
top: calc(50% - 4px);
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-top: -6px;
|
|
||||||
background-color: $fallback--cRed;
|
|
||||||
background-color: var(--badgeNotification, $fallback--cRed);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity .2s
|
transition: opacity .2s
|
||||||
}
|
}
|
||||||
|
@ -530,20 +521,6 @@ nav {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-switcher {
|
|
||||||
display: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 46px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
flex: 1;
|
|
||||||
max-height: 32px;
|
|
||||||
margin: 0.5em;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (min-width: 800px) {
|
@media all and (min-width: 800px) {
|
||||||
body {
|
body {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
@ -648,21 +625,6 @@ nav {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visibility-tray {
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
color: $fallback--lightText;
|
|
||||||
color: var(--lightText, $fallback--lightText);
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.visibility-notice {
|
.visibility-notice {
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
border: 1px solid $fallback--faint;
|
border: 1px solid $fallback--faint;
|
||||||
|
@ -671,6 +633,19 @@ nav {
|
||||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice-dismissible {
|
||||||
|
padding-right: 4rem;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.dismiss {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: .5em;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes modal-background-fadein {
|
@keyframes modal-background-fadein {
|
||||||
from {
|
from {
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
@ -750,6 +725,70 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||||
|
margin: 1em 1em 1.4em;
|
||||||
|
padding-bottom: 1.4em;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailable,
|
||||||
|
.unavailable i {
|
||||||
|
color: var(--cRed, $fallback--cRed);
|
||||||
|
color: $fallback--cRed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-height: 28px;
|
||||||
|
min-width: 10em;
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-input {
|
||||||
|
max-width: 6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select-multiple {
|
||||||
|
display: flex;
|
||||||
|
.option-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setting-list,
|
||||||
|
.option-list{
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 2em;
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.suboptions {
|
||||||
|
margin-top: 0.3em
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.login-hint {
|
.login-hint {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|
15
src/App.vue
15
src/App.vue
|
@ -1,17 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" v-bind:style="bgAppStyle">
|
<div id="app" v-bind:style="bgAppStyle">
|
||||||
<div class="app-bg-wrapper" v-bind:style="bgStyle"></div>
|
<div class="app-bg-wrapper" v-bind:style="bgStyle"></div>
|
||||||
<nav class='nav-bar container' @click="scrollToTop()" id="nav">
|
<MobileNav v-if="isMobileLayout" />
|
||||||
|
<nav v-else class='nav-bar container' @click="scrollToTop()" id="nav">
|
||||||
<div class='logo' :style='logoBgStyle'>
|
<div class='logo' :style='logoBgStyle'>
|
||||||
<div class='mask' :style='logoMaskStyle'></div>
|
<div class='mask' :style='logoMaskStyle'></div>
|
||||||
<img :src='logo' :style='logoStyle'>
|
<img :src='logo' :style='logoStyle'>
|
||||||
</div>
|
</div>
|
||||||
<div class='inner-nav'>
|
<div class='inner-nav'>
|
||||||
<div class='item'>
|
<div class='item'>
|
||||||
<a href="#" class="menu-button" @click.stop.prevent="toggleMobileSidebar()">
|
|
||||||
<i class="button-icon icon-menu"></i>
|
|
||||||
<div class="alert-dot" v-if="unseenNotificationsCount"></div>
|
|
||||||
</a>
|
|
||||||
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
|
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class='item right'>
|
<div class='item right'>
|
||||||
|
@ -21,13 +18,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div v-if="" class="container" id="content">
|
<div class="container" id="content">
|
||||||
<side-drawer ref="sideDrawer" :logout="logout"></side-drawer>
|
|
||||||
<div class="sidebar-flexer mobile-hidden">
|
<div class="sidebar-flexer mobile-hidden">
|
||||||
<div class="sidebar-bounds">
|
<div class="sidebar-bounds">
|
||||||
<div class="sidebar-scroller">
|
<div class="sidebar-scroller">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<user-panel></user-panel>
|
<user-panel></user-panel>
|
||||||
|
<div v-if="!isMobileLayout">
|
||||||
<nav-panel></nav-panel>
|
<nav-panel></nav-panel>
|
||||||
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
|
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
|
||||||
<features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
|
<features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
|
||||||
|
@ -37,6 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div v-if="!currentUser" class="login-hint panel panel-default">
|
<div v-if="!currentUser" class="login-hint panel panel-default">
|
||||||
<router-link :to="{ name: 'login' }" class="panel-body">
|
<router-link :to="{ name: 'login' }" class="panel-body">
|
||||||
|
@ -50,7 +48,8 @@
|
||||||
<media-modal></media-modal>
|
<media-modal></media-modal>
|
||||||
</div>
|
</div>
|
||||||
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
|
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
|
||||||
<MobilePostStatusModal />
|
<UserReportingModal />
|
||||||
|
<portal-target name="modal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
|
|
||||||
import App from '../App.vue'
|
import App from '../App.vue'
|
||||||
|
import { windowWidth } from '../services/window_utils/window_utils'
|
||||||
|
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||||
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
|
|
||||||
const getStatusnetConfig = async ({ store }) => {
|
const getStatusnetConfig = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/api/statusnet/config.json')
|
const res = await window.fetch('/api/statusnet/config.json')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site
|
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
||||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' })
|
||||||
|
|
||||||
// TODO: default values for this stuff, added if to not make it break on
|
// TODO: default values for this stuff, added if to not make it break on
|
||||||
// my dev config out of the box.
|
// my dev config out of the box.
|
||||||
|
@ -91,15 +94,15 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
? 0
|
? 0
|
||||||
: config.logoMargin
|
: config.logoMargin
|
||||||
})
|
})
|
||||||
|
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||||
|
|
||||||
copyInstanceOption('redirectRootNoLogin')
|
copyInstanceOption('redirectRootNoLogin')
|
||||||
copyInstanceOption('redirectRootLogin')
|
copyInstanceOption('redirectRootLogin')
|
||||||
copyInstanceOption('showInstanceSpecificPanel')
|
copyInstanceOption('showInstanceSpecificPanel')
|
||||||
copyInstanceOption('scopeOptionsEnabled')
|
copyInstanceOption('minimalScopesMode')
|
||||||
copyInstanceOption('formattingOptionsEnabled')
|
copyInstanceOption('formattingOptionsEnabled')
|
||||||
copyInstanceOption('hideMutedPosts')
|
copyInstanceOption('hideMutedPosts')
|
||||||
copyInstanceOption('collapseMessageWithSubject')
|
copyInstanceOption('collapseMessageWithSubject')
|
||||||
copyInstanceOption('loginMethod')
|
|
||||||
copyInstanceOption('scopeCopy')
|
copyInstanceOption('scopeCopy')
|
||||||
copyInstanceOption('subjectLineBehavior')
|
copyInstanceOption('subjectLineBehavior')
|
||||||
copyInstanceOption('postContentType')
|
copyInstanceOption('postContentType')
|
||||||
|
@ -170,9 +173,10 @@ const getCustomEmoji = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/api/pleroma/emoji.json')
|
const res = await window.fetch('/api/pleroma/emoji.json')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const values = await res.json()
|
const result = await res.json()
|
||||||
|
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
|
||||||
const emoji = Object.keys(values).map((key) => {
|
const emoji = Object.keys(values).map((key) => {
|
||||||
return { shortcode: key, image_url: values[key] }
|
return { shortcode: key, image_url: values[key].image_url || values[key] }
|
||||||
})
|
})
|
||||||
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
|
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
|
||||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
|
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
|
||||||
|
@ -186,6 +190,17 @@ const getCustomEmoji = async ({ store }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAppSecret = async ({ store }) => {
|
||||||
|
const { state, commit } = store
|
||||||
|
const { oauth, instance } = state
|
||||||
|
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
|
||||||
|
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||||
|
.then((token) => {
|
||||||
|
commit('setAppToken', token.access_token)
|
||||||
|
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getNodeInfo = async ({ store }) => {
|
const getNodeInfo = async ({ store }) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.fetch('/nodeinfo/2.0.json')
|
const res = await window.fetch('/nodeinfo/2.0.json')
|
||||||
|
@ -210,6 +225,7 @@ const getNodeInfo = async ({ store }) => {
|
||||||
|
|
||||||
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 })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
|
||||||
} else {
|
} else {
|
||||||
throw (res)
|
throw (res)
|
||||||
}
|
}
|
||||||
|
@ -219,6 +235,28 @@ const getNodeInfo = async ({ store }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setConfig = async ({ store }) => {
|
||||||
|
// apiConfig, staticConfig
|
||||||
|
const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()])
|
||||||
|
const apiConfig = configInfos[0]
|
||||||
|
const staticConfig = configInfos[1]
|
||||||
|
|
||||||
|
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkOAuthToken = async ({ store }) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (store.getters.getUserToken()) {
|
||||||
|
try {
|
||||||
|
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const afterStoreSetup = async ({ store, i18n }) => {
|
const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
if (store.state.config.customTheme) {
|
if (store.state.config.customTheme) {
|
||||||
// This is a hack to deal with async loading of config.json and themes
|
// This is a hack to deal with async loading of config.json and themes
|
||||||
|
@ -230,19 +268,19 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiConfig = await getStatusnetConfig({ store })
|
const width = windowWidth()
|
||||||
const staticConfig = await getStaticConfig()
|
store.dispatch('setMobileLayout', width <= 800)
|
||||||
await setSettings({ store, apiConfig, staticConfig })
|
|
||||||
await getTOS({ store })
|
|
||||||
await getInstancePanel({ store })
|
|
||||||
await getStaticEmoji({ store })
|
|
||||||
await getCustomEmoji({ store })
|
|
||||||
await getNodeInfo({ store })
|
|
||||||
|
|
||||||
// Now we have the server settings and can try logging in
|
// Now we can try getting the server settings and logging in
|
||||||
if (store.state.oauth.token) {
|
await Promise.all([
|
||||||
await store.dispatch('loginUser', store.state.oauth.token)
|
checkOAuthToken({ store }),
|
||||||
}
|
setConfig({ store }),
|
||||||
|
getTOS({ store }),
|
||||||
|
getInstancePanel({ store }),
|
||||||
|
getStaticEmoji({ store }),
|
||||||
|
getCustomEmoji({ store }),
|
||||||
|
getNodeInfo({ store })
|
||||||
|
])
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PublicAndExternalTimeline from 'components/public_and_external_timeline/p
|
||||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||||
import Mentions from 'components/mentions/mentions.vue'
|
import Interactions from 'components/interactions/interactions.vue'
|
||||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||||
import Settings from 'components/settings/settings.vue'
|
import Settings from 'components/settings/settings.vue'
|
||||||
|
@ -13,7 +13,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||||
import UserSearch from 'components/user_search/user_search.vue'
|
import UserSearch from 'components/user_search/user_search.vue'
|
||||||
import Notifications from 'components/notifications/notifications.vue'
|
import Notifications from 'components/notifications/notifications.vue'
|
||||||
import LoginForm from 'components/login_form/login_form.vue'
|
import AuthForm from 'components/auth_form/auth_form.js'
|
||||||
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
||||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||||
import About from 'components/about/about.vue'
|
import About from 'components/about/about.vue'
|
||||||
|
@ -34,7 +34,7 @@ export default (store) => {
|
||||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||||
{ name: 'mentions', path: '/users/:username/mentions', component: Mentions },
|
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions },
|
||||||
{ name: 'dms', path: '/users/:username/dms', component: DMs },
|
{ name: 'dms', path: '/users/:username/dms', component: DMs },
|
||||||
{ name: 'settings', path: '/settings', component: Settings },
|
{ name: 'settings', path: '/settings', component: Settings },
|
||||||
{ name: 'registration', path: '/registration', component: Registration },
|
{ name: 'registration', path: '/registration', component: Registration },
|
||||||
|
@ -42,7 +42,7 @@ export default (store) => {
|
||||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
|
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
|
||||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
|
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
|
||||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
|
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
|
||||||
{ name: 'login', path: '/login', component: LoginForm },
|
{ name: 'login', path: '/login', component: AuthForm },
|
||||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||||
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
|
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import LoginForm from '../login_form/login_form.vue'
|
||||||
|
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||||
|
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
const AuthForm = {
|
||||||
|
name: 'AuthForm',
|
||||||
|
render (createElement) {
|
||||||
|
return createElement('component', { is: this.authForm })
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
authForm () {
|
||||||
|
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||||
|
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||||
|
return 'LoginForm'
|
||||||
|
},
|
||||||
|
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MFARecoveryForm,
|
||||||
|
MFATOTPForm,
|
||||||
|
LoginForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthForm
|
|
@ -0,0 +1,52 @@
|
||||||
|
const debounceMilliseconds = 500
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
query: { // function to query results and return a promise
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
filter: { // function to filter results in real time
|
||||||
|
type: Function
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Search...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
term: '',
|
||||||
|
timeout: null,
|
||||||
|
results: [],
|
||||||
|
resultsVisible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filtered () {
|
||||||
|
return this.filter ? this.filter(this.results) : this.results
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
term (val) {
|
||||||
|
this.fetchResults(val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchResults (term) {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.results = []
|
||||||
|
if (term) {
|
||||||
|
this.query(term).then((results) => { this.results = results })
|
||||||
|
}
|
||||||
|
}, debounceMilliseconds)
|
||||||
|
},
|
||||||
|
onInputClick () {
|
||||||
|
this.resultsVisible = true
|
||||||
|
},
|
||||||
|
onClickOutside () {
|
||||||
|
this.resultsVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div class="autosuggest" v-click-outside="onClickOutside">
|
||||||
|
<input v-model="term" :placeholder="placeholder" @click="onInputClick" class="autosuggest-input" />
|
||||||
|
<div class="autosuggest-results" v-if="resultsVisible && filtered.length > 0">
|
||||||
|
<slot v-for="item in filtered" :item="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./autosuggest.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.autosuggest {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-results {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
max-height: 400px;
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--lightBg, $fallback--lightBg);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: $fallback--border;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
border-radius: $fallback--inputRadius;
|
||||||
|
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
|
||||||
|
box-shadow: var(--panelShadow);
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
|
const AvatarList = {
|
||||||
|
props: ['users'],
|
||||||
|
computed: {
|
||||||
|
slicedUsers () {
|
||||||
|
return this.users ? this.users.slice(0, 15) : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
userProfileLink (user) {
|
||||||
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarList
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div class="avatars">
|
||||||
|
<router-link :to="userProfileLink(user)" class="avatars-item" v-for="user in slicedUsers">
|
||||||
|
<UserAvatar :user="user" class="avatar-small" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./avatar_list.js" ></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.avatars {
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
// For hiding overflowing elements
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
.avatars-item {
|
||||||
|
margin: 0 0 5px 5px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-small {
|
||||||
|
border-radius: $fallback--avatarAltRadius;
|
||||||
|
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="basic-user-card">
|
<div class="basic-user-card">
|
||||||
<router-link :to="userProfileLink(user)">
|
<router-link :to="userProfileLink(user)">
|
||||||
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
|
<UserAvatar
|
||||||
|
class="avatar"
|
||||||
|
:user="user"
|
||||||
|
@click.prevent.native="toggleUserExpanded"
|
||||||
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="basic-user-card-expanded-content" v-if="userExpanded">
|
<div class="basic-user-card-expanded-content" v-if="userExpanded">
|
||||||
<UserCard :user="user" :rounded="true" :bordered="true"/>
|
<UserCard :user="user" :rounded="true" :bordered="true"/>
|
||||||
|
@ -24,19 +28,11 @@
|
||||||
<script src="./basic_user_card.js"></script>
|
<script src="./basic_user_card.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.basic-user-card {
|
.basic-user-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-top: 0.6em;
|
padding: 0.6em 1em;
|
||||||
padding-right: 1em;
|
|
||||||
padding-bottom: 0.6em;
|
|
||||||
padding-left: 1em;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
border-bottom-color: $fallback--border;
|
|
||||||
border-bottom-color: var(--border, $fallback--border);
|
|
||||||
|
|
||||||
&-collapsed-content {
|
&-collapsed-content {
|
||||||
margin-left: 0.7em;
|
margin-left: 0.7em;
|
||||||
|
@ -52,15 +48,16 @@
|
||||||
width: 16px;
|
width: 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-value {
|
&-user-name-value,
|
||||||
|
&-screen-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&-expanded-content {
|
&-expanded-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" :indeterminate.prop="indeterminate">
|
||||||
|
<i class="checkbox-indicator" />
|
||||||
|
<span v-if="!!$slots.default"><slot></slot></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'checked',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
props: ['checked', 'indeterminate']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 1.2em;
|
||||||
|
min-height: 1.2em;
|
||||||
|
|
||||||
|
&-indicator::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: block;
|
||||||
|
content: '✔';
|
||||||
|
transition: color 200ms;
|
||||||
|
width: 1.1em;
|
||||||
|
height: 1.1em;
|
||||||
|
border-radius: $fallback--checkboxRadius;
|
||||||
|
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||||
|
box-shadow: 0px 0px 2px black inset;
|
||||||
|
box-shadow: var(--inputShadow);
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--input, $fallback--fg);
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.1em;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=checkbox] {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:checked + .checkbox-indicator::before {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:indeterminate + .checkbox-indicator::before {
|
||||||
|
content: '–';
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled + .checkbox-indicator::before {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,4 @@
|
||||||
import { reduce, filter, findIndex } from 'lodash'
|
import { reduce, filter, findIndex, clone } from 'lodash'
|
||||||
import { set } from 'vue'
|
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
const sortById = (a, b) => {
|
||||||
|
@ -36,14 +35,14 @@ const conversation = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
expanded: false,
|
expanded: false
|
||||||
converationStatusIds: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'statusoid',
|
'statusoid',
|
||||||
'collapsable',
|
'collapsable',
|
||||||
'isPage'
|
'isPage',
|
||||||
|
'showPinned'
|
||||||
],
|
],
|
||||||
created () {
|
created () {
|
||||||
if (this.isPage) {
|
if (this.isPage) {
|
||||||
|
@ -54,15 +53,6 @@ const conversation = {
|
||||||
status () {
|
status () {
|
||||||
return this.statusoid
|
return this.statusoid
|
||||||
},
|
},
|
||||||
idsToShow () {
|
|
||||||
if (this.converationStatusIds.length > 0) {
|
|
||||||
return this.converationStatusIds
|
|
||||||
} else if (this.statusId) {
|
|
||||||
return [this.statusId]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
statusId () {
|
statusId () {
|
||||||
if (this.statusoid.retweeted_status) {
|
if (this.statusoid.retweeted_status) {
|
||||||
return this.statusoid.retweeted_status.id
|
return this.statusoid.retweeted_status.id
|
||||||
|
@ -70,6 +60,13 @@ const conversation = {
|
||||||
return this.statusoid.id
|
return this.statusoid.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
conversationId () {
|
||||||
|
if (this.statusoid.retweeted_status) {
|
||||||
|
return this.statusoid.retweeted_status.statusnet_conversation_id
|
||||||
|
} else {
|
||||||
|
return this.statusoid.statusnet_conversation_id
|
||||||
|
}
|
||||||
|
},
|
||||||
conversation () {
|
conversation () {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
return []
|
return []
|
||||||
|
@ -79,12 +76,7 @@ const conversation = {
|
||||||
return [this.status]
|
return [this.status]
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusesObject = this.$store.state.statuses.allStatusesObject
|
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
|
||||||
const conversation = this.idsToShow.reduce((acc, id) => {
|
|
||||||
acc.push(statusesObject[id])
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const statusIndex = findIndex(conversation, { id: this.statusId })
|
const statusIndex = findIndex(conversation, { id: this.statusId })
|
||||||
if (statusIndex !== -1) {
|
if (statusIndex !== -1) {
|
||||||
conversation[statusIndex] = this.status
|
conversation[statusIndex] = this.status
|
||||||
|
@ -131,10 +123,6 @@ const conversation = {
|
||||||
.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 })
|
||||||
set(this, 'converationStatusIds', [].concat(
|
|
||||||
ancestors.map(_ => _.id).filter(_ => _ !== this.statusId),
|
|
||||||
this.statusId,
|
|
||||||
descendants.map(_ => _.id).filter(_ => _ !== this.statusId)))
|
|
||||||
})
|
})
|
||||||
.then(() => this.setHighlight(this.statusId))
|
.then(() => this.setHighlight(this.statusId))
|
||||||
} else {
|
} else {
|
||||||
|
@ -152,6 +140,7 @@ const conversation = {
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
},
|
},
|
||||||
getHighlight () {
|
getHighlight () {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.highlight : null
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
@goto="setHighlight"
|
@goto="setHighlight"
|
||||||
@toggleExpanded="toggleExpanded"
|
@toggleExpanded="toggleExpanded"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
:inlineExpanded="collapsable"
|
:inlineExpanded="collapsable && isExpanded"
|
||||||
:statusoid="status"
|
:statusoid="status"
|
||||||
:expandable='!expanded'
|
:expandable='!isExpanded'
|
||||||
|
:showPinned="showPinned"
|
||||||
:focused="focused(status.id)"
|
:focused="focused(status.id)"
|
||||||
:inConversation="isExpanded"
|
:inConversation="isExpanded"
|
||||||
:highlight="getHighlight()"
|
:highlight="getHighlight()"
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
const DeleteButton = {
|
|
||||||
props: [ 'status' ],
|
|
||||||
methods: {
|
|
||||||
deleteStatus () {
|
|
||||||
const confirmed = window.confirm('Do you really want to delete this status?')
|
|
||||||
if (confirmed) {
|
|
||||||
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
|
||||||
canDelete () { return this.currentUser && this.currentUser.rights.delete_others_notice || this.status.user.id === this.currentUser.id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DeleteButton
|
|
|
@ -1,21 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="canDelete">
|
|
||||||
<a href="#" v-on:click.prevent="deleteStatus()">
|
|
||||||
<i class='button-icon icon-cancel delete-status'></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./delete_button.js" ></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.icon-cancel,.delete-status {
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
color: $fallback--cRed;
|
|
||||||
color: var(--cRed, $fallback--cRed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
const DialogModal = {
|
||||||
|
props: {
|
||||||
|
darkOverlay: {
|
||||||
|
default: true,
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
onCancel: {
|
||||||
|
default: () => {},
|
||||||
|
type: Function
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DialogModal
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<span v-bind:class="{ 'dark-overlay': darkOverlay }" @click.self.stop='onCancel()'>
|
||||||
|
<div class="dialog-modal panel panel-default" @click.stop=''>
|
||||||
|
<div class="panel-heading dialog-modal-heading">
|
||||||
|
<div class="title">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-modal-content">
|
||||||
|
<slot name="default"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-modal-footer user-interactions panel-footer">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./dialog_modal.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
// TODO: unify with other modals.
|
||||||
|
.dark-overlay {
|
||||||
|
&::before {
|
||||||
|
bottom: 0;
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
cursor: default;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
background: rgba(27,31,35,.5);
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-modal.panel {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
max-height: 80vh;
|
||||||
|
max-width: 90vw;
|
||||||
|
margin: 15vh auto;
|
||||||
|
position: fixed;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 999;
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
background-color: $fallback--bg;
|
||||||
|
background-color: var(--bg, $fallback--bg);
|
||||||
|
|
||||||
|
.dialog-modal-heading {
|
||||||
|
padding: .5em .5em;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--panelText);
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--panel, $fallback--fg);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-modal-content {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--lightBg, $fallback--lightBg);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-modal-footer {
|
||||||
|
margin: 0;
|
||||||
|
padding: .5em .5em;
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--lightBg, $fallback--lightBg);
|
||||||
|
border-top: 1px solid $fallback--bg;
|
||||||
|
border-top: 1px solid var(--bg, $fallback--bg);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: auto;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,48 @@
|
||||||
|
const Exporter = {
|
||||||
|
props: {
|
||||||
|
getContent: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
filename: {
|
||||||
|
type: String,
|
||||||
|
default: 'export.csv'
|
||||||
|
},
|
||||||
|
exportButtonLabel: {
|
||||||
|
type: String,
|
||||||
|
default () {
|
||||||
|
return this.$t('exporter.export')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
processingMessage: {
|
||||||
|
type: String,
|
||||||
|
default () {
|
||||||
|
return this.$t('exporter.processing')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
process () {
|
||||||
|
this.processing = true
|
||||||
|
this.getContent()
|
||||||
|
.then((content) => {
|
||||||
|
const fileToDownload = document.createElement('a')
|
||||||
|
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
|
||||||
|
fileToDownload.setAttribute('download', this.filename)
|
||||||
|
fileToDownload.style.display = 'none'
|
||||||
|
document.body.appendChild(fileToDownload)
|
||||||
|
fileToDownload.click()
|
||||||
|
document.body.removeChild(fileToDownload)
|
||||||
|
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||||
|
setTimeout(() => { this.processing = false }, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Exporter
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="exporter">
|
||||||
|
<div v-if="processing">
|
||||||
|
<i class="icon-spin4 animate-spin exporter-processing"></i>
|
||||||
|
<span>{{processingMessage}}</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-default" @click="process" v-else>{{exportButtonLabel}}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./exporter.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.exporter {
|
||||||
|
&-processing {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,64 @@
|
||||||
|
import Popper from 'vue-popperjs/src/component/popper.js.vue'
|
||||||
|
|
||||||
|
const ExtraButtons = {
|
||||||
|
props: [ 'status' ],
|
||||||
|
components: {
|
||||||
|
Popper
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showDropDown: false,
|
||||||
|
showPopper: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteStatus () {
|
||||||
|
this.refreshPopper()
|
||||||
|
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||||
|
if (confirmed) {
|
||||||
|
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleMenu () {
|
||||||
|
this.showDropDown = !this.showDropDown
|
||||||
|
},
|
||||||
|
pinStatus () {
|
||||||
|
this.refreshPopper()
|
||||||
|
this.$store.dispatch('pinStatus', this.status.id)
|
||||||
|
.then(() => this.$emit('onSuccess'))
|
||||||
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
unpinStatus () {
|
||||||
|
this.refreshPopper()
|
||||||
|
this.$store.dispatch('unpinStatus', this.status.id)
|
||||||
|
.then(() => this.$emit('onSuccess'))
|
||||||
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
refreshPopper () {
|
||||||
|
this.showPopper = false
|
||||||
|
this.showDropDown = false
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showPopper = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
|
canDelete () {
|
||||||
|
if (!this.currentUser) { return }
|
||||||
|
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||||
|
return superuser || this.status.user.id === this.currentUser.id
|
||||||
|
},
|
||||||
|
ownStatus () {
|
||||||
|
return this.status.user.id === this.currentUser.id
|
||||||
|
},
|
||||||
|
canPin () {
|
||||||
|
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
|
||||||
|
},
|
||||||
|
enabled () {
|
||||||
|
return this.canPin || this.canDelete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExtraButtons
|
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<Popper
|
||||||
|
trigger="click"
|
||||||
|
@hide='showDropDown = false'
|
||||||
|
append-to-body
|
||||||
|
v-if="enabled && showPopper"
|
||||||
|
:options="{
|
||||||
|
placement: 'top',
|
||||||
|
modifiers: {
|
||||||
|
arrow: { enabled: true },
|
||||||
|
offset: { offset: '0, 5px' },
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="popper-wrapper">
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button class="dropdown-item dropdown-item-icon" @click.prevent="pinStatus" v-if="!status.pinned && canPin">
|
||||||
|
<i class="icon-pin"></i><span>{{$t("status.pin")}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item dropdown-item-icon" @click.prevent="unpinStatus" v-if="status.pinned && canPin">
|
||||||
|
<i class="icon-pin"></i><span>{{$t("status.unpin")}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" v-if="canDelete">
|
||||||
|
<i class="icon-cancel"></i><span>{{$t("status.delete")}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-icon" slot="reference" @click="toggleMenu">
|
||||||
|
<i class='icon-ellipsis' :class="{'icon-clicked': showDropDown}"></i>
|
||||||
|
</div>
|
||||||
|
</Popper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./extra_buttons.js" ></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
@import '../popper/popper.scss';
|
||||||
|
|
||||||
|
.icon-ellipsis {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover, &.icon-clicked {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,7 +6,7 @@ const FeaturesPanel = {
|
||||||
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
||||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||||
scopeOptions: function () { return this.$store.state.instance.scopeOptionsEnabled },
|
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
||||||
textlimit: function () { return this.$store.state.instance.textlimit }
|
textlimit: function () { return this.$store.state.instance.textlimit }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<li v-if="gopher">{{$t('features_panel.gopher')}}</li>
|
<li v-if="gopher">{{$t('features_panel.gopher')}}</li>
|
||||||
<li v-if="whoToFollow">{{$t('features_panel.who_to_follow')}}</li>
|
<li v-if="whoToFollow">{{$t('features_panel.who_to_follow')}}</li>
|
||||||
<li v-if="mediaProxy">{{$t('features_panel.media_proxy')}}</li>
|
<li v-if="mediaProxy">{{$t('features_panel.media_proxy')}}</li>
|
||||||
<li v-if="scopeOptions">{{$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>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,8 +10,7 @@ const FollowCard = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
inProgress: false,
|
inProgress: false,
|
||||||
requestSent: false,
|
requestSent: false
|
||||||
updated: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -19,10 +18,8 @@ const FollowCard = {
|
||||||
RemoteFollow
|
RemoteFollow
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMe () { return this.$store.state.users.currentUser.id === this.user.id },
|
isMe () {
|
||||||
following () { return this.updated ? this.updated.following : this.user.following },
|
return this.$store.state.users.currentUser.id === this.user.id
|
||||||
showFollow () {
|
|
||||||
return !this.following || this.updated && !this.updated.following
|
|
||||||
},
|
},
|
||||||
loggedIn () {
|
loggedIn () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
|
@ -31,17 +28,15 @@ const FollowCard = {
|
||||||
methods: {
|
methods: {
|
||||||
followUser () {
|
followUser () {
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
|
requestFollow(this.user, this.$store).then(({ sent }) => {
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
this.requestSent = sent
|
this.requestSent = sent
|
||||||
this.updated = updated
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unfollowUser () {
|
unfollowUser () {
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
requestUnfollow(this.user, this.$store).then(({ updated }) => {
|
requestUnfollow(this.user, this.$store).then(() => {
|
||||||
this.inProgress = false
|
this.inProgress = false
|
||||||
this.updated = updated
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,14 @@
|
||||||
<span class="faint" v-if="!noFollowsYou && user.follows_you">
|
<span class="faint" v-if="!noFollowsYou && user.follows_you">
|
||||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||||
</span>
|
</span>
|
||||||
<div class="follow-card-follow-button" v-if="showFollow && !loggedIn">
|
<template v-if="!loggedIn">
|
||||||
|
<div class="follow-card-follow-button" v-if="!user.following">
|
||||||
<RemoteFollow :user="user" />
|
<RemoteFollow :user="user" />
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
v-if="showFollow && loggedIn"
|
v-if="!user.following"
|
||||||
class="btn btn-default follow-card-follow-button"
|
class="btn btn-default follow-card-follow-button"
|
||||||
@click="followUser"
|
@click="followUser"
|
||||||
:disabled="inProgress"
|
:disabled="inProgress"
|
||||||
|
@ -24,7 +27,7 @@
|
||||||
{{ $t('user_card.follow') }}
|
{{ $t('user_card.follow') }}
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="following" class="btn btn-default follow-card-follow-button pressed" @click="unfollowUser" :disabled="inProgress">
|
<button v-else class="btn btn-default follow-card-follow-button pressed" @click="unfollowUser" :disabled="inProgress">
|
||||||
<template v-if="inProgress">
|
<template v-if="inProgress">
|
||||||
{{ $t('user_card.follow_progress') }}
|
{{ $t('user_card.follow_progress') }}
|
||||||
</template>
|
</template>
|
||||||
|
@ -32,6 +35,7 @@
|
||||||
{{ $t('user_card.follow_unfollow') }}
|
{{ $t('user_card.follow_unfollow') }}
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</basic-user-card>
|
</basic-user-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{{$t('nav.friend_requests')}}
|
{{$t('nav.friend_requests')}}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FollowRequestCard v-for="request in requests" :key="request.id" :user="request"/>
|
<FollowRequestCard v-for="request in requests" :key="request.id" :user="request" class="list-item"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -70,22 +70,10 @@ const ImageCropper = {
|
||||||
this.dataUrl = undefined
|
this.dataUrl = undefined
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
},
|
},
|
||||||
submit () {
|
submit (cropping = true) {
|
||||||
this.submitting = true
|
this.submitting = true
|
||||||
this.avatarUploadError = null
|
this.avatarUploadError = null
|
||||||
this.submitHandler(this.cropper, this.file)
|
this.submitHandler(cropping && this.cropper, this.file)
|
||||||
.then(() => this.destroy())
|
|
||||||
.catch((err) => {
|
|
||||||
this.submitError = err
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.submitting = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
submitWithoutCropping () {
|
|
||||||
this.submitting = true
|
|
||||||
this.avatarUploadError = null
|
|
||||||
this.submitHandler(false, this.dataUrl)
|
|
||||||
.then(() => this.destroy())
|
.then(() => this.destroy())
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.submitError = err
|
this.submitError = err
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
|
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
|
||||||
</div>
|
</div>
|
||||||
<div class="image-cropper-buttons-wrapper">
|
<div class="image-cropper-buttons-wrapper">
|
||||||
<button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button>
|
<button class="btn" type="button" :disabled="submitting" @click="submit()" v-text="saveText"></button>
|
||||||
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
|
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
|
||||||
<button class="btn" type="button" :disabled="submitting" @click="submitWithoutCropping" v-text="saveWithoutCroppingText"></button>
|
<button class="btn" type="button" :disabled="submitting" @click="submit(false)" v-text="saveWithoutCroppingText"></button>
|
||||||
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
|
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert error" v-if="submitError">
|
<div class="alert error" v-if="submitError">
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
const Importer = {
|
||||||
|
props: {
|
||||||
|
submitHandler: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitButtonLabel: {
|
||||||
|
type: String,
|
||||||
|
default () {
|
||||||
|
return this.$t('importer.submit')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
successMessage: {
|
||||||
|
type: String,
|
||||||
|
default () {
|
||||||
|
return this.$t('importer.success')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
type: String,
|
||||||
|
default () {
|
||||||
|
return this.$t('importer.error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
file: null,
|
||||||
|
error: false,
|
||||||
|
success: false,
|
||||||
|
submitting: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
change () {
|
||||||
|
this.file = this.$refs.input.files[0]
|
||||||
|
},
|
||||||
|
submit () {
|
||||||
|
this.dismiss()
|
||||||
|
this.submitting = true
|
||||||
|
this.submitHandler(this.file)
|
||||||
|
.then(() => { this.success = true })
|
||||||
|
.catch(() => { this.error = true })
|
||||||
|
.finally(() => { this.submitting = false })
|
||||||
|
},
|
||||||
|
dismiss () {
|
||||||
|
this.success = false
|
||||||
|
this.error = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Importer
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<div class="importer">
|
||||||
|
<form>
|
||||||
|
<input type="file" ref="input" v-on:change="change" />
|
||||||
|
</form>
|
||||||
|
<i class="icon-spin4 animate-spin importer-uploading" v-if="submitting"></i>
|
||||||
|
<button class="btn btn-default" v-else @click="submit">{{submitButtonLabel}}</button>
|
||||||
|
<div v-if="success">
|
||||||
|
<i class="icon-cross" @click="dismiss"></i>
|
||||||
|
<p>{{successMessage}}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error">
|
||||||
|
<i class="icon-cross" @click="dismiss"></i>
|
||||||
|
<p>{{errorMessage}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./importer.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.importer {
|
||||||
|
&-uploading {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Notifications from '../notifications/notifications.vue'
|
||||||
|
|
||||||
|
const tabModeDict = {
|
||||||
|
mentions: ['mention'],
|
||||||
|
'likes+repeats': ['repeat', 'like'],
|
||||||
|
follows: ['follow']
|
||||||
|
}
|
||||||
|
|
||||||
|
const Interactions = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
filterMode: tabModeDict['mentions']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onModeSwitch (index, dataset) {
|
||||||
|
this.filterMode = tabModeDict[dataset.filter]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Interactions
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="title">
|
||||||
|
{{ $t("nav.interactions") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<tab-switcher
|
||||||
|
ref="tabSwitcher"
|
||||||
|
:onSwitch="onModeSwitch"
|
||||||
|
>
|
||||||
|
<span data-tab-dummy data-filter="mentions" :label="$t('nav.mentions')"/>
|
||||||
|
<span data-tab-dummy data-filter="likes+repeats" :label="$t('interactions.favs_repeats')"/>
|
||||||
|
<span data-tab-dummy data-filter="follows" :label="$t('interactions.follows')"/>
|
||||||
|
</tab-switcher>
|
||||||
|
<Notifications
|
||||||
|
ref="notifications"
|
||||||
|
:noHeading="true"
|
||||||
|
:minimalMode="true"
|
||||||
|
:filterMode="filterMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./interactions.js"></script>
|
|
@ -26,7 +26,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
languageNames () {
|
languageNames () {
|
||||||
return _.map(this.languageCodes, ISO6391.getName)
|
return _.map(this.languageCodes, this.getLanguageName)
|
||||||
},
|
},
|
||||||
|
|
||||||
language: {
|
language: {
|
||||||
|
@ -36,6 +36,17 @@
|
||||||
this.$i18n.locale = val
|
this.$i18n.locale = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getLanguageName (code) {
|
||||||
|
const specialLanguageNames = {
|
||||||
|
'ja': 'Japanese (やさしいにほんご)',
|
||||||
|
'ja_pedantic': 'Japanese (日本語)',
|
||||||
|
'zh': 'Chinese (简体中文)'
|
||||||
|
}
|
||||||
|
return specialLanguageNames[code] || ISO6391.getName(code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="list">
|
||||||
|
<div v-for="item in items" class="list-item" :key="getKey(item)">
|
||||||
|
<slot name="item" :item="item" />
|
||||||
|
</div>
|
||||||
|
<div class="list-empty-content faint" v-if="items.length === 0 && !!$slots.empty">
|
||||||
|
<slot name="empty" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
getKey: {
|
||||||
|
type: Function,
|
||||||
|
default: item => item.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.list {
|
||||||
|
&-item:not(:last-child) {
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
border-bottom-color: $fallback--border;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-empty-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,50 +1,80 @@
|
||||||
|
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||||
import oauthApi from '../../services/new_api/oauth.js'
|
import oauthApi from '../../services/new_api/oauth.js'
|
||||||
|
|
||||||
const LoginForm = {
|
const LoginForm = {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
user: {},
|
user: {},
|
||||||
authError: false
|
error: false
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
loginMethod () { return this.$store.state.instance.loginMethod },
|
isPasswordAuth () { return this.requiredPassword },
|
||||||
loggingIn () { return this.$store.state.users.loggingIn },
|
isTokenAuth () { return this.requiredToken },
|
||||||
registrationOpen () { return this.$store.state.instance.registrationOpen }
|
...mapState({
|
||||||
|
registrationOpen: state => state.instance.registrationOpen,
|
||||||
|
instance: state => state.instance,
|
||||||
|
loggingIn: state => state.users.loggingIn,
|
||||||
|
oauth: state => state.oauth
|
||||||
|
}),
|
||||||
|
...mapGetters(
|
||||||
|
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
|
||||||
|
)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
oAuthLogin () {
|
...mapMutations('authFlow', ['requireMFA']),
|
||||||
oauthApi.login({
|
...mapActions({ login: 'authFlow/login' }),
|
||||||
oauth: this.$store.state.oauth,
|
|
||||||
instance: this.$store.state.instance.server,
|
|
||||||
commit: this.$store.commit
|
|
||||||
})
|
|
||||||
},
|
|
||||||
submit () {
|
submit () {
|
||||||
|
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||||
|
},
|
||||||
|
submitToken () {
|
||||||
|
const { clientId } = this.oauth
|
||||||
const data = {
|
const data = {
|
||||||
oauth: this.$store.state.oauth,
|
clientId,
|
||||||
instance: this.$store.state.instance.server
|
instance: this.instance.server,
|
||||||
|
commit: this.$store.commit
|
||||||
}
|
}
|
||||||
this.clearError()
|
|
||||||
|
oauthApi.getOrCreateApp(data)
|
||||||
|
.then((app) => { oauthApi.login({ ...app, ...data }) })
|
||||||
|
},
|
||||||
|
submitPassword () {
|
||||||
|
const { clientId } = this.oauth
|
||||||
|
const data = {
|
||||||
|
clientId,
|
||||||
|
oauth: this.oauth,
|
||||||
|
instance: this.instance.server,
|
||||||
|
commit: this.$store.commit
|
||||||
|
}
|
||||||
|
this.error = false
|
||||||
|
|
||||||
oauthApi.getOrCreateApp(data).then((app) => {
|
oauthApi.getOrCreateApp(data).then((app) => {
|
||||||
oauthApi.getTokenWithCredentials(
|
oauthApi.getTokenWithCredentials(
|
||||||
{
|
{
|
||||||
app,
|
...app,
|
||||||
instance: data.instance,
|
instance: data.instance,
|
||||||
username: this.user.username,
|
username: this.user.username,
|
||||||
password: this.user.password
|
password: this.user.password
|
||||||
}
|
}
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
this.authError = result.error
|
if (result.error === 'mfa_required') {
|
||||||
this.user.password = ''
|
this.requireMFA({app: app, settings: result})
|
||||||
|
} else {
|
||||||
|
this.error = result.error
|
||||||
|
this.focusOnPasswordInput()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$store.commit('setToken', result.access_token)
|
this.login(result).then(() => {
|
||||||
this.$store.dispatch('loginUser', result.access_token)
|
|
||||||
this.$router.push({name: 'friends'})
|
this.$router.push({name: 'friends'})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
clearError () {
|
clearError () { this.error = false },
|
||||||
this.authError = false
|
focusOnPasswordInput () {
|
||||||
|
let passwordInput = this.$refs.passwordInput
|
||||||
|
passwordInput.focus()
|
||||||
|
passwordInput.setSelectionRange(0, passwordInput.value.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="login panel panel-default">
|
<div class="login panel panel-default">
|
||||||
<!-- Default panel contents -->
|
<!-- Default panel contents -->
|
||||||
<div class="panel-heading">
|
|
||||||
{{$t('login.login')}}
|
<div class="panel-heading">{{$t('login.login')}}</div>
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'>
|
<form class='login-form' @submit.prevent='submit'>
|
||||||
|
<template v-if="isPasswordAuth">
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label for='username'>{{$t('login.username')}}</label>
|
<label for='username'>{{$t('login.username')}}</label>
|
||||||
<input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')">
|
<input :disabled="loggingIn" v-model='user.username'
|
||||||
|
class='form-control' id='username'
|
||||||
|
:placeholder="$t('login.placeholder')">
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label for='password'>{{$t('login.password')}}</label>
|
<label for='password'>{{$t('login.password')}}</label>
|
||||||
<input :disabled="loggingIn" v-model='user.password' class='form-control' id='password' type='password'>
|
<input :disabled="loggingIn" v-model='user.password'
|
||||||
|
ref='passwordInput' class='form-control' id='password' type='password'>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
</template>
|
||||||
<div class='login-bottom'>
|
|
||||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
|
||||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form v-if="loginMethod == 'token'" v-on:submit.prevent='oAuthLogin' class="login-form">
|
<div class="form-group" v-if="isTokenAuth">
|
||||||
<div class="form-group">
|
|
||||||
<p>{{$t('login.description')}}</p>
|
<p>{{$t('login.description')}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<div class='login-bottom'>
|
<div class='login-bottom'>
|
||||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
<div>
|
||||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
<router-link :to="{name: 'registration'}"
|
||||||
|
v-if='registrationOpen'
|
||||||
|
class='register'>
|
||||||
|
{{$t('login.register')}}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<button :disabled="loggingIn" type='submit' class='btn btn-default'>
|
||||||
|
{{$t('login.login')}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="authError" class='form-group'>
|
<div v-if="error" class='form-group'>
|
||||||
<div class='alert error'>
|
<div class='alert error'>
|
||||||
{{authError}}
|
{{error}}
|
||||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./login_form.js" ></script>
|
<script src="./login_form.js" ></script>
|
||||||
|
@ -50,6 +56,10 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.6em;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
width: 10em;
|
width: 10em;
|
||||||
|
@ -66,9 +76,30 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.login {
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.3em 0.5em 0.6em;
|
||||||
|
line-height:24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-bottom {
|
||||||
|
display: flex;
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.35em;
|
||||||
|
padding: 0.35em;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,8 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.media-modal-view {
|
.media-modal-view {
|
||||||
|
z-index: 1001;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.modal-view-button-arrow {
|
.modal-view-button-arrow {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Timeline :title="$t('nav.mentions')" v-bind:timeline="timeline" v-bind:timeline-name="'mentions'"/>
|
<Timeline :title="$t('nav.interactions')" v-bind:timeline="timeline" v-bind:timeline-name="'mentions'"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./mentions.js"></script>
|
<script src="./mentions.js"></script>
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import mfaApi from '../../services/new_api/mfa.js'
|
||||||
|
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
code: null,
|
||||||
|
error: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
authApp: 'authFlow/app',
|
||||||
|
authSettings: 'authFlow/settings'
|
||||||
|
}),
|
||||||
|
...mapState({ instance: 'instance' })
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
|
||||||
|
...mapActions({ login: 'authFlow/login' }),
|
||||||
|
clearError () { this.error = false },
|
||||||
|
submit () {
|
||||||
|
const data = {
|
||||||
|
app: this.authApp,
|
||||||
|
instance: this.instance.server,
|
||||||
|
mfaToken: this.authSettings.mfa_token,
|
||||||
|
code: this.code
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaApi.verifyRecoveryCode(data).then((result) => {
|
||||||
|
if (result.error) {
|
||||||
|
this.error = result.error
|
||||||
|
this.code = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.login(result).then(() => {
|
||||||
|
this.$router.push({name: 'friends'})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="login panel panel-default">
|
||||||
|
<!-- Default panel contents -->
|
||||||
|
|
||||||
|
<div class="panel-heading">{{$t('login.heading.recovery')}}</div>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<form class='login-form' @submit.prevent='submit'>
|
||||||
|
<div class='form-group'>
|
||||||
|
<label for='code'>{{$t('login.recovery_code')}}</label>
|
||||||
|
<input v-model='code' class='form-control' id='code'>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='form-group'>
|
||||||
|
<div class='login-bottom'>
|
||||||
|
<div>
|
||||||
|
<a href="#" @click.prevent="requireTOTP">
|
||||||
|
{{$t('login.enter_two_factor_code')}}
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<a href="#" @click.prevent="abortMFA">
|
||||||
|
{{$t('general.cancel')}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button type='submit' class='btn btn-default'>
|
||||||
|
{{$t('general.verify')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class='form-group'>
|
||||||
|
<div class='alert error'>
|
||||||
|
{{error}}
|
||||||
|
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script src="./recovery_form.js" ></script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
import mfaApi from '../../services/new_api/mfa.js'
|
||||||
|
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
code: null,
|
||||||
|
error: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
authApp: 'authFlow/app',
|
||||||
|
authSettings: 'authFlow/settings'
|
||||||
|
}),
|
||||||
|
...mapState({ instance: 'instance' })
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
|
||||||
|
...mapActions({ login: 'authFlow/login' }),
|
||||||
|
clearError () { this.error = false },
|
||||||
|
submit () {
|
||||||
|
const data = {
|
||||||
|
app: this.authApp,
|
||||||
|
instance: this.instance.server,
|
||||||
|
mfaToken: this.authSettings.mfa_token,
|
||||||
|
code: this.code
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaApi.verifyOTPCode(data).then((result) => {
|
||||||
|
if (result.error) {
|
||||||
|
this.error = result.error
|
||||||
|
this.code = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.login(result).then(() => {
|
||||||
|
this.$router.push({name: 'friends'})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div class="login panel panel-default">
|
||||||
|
<!-- Default panel contents -->
|
||||||
|
|
||||||
|
<div class="panel-heading">
|
||||||
|
{{$t('login.heading.totp')}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<form class='login-form' @submit.prevent='submit'>
|
||||||
|
<div class='form-group'>
|
||||||
|
<label for='code'>
|
||||||
|
{{$t('login.authentication_code')}}
|
||||||
|
</label>
|
||||||
|
<input v-model='code' class='form-control' id='code'>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='form-group'>
|
||||||
|
<div class='login-bottom'>
|
||||||
|
<div>
|
||||||
|
<a href="#" @click.prevent="requireRecovery">
|
||||||
|
{{$t('login.enter_recovery_code')}}
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<a href="#" @click.prevent="abortMFA">
|
||||||
|
{{$t('general.cancel')}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button type='submit' class='btn btn-default'>
|
||||||
|
{{$t('general.verify')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class='form-group'>
|
||||||
|
<div class='alert error'>
|
||||||
|
{{error}}
|
||||||
|
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script src="./totp_form.js"></script>
|
|
@ -0,0 +1,82 @@
|
||||||
|
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||||
|
import Notifications from '../notifications/notifications.vue'
|
||||||
|
import MobilePostStatusModal from '../mobile_post_status_modal/mobile_post_status_modal.vue'
|
||||||
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
|
|
||||||
|
const MobileNav = {
|
||||||
|
components: {
|
||||||
|
SideDrawer,
|
||||||
|
Notifications,
|
||||||
|
MobilePostStatusModal
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
notificationsCloseGesture: undefined,
|
||||||
|
notificationsOpen: false
|
||||||
|
}),
|
||||||
|
created () {
|
||||||
|
this.notificationsCloseGesture = GestureService.swipeGesture(
|
||||||
|
GestureService.DIRECTION_RIGHT,
|
||||||
|
this.closeMobileNotifications,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentUser () {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
unseenNotifications () {
|
||||||
|
return unseenNotificationsFromStore(this.$store)
|
||||||
|
},
|
||||||
|
unseenNotificationsCount () {
|
||||||
|
return this.unseenNotifications.length
|
||||||
|
},
|
||||||
|
sitename () { return this.$store.state.instance.name }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleMobileSidebar () {
|
||||||
|
this.$refs.sideDrawer.toggleDrawer()
|
||||||
|
},
|
||||||
|
openMobileNotifications () {
|
||||||
|
this.notificationsOpen = true
|
||||||
|
},
|
||||||
|
closeMobileNotifications () {
|
||||||
|
if (this.notificationsOpen) {
|
||||||
|
// make sure to mark notifs seen only when the notifs were open and not
|
||||||
|
// from close-calls.
|
||||||
|
this.notificationsOpen = false
|
||||||
|
this.markNotificationsAsSeen()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notificationsTouchStart (e) {
|
||||||
|
GestureService.beginSwipe(e, this.notificationsCloseGesture)
|
||||||
|
},
|
||||||
|
notificationsTouchMove (e) {
|
||||||
|
GestureService.updateSwipe(e, this.notificationsCloseGesture)
|
||||||
|
},
|
||||||
|
scrollToTop () {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
},
|
||||||
|
logout () {
|
||||||
|
this.$router.replace('/main/public')
|
||||||
|
this.$store.dispatch('logout')
|
||||||
|
},
|
||||||
|
markNotificationsAsSeen () {
|
||||||
|
this.$refs.notifications.markAsSeen()
|
||||||
|
},
|
||||||
|
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||||
|
if (this.$store.state.config.autoLoad && scrollTop + clientHeight >= scrollHeight) {
|
||||||
|
this.$refs.notifications.fetchOlderNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route () {
|
||||||
|
// handles closing notificaitons when you press any router-link on the
|
||||||
|
// notifications.
|
||||||
|
this.closeMobileNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MobileNav
|
|
@ -0,0 +1,144 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nav class='nav-bar container' id="nav">
|
||||||
|
<div class='mobile-inner-nav' @click="scrollToTop()">
|
||||||
|
<div class='item'>
|
||||||
|
<a href="#" class="mobile-nav-button" @click.stop.prevent="toggleMobileSidebar()">
|
||||||
|
<i class="button-icon icon-menu"></i>
|
||||||
|
</a>
|
||||||
|
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
|
||||||
|
</div>
|
||||||
|
<div class='item right'>
|
||||||
|
<a class="mobile-nav-button" v-if="currentUser" href="#" @click.stop.prevent="openMobileNotifications()">
|
||||||
|
<i class="button-icon icon-bell-alt"></i>
|
||||||
|
<div class="alert-dot" v-if="unseenNotificationsCount"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div v-if="currentUser"
|
||||||
|
class="mobile-notifications-drawer"
|
||||||
|
:class="{ 'closed': !notificationsOpen }"
|
||||||
|
@touchstart.stop="notificationsTouchStart"
|
||||||
|
@touchmove.stop="notificationsTouchMove"
|
||||||
|
>
|
||||||
|
<div class="mobile-notifications-header">
|
||||||
|
<span class="title">{{$t('notifications.notifications')}}</span>
|
||||||
|
<a class="mobile-nav-button" @click.stop.prevent="closeMobileNotifications()">
|
||||||
|
<i class="button-icon icon-cancel"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-notifications" @scroll="onScroll">
|
||||||
|
<Notifications ref="notifications" :noHeading="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SideDrawer ref="sideDrawer" :logout="logout"/>
|
||||||
|
<MobilePostStatusModal />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./mobile_nav.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.mobile-inner-nav {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-nav-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 50px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-dot {
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 4px);
|
||||||
|
top: calc(50% - 4px);
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-top: -6px;
|
||||||
|
background-color: $fallback--cRed;
|
||||||
|
background-color: var(--badgeNotification, $fallback--cRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-notifications-drawer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||||
|
box-shadow: var(--panelShadow);
|
||||||
|
transition-property: transform;
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
transform: translateX(0);
|
||||||
|
z-index: 1001;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-notifications-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
color: var(--topBarText);
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--topBar, $fallback--fg);
|
||||||
|
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
||||||
|
box-shadow: var(--topBarShadow);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-left: 0.6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-notifications {
|
||||||
|
margin-top: 50px;
|
||||||
|
width: 100vw;
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
background-color: $fallback--bg;
|
||||||
|
background-color: var(--bg, $fallback--bg);
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
.panel {
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.panel:after {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.panel .panel-heading {
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
import { throttle } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
|
|
||||||
const MobilePostStatusModal = {
|
const MobilePostStatusModal = {
|
||||||
components: {
|
components: {
|
||||||
|
@ -16,11 +16,15 @@ const MobilePostStatusModal = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
window.addEventListener('scroll', this.handleScroll)
|
if (this.autohideFloatingPostButton) {
|
||||||
|
this.activateFloatingPostButtonAutohide()
|
||||||
|
}
|
||||||
window.addEventListener('resize', this.handleOSK)
|
window.addEventListener('resize', this.handleOSK)
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
window.removeEventListener('scroll', this.handleScroll)
|
if (this.autohideFloatingPostButton) {
|
||||||
|
this.deactivateFloatingPostButtonAutohide()
|
||||||
|
}
|
||||||
window.removeEventListener('resize', this.handleOSK)
|
window.removeEventListener('resize', this.handleOSK)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -28,10 +32,30 @@ const MobilePostStatusModal = {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
isHidden () {
|
isHidden () {
|
||||||
return this.hidden || this.inputActive
|
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||||
|
},
|
||||||
|
autohideFloatingPostButton () {
|
||||||
|
return !!this.$store.state.config.autohideFloatingPostButton
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
autohideFloatingPostButton: function (isEnabled) {
|
||||||
|
if (isEnabled) {
|
||||||
|
this.activateFloatingPostButtonAutohide()
|
||||||
|
} else {
|
||||||
|
this.deactivateFloatingPostButtonAutohide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
activateFloatingPostButtonAutohide () {
|
||||||
|
window.addEventListener('scroll', this.handleScrollStart)
|
||||||
|
window.addEventListener('scroll', this.handleScrollEnd)
|
||||||
|
},
|
||||||
|
deactivateFloatingPostButtonAutohide () {
|
||||||
|
window.removeEventListener('scroll', this.handleScrollStart)
|
||||||
|
window.removeEventListener('scroll', this.handleScrollEnd)
|
||||||
|
},
|
||||||
openPostForm () {
|
openPostForm () {
|
||||||
this.postFormOpen = true
|
this.postFormOpen = true
|
||||||
this.hidden = true
|
this.hidden = true
|
||||||
|
@ -65,26 +89,19 @@ const MobilePostStatusModal = {
|
||||||
this.inputActive = false
|
this.inputActive = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleScroll: throttle(function () {
|
handleScrollStart: debounce(function () {
|
||||||
const scrollAmount = window.scrollY - this.oldScrollPos
|
if (window.scrollY > this.oldScrollPos) {
|
||||||
const scrollingDown = scrollAmount > 0
|
this.hidden = true
|
||||||
|
} else {
|
||||||
if (scrollingDown !== this.scrollingDown) {
|
|
||||||
this.amountScrolled = 0
|
|
||||||
this.scrollingDown = scrollingDown
|
|
||||||
if (!scrollingDown) {
|
|
||||||
this.hidden = false
|
this.hidden = false
|
||||||
}
|
}
|
||||||
} else if (scrollingDown) {
|
|
||||||
this.amountScrolled += scrollAmount
|
|
||||||
if (this.amountScrolled > 100 && !this.hidden) {
|
|
||||||
this.hidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.oldScrollPos = window.scrollY
|
this.oldScrollPos = window.scrollY
|
||||||
this.scrollingDown = scrollingDown
|
}, 100, {leading: true, trailing: false}),
|
||||||
}, 100)
|
|
||||||
|
handleScrollEnd: debounce(function () {
|
||||||
|
this.hidden = false
|
||||||
|
this.oldScrollPos = window.scrollY
|
||||||
|
}, 100, {leading: false, trailing: true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
>
|
>
|
||||||
<div class="post-form-modal-panel panel" @click.stop="">
|
<div class="post-form-modal-panel panel" @click.stop="">
|
||||||
<div class="panel-heading">{{$t('post_status.new_status')}}</div>
|
<div class="panel-heading">{{$t('post_status.new_status')}}</div>
|
||||||
<PostStatusForm class="panel-body" @posted="closePostForm"/>
|
<PostStatusForm class="panel-body" @posted="closePostForm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||||
|
import Popper from 'vue-popperjs/src/component/popper.js.vue'
|
||||||
|
|
||||||
|
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||||
|
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||||
|
const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
|
||||||
|
const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'
|
||||||
|
const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
|
||||||
|
const SANDBOX = 'mrf_tag:sandbox'
|
||||||
|
const QUARANTINE = 'mrf_tag:quarantine'
|
||||||
|
|
||||||
|
const ModerationTools = {
|
||||||
|
props: [
|
||||||
|
'user'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showDropDown: false,
|
||||||
|
tags: {
|
||||||
|
FORCE_NSFW,
|
||||||
|
STRIP_MEDIA,
|
||||||
|
FORCE_UNLISTED,
|
||||||
|
DISABLE_REMOTE_SUBSCRIPTION,
|
||||||
|
DISABLE_ANY_SUBSCRIPTION,
|
||||||
|
SANDBOX,
|
||||||
|
QUARANTINE
|
||||||
|
},
|
||||||
|
showDeleteUserDialog: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
DialogModal,
|
||||||
|
Popper
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tagsSet () {
|
||||||
|
return new Set(this.user.tags)
|
||||||
|
},
|
||||||
|
hasTagPolicy () {
|
||||||
|
return this.$store.state.instance.tagPolicyAvailable
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleMenu () {
|
||||||
|
this.showDropDown = !this.showDropDown
|
||||||
|
},
|
||||||
|
hasTag (tagName) {
|
||||||
|
return this.tagsSet.has(tagName)
|
||||||
|
},
|
||||||
|
toggleTag (tag) {
|
||||||
|
const store = this.$store
|
||||||
|
if (this.tagsSet.has(tag)) {
|
||||||
|
store.state.api.backendInteractor.untagUser(this.user, tag).then(response => {
|
||||||
|
if (!response.ok) { return }
|
||||||
|
store.commit('untagUser', {user: this.user, tag})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
store.state.api.backendInteractor.tagUser(this.user, tag).then(response => {
|
||||||
|
if (!response.ok) { return }
|
||||||
|
store.commit('tagUser', {user: this.user, tag})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleRight (right) {
|
||||||
|
const store = this.$store
|
||||||
|
if (this.user.rights[right]) {
|
||||||
|
store.state.api.backendInteractor.deleteRight(this.user, right).then(response => {
|
||||||
|
if (!response.ok) { return }
|
||||||
|
store.commit('updateRight', {user: this.user, right: right, value: false})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
store.state.api.backendInteractor.addRight(this.user, right).then(response => {
|
||||||
|
if (!response.ok) { return }
|
||||||
|
store.commit('updateRight', {user: this.user, right: right, value: true})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleActivationStatus () {
|
||||||
|
const store = this.$store
|
||||||
|
const status = !!this.user.deactivated
|
||||||
|
store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => {
|
||||||
|
if (!response.ok) { return }
|
||||||
|
store.commit('updateActivationStatus', {user: this.user, status: status})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteUserDialog (show) {
|
||||||
|
this.showDeleteUserDialog = show
|
||||||
|
},
|
||||||
|
deleteUser () {
|
||||||
|
const store = this.$store
|
||||||
|
const user = this.user
|
||||||
|
const {id, name} = user
|
||||||
|
store.state.api.backendInteractor.deleteUser(user)
|
||||||
|
.then(e => {
|
||||||
|
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
|
||||||
|
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
|
||||||
|
const isTargetUser = this.$route.params.name === name || this.$route.params.id === id
|
||||||
|
if (isProfile && isTargetUser) {
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModerationTools
|
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div class='block' style='position: relative'>
|
||||||
|
<Popper
|
||||||
|
trigger="click"
|
||||||
|
@hide='showDropDown = false'
|
||||||
|
append-to-body
|
||||||
|
:options="{
|
||||||
|
placement: 'bottom-end',
|
||||||
|
modifiers: {
|
||||||
|
arrow: { enabled: true },
|
||||||
|
offset: { offset: '0, 5px' },
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="popper-wrapper">
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<span v-if='user.is_local'>
|
||||||
|
<button class="dropdown-item" @click='toggleRight("admin")'>
|
||||||
|
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" @click='toggleRight("moderator")'>
|
||||||
|
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
|
||||||
|
</button>
|
||||||
|
<div role="separator" class="dropdown-divider"></div>
|
||||||
|
</span>
|
||||||
|
<button class="dropdown-item" @click='toggleActivationStatus()'>
|
||||||
|
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" @click='deleteUserDialog(true)'>
|
||||||
|
{{ $t('user_card.admin_menu.delete_account') }}
|
||||||
|
</button>
|
||||||
|
<div role="separator" class="dropdown-divider" v-if='hasTagPolicy'></div>
|
||||||
|
<span v-if='hasTagPolicy'>
|
||||||
|
<button class="dropdown-item" @click='toggleTag(tags.FORCE_NSFW)'>
|
||||||
|
{{ $t('user_card.admin_menu.force_nsfw') }}
|
||||||
|
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"></span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" @click='toggleTag(tags.STRIP_MEDIA)'>
|
||||||
|
{{ $t('user_card.admin_menu.strip_media') }}
|
||||||
|
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"></span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" @click='toggleTag(tags.FORCE_UNLISTED)'>
|
||||||
|
{{ $t('user_card.admin_menu.force_unlisted') }}
|
||||||
|
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"></span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" @click='toggleTag(tags.SANDBOX)'>
|
||||||
|
{{ $t('user_card.admin_menu.sandbox') }}
|
||||||
|
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"></span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)'>
|
||||||
|
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
|
||||||
|
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"></span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)'>
|
||||||
|
{{ $t('user_card.admin_menu.disable_any_subscription') }}
|
||||||
|
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"></span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.QUARANTINE)'>
|
||||||
|
{{ $t('user_card.admin_menu.quarantine') }}
|
||||||
|
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button slot="reference" v-bind:class="{ pressed: showDropDown }" @click='toggleMenu'>
|
||||||
|
{{ $t('user_card.admin_menu.moderation') }}
|
||||||
|
</button>
|
||||||
|
</Popper>
|
||||||
|
<portal to="modal">
|
||||||
|
<DialogModal v-if="showDeleteUserDialog" :onCancel='deleteUserDialog.bind(this, false)'>
|
||||||
|
<template slot="header">{{ $t('user_card.admin_menu.delete_user') }}</template>
|
||||||
|
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
||||||
|
<template slot="footer">
|
||||||
|
<button class="btn btn-default" @click='deleteUserDialog(false)'>
|
||||||
|
{{ $t('general.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default danger" @click='deleteUser()'>
|
||||||
|
{{ $t('user_card.admin_menu.delete_user') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
|
</portal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./moderation_tools.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
@import '../popper/popper.scss';
|
||||||
|
|
||||||
|
.menu-checkbox {
|
||||||
|
float: right;
|
||||||
|
min-width: 22px;
|
||||||
|
max-width: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
max-height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--input, $fallback--fg);
|
||||||
|
box-shadow: 0px 0px 2px black inset;
|
||||||
|
box-shadow: var(--inputShadow);
|
||||||
|
|
||||||
|
&.menu-checkbox-checked::after {
|
||||||
|
content: '✔';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -8,8 +8,8 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if='currentUser'>
|
<li v-if='currentUser'>
|
||||||
<router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
|
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
|
||||||
{{ $t("nav.mentions") }}
|
{{ $t("nav.interactions") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if='currentUser'>
|
<li v-if='currentUser'>
|
||||||
|
|
|
@ -21,16 +21,28 @@ const Notification = {
|
||||||
},
|
},
|
||||||
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)
|
||||||
|
},
|
||||||
|
getUser (notification) {
|
||||||
|
return this.$store.state.users.usersObject[notification.from_profile.id]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userClass () {
|
userClass () {
|
||||||
return highlightClass(this.notification.action.user)
|
return highlightClass(this.notification.from_profile)
|
||||||
},
|
},
|
||||||
userStyle () {
|
userStyle () {
|
||||||
const highlight = this.$store.state.config.highlight
|
const highlight = this.$store.state.config.highlight
|
||||||
const user = this.notification.action.user
|
const user = this.notification.from_profile
|
||||||
return highlightStyle(highlight[user.screen_name])
|
return highlightStyle(highlight[user.screen_name])
|
||||||
|
},
|
||||||
|
userInStore () {
|
||||||
|
return this.$store.getters.findUser(this.notification.from_profile.id)
|
||||||
|
},
|
||||||
|
user () {
|
||||||
|
if (this.userInStore) {
|
||||||
|
return this.userInStore
|
||||||
|
}
|
||||||
|
return this.notification.from_profile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
<status
|
||||||
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
|
v-if="notification.type === 'mention'"
|
||||||
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
:compact="true"
|
||||||
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
|
:statusoid="notification.status"
|
||||||
|
>
|
||||||
|
</status>
|
||||||
|
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else>
|
||||||
|
<a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||||
|
<UserAvatar :compact="true" :betterShadow="betterShadow" :user="notification.from_profile"/>
|
||||||
</a>
|
</a>
|
||||||
<div class='notification-right'>
|
<div class='notification-right'>
|
||||||
<UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
|
<UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />
|
||||||
<span class="notification-details">
|
<span class="notification-details">
|
||||||
<div class="name-and-action">
|
<div class="name-and-action">
|
||||||
<span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
|
<span class="username" v-if="!!notification.from_profile.name_html" :title="'@'+notification.from_profile.screen_name" v-html="notification.from_profile.name_html"></span>
|
||||||
<span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
|
<span class="username" v-else :title="'@'+notification.from_profile.screen_name">{{ notification.from_profile.name }}</span>
|
||||||
<span v-if="notification.type === 'like'">
|
<span v-if="notification.type === 'like'">
|
||||||
<i class="fa icon-star lit"></i>
|
<i class="fa icon-star lit"></i>
|
||||||
<small>{{$t('notifications.favorited_you')}}</small>
|
<small>{{$t('notifications.favorited_you')}}</small>
|
||||||
|
@ -23,19 +28,24 @@
|
||||||
<small>{{$t('notifications.followed_you')}}</small>
|
<small>{{$t('notifications.followed_you')}}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeago">
|
<div class="timeago" v-if="notification.type === 'follow'">
|
||||||
|
<span class="faint">
|
||||||
|
<timeago :since="notification.created_at" :auto-update="240"></timeago>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeago" v-else>
|
||||||
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
|
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
|
||||||
<timeago :since="notification.action.created_at" :auto-update="240"></timeago>
|
<timeago :since="notification.created_at" :auto-update="240"></timeago>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<div class="follow-text" v-if="notification.type === 'follow'">
|
<div class="follow-text" v-if="notification.type === 'follow'">
|
||||||
<router-link :to="userProfileLink(notification.action.user)">
|
<router-link :to="userProfileLink(notification.from_profile)">
|
||||||
@{{notification.action.user.screen_name}}
|
@{{notification.from_profile.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
|
<status class="faint" :compact="true" :statusoid="notification.action" :noHeading="true"></status>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,12 +7,14 @@ import {
|
||||||
} from '../../services/notification_utils/notification_utils.js'
|
} from '../../services/notification_utils/notification_utils.js'
|
||||||
|
|
||||||
const Notifications = {
|
const Notifications = {
|
||||||
created () {
|
props: {
|
||||||
const store = this.$store
|
// Disables display of panel header
|
||||||
const credentials = store.state.users.currentUser.credentials
|
noHeading: Boolean,
|
||||||
|
// Disables panel styles, unread mark, potentially other notification-related actions
|
||||||
const fetcherId = notificationsFetcher.startFetching({ store, credentials })
|
// meant for "Interactions" timeline
|
||||||
this.$store.commit('setNotificationFetcher', { fetcherId })
|
minimalMode: Boolean,
|
||||||
|
// Custom filter mode, an array of strings, possible values 'mention', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline
|
||||||
|
filterMode: Array
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -20,6 +22,9 @@ const Notifications = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
mainClass () {
|
||||||
|
return this.minimalMode ? '' : 'panel panel-default'
|
||||||
|
},
|
||||||
notifications () {
|
notifications () {
|
||||||
return notificationsFromStore(this.$store)
|
return notificationsFromStore(this.$store)
|
||||||
},
|
},
|
||||||
|
@ -30,7 +35,7 @@ const Notifications = {
|
||||||
return unseenNotificationsFromStore(this.$store)
|
return unseenNotificationsFromStore(this.$store)
|
||||||
},
|
},
|
||||||
visibleNotifications () {
|
visibleNotifications () {
|
||||||
return visibleNotificationsFromStore(this.$store)
|
return visibleNotificationsFromStore(this.$store, this.filterMode)
|
||||||
},
|
},
|
||||||
unseenCount () {
|
unseenCount () {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
|
@ -53,9 +58,13 @@ const Notifications = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
markAsSeen () {
|
markAsSeen () {
|
||||||
this.$store.dispatch('markNotificationsAsSeen', this.visibleNotifications)
|
this.$store.dispatch('markNotificationsAsSeen')
|
||||||
},
|
},
|
||||||
fetchOlderNotifications () {
|
fetchOlderNotifications () {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
const credentials = store.state.users.currentUser.credentials
|
const credentials = store.state.users.currentUser.credentials
|
||||||
store.commit('setNotificationsLoading', { value: true })
|
store.commit('setNotificationsLoading', { value: true })
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.notifications {
|
.notifications {
|
||||||
|
&:not(.minimal) {
|
||||||
// a bit of a hack to allow scrolling below notifications
|
// a bit of a hack to allow scrolling below notifications
|
||||||
padding-bottom: 15em;
|
padding-bottom: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
.loadmore-error {
|
.loadmore-error {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="notifications">
|
<div :class="{ minimal: minimalMode }" class="notifications">
|
||||||
<div class="panel panel-default">
|
<div :class="mainClass">
|
||||||
<div class="panel-heading">
|
<div v-if="!noHeading" class="panel-heading">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{$t('notifications.notifications')}}
|
{{$t('notifications.notifications')}}
|
||||||
<span class="badge badge-notification unseen-count" v-if="unseenCount">{{unseenCount}}</span>
|
<span class="badge badge-notification unseen-count" v-if="unseenCount">{{unseenCount}}</span>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
|
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
|
<div v-for="notification in visibleNotifications" :key="notification.id" class="notification" :class='{"unseen": !minimalMode && !notification.seen}'>
|
||||||
<div class="notification-overlay"></div>
|
<div class="notification-overlay"></div>
|
||||||
<notification :notification="notification"></notification>
|
<notification :notification="notification"></notification>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,9 @@
|
||||||
{{$t('notifications.no_more_notifications')}}
|
{{$t('notifications.no_more_notifications')}}
|
||||||
</div>
|
</div>
|
||||||
<a v-else-if="!loading" href="#" v-on:click.prevent="fetchOlderNotifications()">
|
<a v-else-if="!loading" href="#" v-on:click.prevent="fetchOlderNotifications()">
|
||||||
<div class="new-status-notification text-center panel-footer">{{$t('notifications.load_older')}}</div>
|
<div class="new-status-notification text-center panel-footer">
|
||||||
|
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older')}}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div v-else class="new-status-notification text-center panel-footer">
|
<div v-else class="new-status-notification text-center panel-footer">
|
||||||
<i class="icon-spin3 animate-spin"/>
|
<i class="icon-spin3 animate-spin"/>
|
||||||
|
|
|
@ -4,14 +4,16 @@ const oac = {
|
||||||
props: ['code'],
|
props: ['code'],
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.code) {
|
if (this.code) {
|
||||||
|
const { clientId } = this.$store.state.oauth
|
||||||
|
|
||||||
oauth.getToken({
|
oauth.getToken({
|
||||||
app: this.$store.state.oauth,
|
clientId,
|
||||||
instance: this.$store.state.instance.server,
|
instance: this.$store.state.instance.server,
|
||||||
code: this.code
|
code: this.code
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
this.$store.commit('setToken', result.access_token)
|
this.$store.commit('setToken', result.access_token)
|
||||||
this.$store.dispatch('loginUser', result.access_token)
|
this.$store.dispatch('loginUser', result.access_token)
|
||||||
this.$router.push({name: 'friends'})
|
this.$router.push({ name: 'friends' })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.popper-wrapper {
|
||||||
|
z-index: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper .popper__arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
position: absolute;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="top"] {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="top"] .popper__arrow {
|
||||||
|
border-width: 5px 5px 0 5px;
|
||||||
|
border-color: $fallback--bg transparent transparent transparent;
|
||||||
|
border-color: var(--bg, $fallback--bg) transparent transparent transparent;
|
||||||
|
bottom: -5px;
|
||||||
|
left: calc(50% - 5px);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="bottom"] {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="bottom"] .popper__arrow {
|
||||||
|
border-width: 0 5px 5px 5px;
|
||||||
|
border-color: transparent transparent $fallback--bg transparent;
|
||||||
|
border-color: transparent transparent var(--bg, $fallback--bg) transparent;
|
||||||
|
top: -5px;
|
||||||
|
left: calc(50% - 5px);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="right"] {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="right"] .popper__arrow {
|
||||||
|
border-width: 5px 5px 5px 0;
|
||||||
|
border-color: transparent $fallback--bg transparent transparent;
|
||||||
|
border-color: transparent var(--bg, $fallback--bg) transparent transparent;
|
||||||
|
left: -5px;
|
||||||
|
top: calc(50% - 5px);
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="left"] {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper-wrapper[x-placement^="left"] .popper__arrow {
|
||||||
|
border-width: 5px 0 5px 5px;
|
||||||
|
border-color: transparent transparent transparent $fallback--bg;
|
||||||
|
border-color: transparent transparent transparent var(--bg, $fallback--bg);
|
||||||
|
right: -5px;
|
||||||
|
top: calc(50% - 5px);
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
padding: .5rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
list-style: none;
|
||||||
|
max-width: 100vw;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||||
|
box-shadow: var(--panelShadow);
|
||||||
|
border: none;
|
||||||
|
border-radius: $fallback--btnRadius;
|
||||||
|
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||||
|
background-color: $fallback--bg;
|
||||||
|
background-color: var(--bg, $fallback--bg);
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
height: 0;
|
||||||
|
margin: .5rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: 1px solid $fallback--border;
|
||||||
|
border-top: 1px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
line-height: 21px;
|
||||||
|
margin-right: 5px;
|
||||||
|
overflow: auto;
|
||||||
|
display: block;
|
||||||
|
padding: .25rem 1.0rem .25rem 1.5rem;
|
||||||
|
clear: both;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: inherit;
|
||||||
|
white-space: normal;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
// TODO: improve the look on breeze themes
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--btn, $fallback--fg);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
||||||
import MediaUpload from '../media_upload/media_upload.vue'
|
import MediaUpload from '../media_upload/media_upload.vue'
|
||||||
|
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||||
import EmojiInput from '../emoji-input/emoji-input.vue'
|
import EmojiInput from '../emoji-input/emoji-input.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import Completion from '../../services/completion/completion.js'
|
import Completion from '../../services/completion/completion.js'
|
||||||
|
@ -30,6 +31,7 @@ const PostStatusForm = {
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
MediaUpload,
|
MediaUpload,
|
||||||
|
ScopeSelector,
|
||||||
EmojiInput
|
EmojiInput
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -80,14 +82,6 @@ const PostStatusForm = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
vis () {
|
|
||||||
return {
|
|
||||||
public: { selected: this.newStatus.visibility === 'public' },
|
|
||||||
unlisted: { selected: this.newStatus.visibility === 'unlisted' },
|
|
||||||
private: { selected: this.newStatus.visibility === 'private' },
|
|
||||||
direct: { selected: this.newStatus.visibility === 'direct' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
candidates () {
|
candidates () {
|
||||||
const firstchar = this.textAtCaret.charAt(0)
|
const firstchar = this.textAtCaret.charAt(0)
|
||||||
if (firstchar === '@') {
|
if (firstchar === '@') {
|
||||||
|
@ -135,6 +129,15 @@ const PostStatusForm = {
|
||||||
users () {
|
users () {
|
||||||
return this.$store.state.users.users
|
return this.$store.state.users.users
|
||||||
},
|
},
|
||||||
|
userDefaultScope () {
|
||||||
|
return this.$store.state.users.currentUser.default_scope
|
||||||
|
},
|
||||||
|
showAllScopes () {
|
||||||
|
const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined'
|
||||||
|
? this.$store.state.instance.minimalScopesMode
|
||||||
|
: this.$store.state.config.minimalScopesMode
|
||||||
|
return !minimalScopesMode
|
||||||
|
},
|
||||||
emoji () {
|
emoji () {
|
||||||
return this.$store.state.instance.emoji || []
|
return this.$store.state.instance.emoji || []
|
||||||
},
|
},
|
||||||
|
@ -159,8 +162,8 @@ const PostStatusForm = {
|
||||||
isOverLengthLimit () {
|
isOverLengthLimit () {
|
||||||
return this.hasStatusLengthLimit && (this.charactersLeft < 0)
|
return this.hasStatusLengthLimit && (this.charactersLeft < 0)
|
||||||
},
|
},
|
||||||
scopeOptionsEnabled () {
|
minimalScopesMode () {
|
||||||
return this.$store.state.instance.scopeOptionsEnabled
|
return this.$store.state.instance.minimalScopesMode
|
||||||
},
|
},
|
||||||
alwaysShowSubject () {
|
alwaysShowSubject () {
|
||||||
if (typeof this.$store.state.config.alwaysShowSubjectInput !== 'undefined') {
|
if (typeof this.$store.state.config.alwaysShowSubjectInput !== 'undefined') {
|
||||||
|
@ -168,7 +171,7 @@ const PostStatusForm = {
|
||||||
} else if (typeof this.$store.state.instance.alwaysShowSubjectInput !== 'undefined') {
|
} else if (typeof this.$store.state.instance.alwaysShowSubjectInput !== 'undefined') {
|
||||||
return this.$store.state.instance.alwaysShowSubjectInput
|
return this.$store.state.instance.alwaysShowSubjectInput
|
||||||
} else {
|
} else {
|
||||||
return this.$store.state.instance.scopeOptionsEnabled
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formattingOptionsEnabled () {
|
formattingOptionsEnabled () {
|
||||||
|
@ -176,6 +179,12 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
postFormats () {
|
postFormats () {
|
||||||
return this.$store.state.instance.postFormats || []
|
return this.$store.state.instance.postFormats || []
|
||||||
|
},
|
||||||
|
safeDMEnabled () {
|
||||||
|
return this.$store.state.instance.safeDM
|
||||||
|
},
|
||||||
|
hideScopeNotice () {
|
||||||
|
return this.$store.state.config.hideScopeNotice
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -332,6 +341,9 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
changeVis (visibility) {
|
changeVis (visibility) {
|
||||||
this.newStatus.visibility = visibility
|
this.newStatus.visibility = visibility
|
||||||
|
},
|
||||||
|
dismissScopeNotice () {
|
||||||
|
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,34 @@
|
||||||
<form @submit.prevent="postStatus(newStatus)">
|
<form @submit.prevent="postStatus(newStatus)">
|
||||||
<div class="form-group" >
|
<div class="form-group" >
|
||||||
<i18n
|
<i18n
|
||||||
v-if="!this.$store.state.users.currentUser.locked && this.newStatus.visibility == 'private'"
|
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
|
||||||
path="post_status.account_not_locked_warning"
|
path="post_status.account_not_locked_warning"
|
||||||
tag="p"
|
tag="p"
|
||||||
class="visibility-notice">
|
class="visibility-notice">
|
||||||
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
|
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
|
||||||
</i18n>
|
</i18n>
|
||||||
<p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p>
|
<p v-if="!hideScopeNotice && newStatus.visibility === 'public'" class="visibility-notice notice-dismissible">
|
||||||
|
<span>{{ $t('post_status.scope_notice.public') }}</span>
|
||||||
|
<a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
|
||||||
|
<i class='icon-cancel'></i>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="!hideScopeNotice && newStatus.visibility === 'unlisted'" class="visibility-notice notice-dismissible">
|
||||||
|
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
|
||||||
|
<a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
|
||||||
|
<i class='icon-cancel'></i>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="!hideScopeNotice && newStatus.visibility === 'private' && $store.state.users.currentUser.locked" class="visibility-notice notice-dismissible">
|
||||||
|
<span>{{ $t('post_status.scope_notice.private') }}</span>
|
||||||
|
<a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
|
||||||
|
<i class='icon-cancel'></i>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="newStatus.visibility === 'direct'" class="visibility-notice">
|
||||||
|
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
||||||
|
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
||||||
|
</p>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
v-if="newStatus.spoilerText || alwaysShowSubject"
|
v-if="newStatus.spoilerText || alwaysShowSubject"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -37,7 +58,7 @@
|
||||||
>
|
>
|
||||||
</textarea>
|
</textarea>
|
||||||
<div class="visibility-tray">
|
<div class="visibility-tray">
|
||||||
<span class="text-format" v-if="formattingOptionsEnabled">
|
<div class="text-format" v-if="formattingOptionsEnabled">
|
||||||
<label for="post-content-type" class="select">
|
<label for="post-content-type" class="select">
|
||||||
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
|
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
|
||||||
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
|
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
|
||||||
|
@ -46,14 +67,14 @@
|
||||||
</select>
|
</select>
|
||||||
<i class="icon-down-open"></i>
|
<i class="icon-down-open"></i>
|
||||||
</label>
|
</label>
|
||||||
</span>
|
|
||||||
|
|
||||||
<div v-if="scopeOptionsEnabled">
|
|
||||||
<i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct" :title="$t('post_status.scope.direct')"></i>
|
|
||||||
<i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private" :title="$t('post_status.scope.private')"></i>
|
|
||||||
<i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted" :title="$t('post_status.scope.unlisted')"></i>
|
|
||||||
<i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public" :title="$t('post_status.scope.public')"></i>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<scope-selector
|
||||||
|
:showAll="showAllScopes"
|
||||||
|
:userDefault="userDefaultScope"
|
||||||
|
:originalScope="copyMessageScope"
|
||||||
|
:initialScope="newStatus.visibility"
|
||||||
|
:onScopeChange="changeVis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="autocomplete-panel" v-if="candidates">
|
<div class="autocomplete-panel" v-if="candidates">
|
||||||
|
@ -131,10 +152,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-status-form, .login {
|
.post-status-form {
|
||||||
.form-bottom {
|
.form-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
@ -229,7 +251,7 @@
|
||||||
.form-group {
|
.form-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0.3em 0.5em 0.6em;
|
padding: 0.25em 0.5em 0.5em;
|
||||||
line-height:24px;
|
line-height:24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<button :disabled="progress || disabled" @click="onClick">
|
||||||
|
<template v-if="progress">
|
||||||
|
<slot name="progress" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
disabled: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
click: { // click event handler. Must return a promise
|
||||||
|
type: Function,
|
||||||
|
default: () => Promise.resolve()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
progress: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.progress = true
|
||||||
|
this.click().then(() => { this.progress = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
|
||||||
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
|
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' })
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
||||||
|
|
|
@ -7,7 +7,7 @@ const PublicTimeline = {
|
||||||
timeline () { return this.$store.state.statuses.timelines.public }
|
timeline () { return this.$store.state.statuses.timelines.public }
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('startFetching', { timeline: 'public' })
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.dispatch('stopFetching', 'public')
|
this.$store.dispatch('stopFetching', 'public')
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
const ScopeSelector = {
|
||||||
|
props: [
|
||||||
|
'showAll',
|
||||||
|
'userDefault',
|
||||||
|
'originalScope',
|
||||||
|
'initialScope',
|
||||||
|
'onScopeChange'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
currentScope: this.initialScope
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showNothing () {
|
||||||
|
return !this.showPublic && !this.showUnlisted && !this.showPrivate && !this.showDirect
|
||||||
|
},
|
||||||
|
showPublic () {
|
||||||
|
return this.originalScope !== 'direct' && this.shouldShow('public')
|
||||||
|
},
|
||||||
|
showUnlisted () {
|
||||||
|
return this.originalScope !== 'direct' && this.shouldShow('unlisted')
|
||||||
|
},
|
||||||
|
showPrivate () {
|
||||||
|
return this.originalScope !== 'direct' && this.shouldShow('private')
|
||||||
|
},
|
||||||
|
showDirect () {
|
||||||
|
return this.shouldShow('direct')
|
||||||
|
},
|
||||||
|
css () {
|
||||||
|
return {
|
||||||
|
public: {selected: this.currentScope === 'public'},
|
||||||
|
unlisted: {selected: this.currentScope === 'unlisted'},
|
||||||
|
private: {selected: this.currentScope === 'private'},
|
||||||
|
direct: {selected: this.currentScope === 'direct'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
shouldShow (scope) {
|
||||||
|
return this.showAll ||
|
||||||
|
this.currentScope === scope ||
|
||||||
|
this.originalScope === scope ||
|
||||||
|
this.userDefault === scope ||
|
||||||
|
scope === 'direct'
|
||||||
|
},
|
||||||
|
changeVis (scope) {
|
||||||
|
this.currentScope = scope
|
||||||
|
this.onScopeChange && this.onScopeChange(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScopeSelector
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="!showNothing" class="scope-selector">
|
||||||
|
<i class="icon-mail-alt"
|
||||||
|
:class="css.direct"
|
||||||
|
:title="$t('post_status.scope.direct')"
|
||||||
|
v-if="showDirect"
|
||||||
|
@click="changeVis('direct')">
|
||||||
|
</i>
|
||||||
|
<i class="icon-lock"
|
||||||
|
:class="css.private"
|
||||||
|
:title="$t('post_status.scope.private')"
|
||||||
|
v-if="showPrivate"
|
||||||
|
v-on:click="changeVis('private')">
|
||||||
|
</i>
|
||||||
|
<i class="icon-lock-open-alt"
|
||||||
|
:class="css.unlisted"
|
||||||
|
:title="$t('post_status.scope.unlisted')"
|
||||||
|
v-if="showUnlisted"
|
||||||
|
@click="changeVis('unlisted')">
|
||||||
|
</i>
|
||||||
|
<i class="icon-globe"
|
||||||
|
:class="css.public"
|
||||||
|
:title="$t('post_status.scope.public')"
|
||||||
|
v-if="showPublic"
|
||||||
|
@click="changeVis('public')">
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./scope_selector.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.scope-selector {
|
||||||
|
i {
|
||||||
|
font-size: 1.2em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
color: $fallback--lightText;
|
||||||
|
color: var(--lightText, $fallback--lightText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,66 @@
|
||||||
|
import List from '../list/list.vue'
|
||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const SelectableList = {
|
||||||
|
components: {
|
||||||
|
List,
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
getKey: {
|
||||||
|
type: Function,
|
||||||
|
default: item => item.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
selected: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
allKeys () {
|
||||||
|
return this.items.map(this.getKey)
|
||||||
|
},
|
||||||
|
filteredSelected () {
|
||||||
|
return this.allKeys.filter(key => this.selected.indexOf(key) !== -1)
|
||||||
|
},
|
||||||
|
allSelected () {
|
||||||
|
return this.filteredSelected.length === this.items.length
|
||||||
|
},
|
||||||
|
noneSelected () {
|
||||||
|
return this.filteredSelected.length === 0
|
||||||
|
},
|
||||||
|
someSelected () {
|
||||||
|
return !this.allSelected && !this.noneSelected
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isSelected (item) {
|
||||||
|
return this.filteredSelected.indexOf(this.getKey(item)) !== -1
|
||||||
|
},
|
||||||
|
toggle (checked, item) {
|
||||||
|
const key = this.getKey(item)
|
||||||
|
const oldChecked = this.isSelected(key)
|
||||||
|
if (checked !== oldChecked) {
|
||||||
|
if (checked) {
|
||||||
|
this.selected.push(key)
|
||||||
|
} else {
|
||||||
|
this.selected.splice(this.selected.indexOf(key), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleAll (value) {
|
||||||
|
if (value) {
|
||||||
|
this.selected = this.allKeys.slice(0)
|
||||||
|
} else {
|
||||||
|
this.selected = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectableList
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="selectable-list">
|
||||||
|
<div class="selectable-list-header" v-if="items.length > 0">
|
||||||
|
<div class="selectable-list-checkbox-wrapper">
|
||||||
|
<Checkbox :checked="allSelected" @change="toggleAll" :indeterminate="someSelected">{{ $t('selectable_list.select_all') }}</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="selectable-list-header-actions">
|
||||||
|
<slot name="header" :selected="filteredSelected" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<List :items="items" :getKey="getKey">
|
||||||
|
<template slot="item" slot-scope="{item}">
|
||||||
|
<div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }">
|
||||||
|
<div class="selectable-list-checkbox-wrapper">
|
||||||
|
<Checkbox :checked="isSelected(item)" @change="checked => toggle(checked, item)" />
|
||||||
|
</div>
|
||||||
|
<slot name="item" :item="item" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template slot="empty"><slot name="empty" /></template>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./selectable_list.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.selectable-list {
|
||||||
|
&-item-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item-selected-inner {
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--lightBg, $fallback--lightBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.6em 0;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-bottom-color: $fallback--border;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
|
||||||
|
&-actions {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-checkbox-wrapper {
|
||||||
|
padding: 0 10px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -46,6 +46,7 @@ const settings = {
|
||||||
streamingLocal: user.streaming,
|
streamingLocal: user.streaming,
|
||||||
pauseOnUnfocusedLocal: user.pauseOnUnfocused,
|
pauseOnUnfocusedLocal: user.pauseOnUnfocused,
|
||||||
hoverPreviewLocal: user.hoverPreview,
|
hoverPreviewLocal: user.hoverPreview,
|
||||||
|
autohideFloatingPostButtonLocal: user.autohideFloatingPostButton,
|
||||||
|
|
||||||
hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
|
hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
|
||||||
? instance.hideMutedPosts
|
? instance.hideMutedPosts
|
||||||
|
@ -70,13 +71,18 @@ const settings = {
|
||||||
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
|
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
|
||||||
? instance.alwaysShowSubjectInput
|
? instance.alwaysShowSubjectInput
|
||||||
: user.alwaysShowSubjectInput,
|
: user.alwaysShowSubjectInput,
|
||||||
alwaysShowSubjectInputDefault: instance.alwaysShowSubjectInput,
|
alwaysShowSubjectInputDefault: this.$t('settings.values.' + instance.alwaysShowSubjectInput),
|
||||||
|
|
||||||
scopeCopyLocal: typeof user.scopeCopy === 'undefined'
|
scopeCopyLocal: typeof user.scopeCopy === 'undefined'
|
||||||
? instance.scopeCopy
|
? instance.scopeCopy
|
||||||
: user.scopeCopy,
|
: user.scopeCopy,
|
||||||
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
||||||
|
|
||||||
|
minimalScopesModeLocal: typeof user.minimalScopesMode === 'undefined'
|
||||||
|
? instance.minimalScopesMode
|
||||||
|
: user.minimalScopesMode,
|
||||||
|
minimalScopesModeDefault: this.$t('settings.values.' + instance.minimalScopesMode),
|
||||||
|
|
||||||
stopGifs: user.stopGifs,
|
stopGifs: user.stopGifs,
|
||||||
webPushNotificationsLocal: user.webPushNotifications,
|
webPushNotificationsLocal: user.webPushNotifications,
|
||||||
loopVideoSilentOnlyLocal: user.loopVideosSilentOnly,
|
loopVideoSilentOnlyLocal: user.loopVideosSilentOnly,
|
||||||
|
@ -178,6 +184,9 @@ const settings = {
|
||||||
hoverPreviewLocal (value) {
|
hoverPreviewLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
|
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
|
||||||
},
|
},
|
||||||
|
autohideFloatingPostButtonLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'autohideFloatingPostButton', value })
|
||||||
|
},
|
||||||
muteWordsString (value) {
|
muteWordsString (value) {
|
||||||
value = filter(value.split('\n'), (word) => trim(word).length > 0)
|
value = filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||||
this.$store.dispatch('setOption', { name: 'muteWords', value })
|
this.$store.dispatch('setOption', { name: 'muteWords', value })
|
||||||
|
@ -200,6 +209,9 @@ const settings = {
|
||||||
postContentTypeLocal (value) {
|
postContentTypeLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'postContentType', value })
|
this.$store.dispatch('setOption', { name: 'postContentType', value })
|
||||||
},
|
},
|
||||||
|
minimalScopesModeLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'minimalScopesMode', value })
|
||||||
|
},
|
||||||
stopGifs (value) {
|
stopGifs (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
||||||
},
|
},
|
||||||
|
|
|
@ -42,9 +42,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
|
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
|
||||||
<label for="collapseMessageWithSubject">
|
<label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}</label>
|
||||||
{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}
|
|
||||||
</label>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<input type="checkbox" id="streaming" v-model="streamingLocal">
|
<input type="checkbox" id="streaming" v-model="streamingLocal">
|
||||||
|
@ -118,6 +116,16 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="minimalScopesMode" v-model="minimalScopesModeLocal">
|
||||||
|
<label for="minimalScopesMode">
|
||||||
|
{{$t('settings.minimal_scopes_mode')}} {{$t('settings.instance_default', { value: minimalScopesModeDefault })}}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="autohideFloatingPostButton" v-model="autohideFloatingPostButtonLocal">
|
||||||
|
<label for="autohideFloatingPostButton">{{$t('settings.autohide_floating_post_button')}}</label>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -295,70 +303,3 @@
|
||||||
|
|
||||||
<script src="./settings.js">
|
<script src="./settings.js">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.setting-item {
|
|
||||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
|
||||||
margin: 1em 1em 1.4em;
|
|
||||||
padding-bottom: 1.4em;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin-bottom: .5em;
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
min-width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unavailable,
|
|
||||||
.unavailable i {
|
|
||||||
color: var(--cRed, $fallback--cRed);
|
|
||||||
color: $fallback--cRed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
min-height: 28px;
|
|
||||||
min-width: 10em;
|
|
||||||
padding: 0 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number-input {
|
|
||||||
max-width: 6em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.select-multiple {
|
|
||||||
display: flex;
|
|
||||||
.option-list {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: .5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setting-list,
|
|
||||||
.option-list{
|
|
||||||
list-style-type: none;
|
|
||||||
padding-left: 2em;
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
.suboptions {
|
|
||||||
margin-top: 0.3em
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -22,13 +22,13 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="currentUser" @click="toggleDrawer">
|
<li v-if="currentUser" @click="toggleDrawer">
|
||||||
<router-link :to="{ name: 'notifications', params: { username: currentUser.screen_name } }">
|
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
|
||||||
{{ $t("notifications.notifications") }} {{ unseenNotificationsCount > 0 ? `(${unseenNotificationsCount})` : '' }}
|
{{ $t("nav.dms") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="currentUser" @click="toggleDrawer">
|
<li v-if="currentUser" @click="toggleDrawer">
|
||||||
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
|
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
|
||||||
{{ $t("nav.dms") }}
|
{{ $t("nav.interactions") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import Attachment from '../attachment/attachment.vue'
|
import Attachment from '../attachment/attachment.vue'
|
||||||
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
||||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||||
import DeleteButton from '../delete_button/delete_button.vue'
|
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import Gallery from '../gallery/gallery.vue'
|
import Gallery from '../gallery/gallery.vue'
|
||||||
import LinkPreview from '../link-preview/link-preview.vue'
|
import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
|
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||||
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'
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
import fileType from 'src/services/file_type/file_type.service'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||||
import { filter, find, unescape } from 'lodash'
|
import { filter, find, unescape, uniqBy } from 'lodash'
|
||||||
|
|
||||||
const Status = {
|
const Status = {
|
||||||
name: 'Status',
|
name: 'Status',
|
||||||
|
@ -25,18 +26,19 @@ const Status = {
|
||||||
'replies',
|
'replies',
|
||||||
'isPreview',
|
'isPreview',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'inlineExpanded'
|
'inlineExpanded',
|
||||||
|
'showPinned'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
replying: false,
|
replying: false,
|
||||||
expanded: false,
|
|
||||||
unmuted: false,
|
unmuted: false,
|
||||||
userExpanded: false,
|
userExpanded: false,
|
||||||
preview: null,
|
preview: null,
|
||||||
showPreview: false,
|
showPreview: false,
|
||||||
showingTall: this.inConversation && this.focused,
|
showingTall: this.inConversation && this.focused,
|
||||||
showingLongSubject: false,
|
showingLongSubject: false,
|
||||||
|
error: null,
|
||||||
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
|
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
|
||||||
? !this.$store.state.instance.collapseMessageWithSubject
|
? !this.$store.state.instance.collapseMessageWithSubject
|
||||||
: !this.$store.state.config.collapseMessageWithSubject,
|
: !this.$store.state.config.collapseMessageWithSubject,
|
||||||
|
@ -97,6 +99,10 @@ const Status = {
|
||||||
return this.statusoid
|
return this.statusoid
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
statusFromGlobalRepository () {
|
||||||
|
// NOTE: Consider to replace status with statusFromGlobalRepository
|
||||||
|
return this.$store.state.statuses.allStatusesObject[this.status.id]
|
||||||
|
},
|
||||||
loggedIn () {
|
loggedIn () {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
|
@ -156,7 +162,7 @@ const Status = {
|
||||||
if (this.$store.state.config.replyVisibility === 'all') {
|
if (this.$store.state.config.replyVisibility === 'all') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (this.inlineExpanded || this.expanded || this.inConversation || !this.isReply) {
|
if (this.inConversation || !this.isReply) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (this.status.user.id === this.$store.state.users.currentUser.id) {
|
if (this.status.user.id === this.$store.state.users.currentUser.id) {
|
||||||
|
@ -170,7 +176,7 @@ const Status = {
|
||||||
if (this.status.user.id === this.status.attentions[i].id) {
|
if (this.status.user.id === this.status.attentions[i].id) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (checkFollowing && this.status.attentions[i].following) {
|
if (checkFollowing && this.$store.getters.findUser(this.status.attentions[i].id).following) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
|
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
|
||||||
|
@ -251,18 +257,39 @@ const Status = {
|
||||||
},
|
},
|
||||||
maxThumbnails () {
|
maxThumbnails () {
|
||||||
return this.$store.state.config.maxThumbnails
|
return this.$store.state.config.maxThumbnails
|
||||||
|
},
|
||||||
|
contentHtml () {
|
||||||
|
if (!this.status.summary_html) {
|
||||||
|
return this.status.statusnet_html
|
||||||
|
}
|
||||||
|
return this.status.summary_html + '<br />' + this.status.statusnet_html
|
||||||
|
},
|
||||||
|
combinedFavsAndRepeatsUsers () {
|
||||||
|
// Use the status from the global status repository since favs and repeats are saved in it
|
||||||
|
const combinedUsers = [].concat(
|
||||||
|
this.statusFromGlobalRepository.favoritedBy,
|
||||||
|
this.statusFromGlobalRepository.rebloggedBy
|
||||||
|
)
|
||||||
|
return uniqBy(combinedUsers, 'id')
|
||||||
|
},
|
||||||
|
ownStatus () {
|
||||||
|
return this.status.user.id === this.$store.state.users.currentUser.id
|
||||||
|
},
|
||||||
|
tags () {
|
||||||
|
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Attachment,
|
Attachment,
|
||||||
FavoriteButton,
|
FavoriteButton,
|
||||||
RetweetButton,
|
RetweetButton,
|
||||||
DeleteButton,
|
ExtraButtons,
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
UserCard,
|
UserCard,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
Gallery,
|
Gallery,
|
||||||
LinkPreview
|
LinkPreview,
|
||||||
|
AvatarList
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
visibilityIcon (visibility) {
|
visibilityIcon (visibility) {
|
||||||
|
@ -277,6 +304,12 @@ const Status = {
|
||||||
return 'icon-globe'
|
return 'icon-globe'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showError (error) {
|
||||||
|
this.error = error
|
||||||
|
},
|
||||||
|
clearError () {
|
||||||
|
this.error = undefined
|
||||||
|
},
|
||||||
linkClicked (event) {
|
linkClicked (event) {
|
||||||
let { target } = event
|
let { target } = event
|
||||||
if (target.tagName === 'SPAN') {
|
if (target.tagName === 'SPAN') {
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
|
<div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
|
||||||
|
<div v-if="error" class="alert error">
|
||||||
|
{{error}}
|
||||||
|
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||||
|
</div>
|
||||||
<template v-if="muted && !isPreview">
|
<template v-if="muted && !isPreview">
|
||||||
<div class="media status container muted">
|
<div class="media status container muted">
|
||||||
<small>
|
<small>
|
||||||
|
@ -12,8 +16,12 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div v-if="showPinned && statusoid.pinned" class="status-pin">
|
||||||
|
<i class="fa icon-pin faint"></i>
|
||||||
|
<span class="faint">{{$t('status.pinned')}}</span>
|
||||||
|
</div>
|
||||||
<div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
<div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
||||||
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
|
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :user="statusoid.user"/>
|
||||||
<div class="media-body faint">
|
<div class="media-body faint">
|
||||||
<span class="user-name">
|
<span class="user-name">
|
||||||
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
|
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
|
||||||
|
@ -24,10 +32,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status">
|
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status" :data-tags="tags">
|
||||||
<div v-if="!noHeading" class="media-left">
|
<div v-if="!noHeading" class="media-left">
|
||||||
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
|
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
|
||||||
<UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
|
<UserAvatar :compact="compact" :betterShadow="betterShadow" :user="status.user"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-body">
|
<div class="status-body">
|
||||||
|
@ -91,23 +99,28 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showPreview" class="status-preview-container">
|
<div v-if="showPreview" class="status-preview-container">
|
||||||
<status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status>
|
<status class="status-preview"
|
||||||
<div class="status-preview status-preview-loading" v-else>
|
v-if="preview"
|
||||||
|
:isPreview="true"
|
||||||
|
:statusoid="preview"
|
||||||
|
:compact="true"
|
||||||
|
/>
|
||||||
|
<div v-else class="status-preview status-preview-loading">
|
||||||
<i class="icon-spin4 animate-spin"></i>
|
<i class="icon-spin4 animate-spin"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-content-wrapper" :class="{ 'tall-status': !showingLongSubject }" v-if="longSubject">
|
<div class="status-content-wrapper" :class="{ 'tall-status': !showingLongSubject }" v-if="longSubject">
|
||||||
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="!showingLongSubject" href="#" @click.prevent="showingLongSubject=true">Show more</a>
|
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="!showingLongSubject" href="#" @click.prevent="showingLongSubject=true">{{$t("general.show_more")}}</a>
|
||||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div>
|
<div @click.prevent="linkClicked" class="status-content media-body" v-html="contentHtml"></div>
|
||||||
<a v-if="showingLongSubject" href="#" class="status-unhider" @click.prevent="showingLongSubject=false">Show less</a>
|
<a v-if="showingLongSubject" href="#" class="status-unhider" @click.prevent="showingLongSubject=false">{{$t("general.show_less")}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" v-else>
|
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" v-else>
|
||||||
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
|
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">{{$t("general.show_more")}}</a>
|
||||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
|
<div @click.prevent="linkClicked" class="status-content media-body" v-html="contentHtml" v-if="!hideSubjectStatus"></div>
|
||||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
|
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
|
||||||
<a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
|
<a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">{{$t("general.show_more")}}</a>
|
||||||
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
|
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">{{$t("general.show_less")}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
|
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
|
||||||
|
@ -133,19 +146,37 @@
|
||||||
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
|
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<transition name="fade">
|
||||||
|
<div class="favs-repeated-users" v-if="isFocused && combinedFavsAndRepeatsUsers.length > 0">
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-count" v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0">
|
||||||
|
<a class="stat-title">{{ $t('status.repeats') }}</a>
|
||||||
|
<div class="stat-number">{{ statusFromGlobalRepository.rebloggedBy.length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-count" v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0">
|
||||||
|
<a class="stat-title">{{ $t('status.favorites') }}</a>
|
||||||
|
<div class="stat-number">{{ statusFromGlobalRepository.favoritedBy.length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-row">
|
||||||
|
<AvatarList :users="combinedFavsAndRepeatsUsers"></AvatarList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<div v-if="!noHeading && !isPreview" class='status-actions media-body'>
|
<div v-if="!noHeading && !isPreview" class='status-actions media-body'>
|
||||||
<div v-if="loggedIn">
|
<div>
|
||||||
<i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i>
|
<i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'button-icon-active': replying}" v-if="loggedIn"/>
|
||||||
|
<i class="button-icon button-icon-disabled icon-reply" :title="$t('tool_tip.reply')" v-else />
|
||||||
<span v-if="status.replies_count > 0">{{status.replies_count}}</span>
|
<span v-if="status.replies_count > 0">{{status.replies_count}}</span>
|
||||||
</div>
|
</div>
|
||||||
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
|
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
|
||||||
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
|
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
|
||||||
<delete-button :status='status'></delete-button>
|
<extra-buttons :status="status" @onError="showError" @onSuccess="clearError"></extra-buttons>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" v-if="replying">
|
<div class="container" v-if="replying">
|
||||||
<div class="reply-left"/>
|
|
||||||
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :copy-message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
|
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :copy-message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -175,6 +206,13 @@ $status-margin: 0.75em;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-pin {
|
||||||
|
padding: $status-margin $status-margin 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.status-preview {
|
.status-preview {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
max-width: 95%;
|
max-width: 95%;
|
||||||
|
@ -218,7 +256,6 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-el {
|
.status-el {
|
||||||
hyphens: auto;
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
@ -547,15 +584,13 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-reply:hover {
|
.button-icon.icon-reply {
|
||||||
|
&:not(.button-icon-disabled):hover,
|
||||||
|
&.button-icon-active {
|
||||||
color: $fallback--cBlue;
|
color: $fallback--cBlue;
|
||||||
color: var(--cBlue, $fallback--cBlue);
|
color: var(--cBlue, $fallback--cBlue);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-reply.icon-reply-active {
|
|
||||||
color: $fallback--cBlue;
|
|
||||||
color: var(--cBlue, $fallback--cBlue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status:hover .animated.avatar {
|
.status:hover .animated.avatar {
|
||||||
|
@ -595,16 +630,11 @@ a.unmute {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-left {
|
|
||||||
flex: 0;
|
|
||||||
min-width: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-body {
|
.reply-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline > {
|
.timeline :not(.panel-disabled) > {
|
||||||
.status-el:last-child {
|
.status-el:last-child {
|
||||||
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);
|
||||||
|
@ -612,6 +642,50 @@ a.unmute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.favs-repeated-users {
|
||||||
|
margin-top: $status-margin;
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
line-height: 1em;
|
||||||
|
|
||||||
|
.stat-count {
|
||||||
|
margin-right: $status-margin;
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
color: var(--faint, $fallback--faint);
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-weight: bolder;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-row {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
left: 0;
|
||||||
|
background-color: var(--faint, $fallback--faint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 800px) {
|
@media all and (max-width: 800px) {
|
||||||
.status-el {
|
.status-el {
|
||||||
.retweet-info {
|
.retweet-info {
|
||||||
|
|
|
@ -4,15 +4,18 @@ import './tab_switcher.scss'
|
||||||
|
|
||||||
export default Vue.component('tab-switcher', {
|
export default Vue.component('tab-switcher', {
|
||||||
name: 'TabSwitcher',
|
name: 'TabSwitcher',
|
||||||
props: ['renderOnlyFocused'],
|
props: ['renderOnlyFocused', 'onSwitch'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
active: this.$slots.default.findIndex(_ => _.tag)
|
active: this.$slots.default.findIndex(_ => _.tag)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activateTab (index) {
|
activateTab (index, dataset) {
|
||||||
return () => {
|
return () => {
|
||||||
|
if (typeof this.onSwitch === 'function') {
|
||||||
|
this.onSwitch.call(null, index, this.$slots.default[index].elm.dataset)
|
||||||
|
}
|
||||||
this.active = index
|
this.active = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +40,11 @@ export default Vue.component('tab-switcher', {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={ classesWrapper.join(' ')}>
|
<div class={ classesWrapper.join(' ')}>
|
||||||
<button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
<button
|
||||||
|
disabled={slot.data.attrs.disabled}
|
||||||
|
onClick={this.activateTab(index)}
|
||||||
|
class={classesTab.join(' ')}>
|
||||||
|
{slot.data.attrs.label}</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
|
||||||
const TagTimeline = {
|
const TagTimeline = {
|
||||||
created () {
|
created () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||||
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Timeline
|
Timeline
|
||||||
|
@ -15,7 +15,7 @@ const TagTimeline = {
|
||||||
watch: {
|
watch: {
|
||||||
tag () {
|
tag () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||||
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
|
|
|
@ -52,7 +52,7 @@ const Timeline = {
|
||||||
|
|
||||||
window.addEventListener('scroll', this.scrollLoad)
|
window.addEventListener('scroll', this.scrollLoad)
|
||||||
|
|
||||||
if (this.timelineName === 'friends' && !credentials) { return false }
|
if (store.state.api.fetchers[this.timelineName]) { return false }
|
||||||
|
|
||||||
timelineFetcher.fetchAndUpdate({
|
timelineFetcher.fetchAndUpdate({
|
||||||
store,
|
store,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import StillImage from '../still-image/still-image.vue'
|
||||||
|
|
||||||
const UserAvatar = {
|
const UserAvatar = {
|
||||||
props: [
|
props: [
|
||||||
'src',
|
'user',
|
||||||
'betterShadow',
|
'betterShadow',
|
||||||
'compact'
|
'compact'
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<StillImage
|
<StillImage
|
||||||
class="avatar"
|
class="avatar"
|
||||||
|
:alt="user.screen_name"
|
||||||
|
:title="user.screen_name"
|
||||||
|
:src="user.profile_image_url_original"
|
||||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||||
:src="imgSrc"
|
|
||||||
:imageLoadError="imageLoadError"
|
:imageLoadError="imageLoadError"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||||
|
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||||
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'
|
||||||
|
@ -93,15 +94,17 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visibleRole () {
|
visibleRole () {
|
||||||
const validRole = (this.user.role === 'admin' || this.user.role === 'moderator')
|
const rights = this.user.rights
|
||||||
const showRole = this.isOtherUser || this.user.show_role
|
if (!rights) { return }
|
||||||
|
const validRole = rights.admin || rights.moderator
|
||||||
return validRole && showRole && this.user.role
|
const roleTitle = rights.admin ? 'admin' : 'moderator'
|
||||||
|
return validRole && roleTitle
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
RemoteFollow
|
RemoteFollow,
|
||||||
|
ModerationTools
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
followUser () {
|
followUser () {
|
||||||
|
@ -148,6 +151,9 @@ export default {
|
||||||
},
|
},
|
||||||
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)
|
||||||
|
},
|
||||||
|
reportUser () {
|
||||||
|
this.$store.dispatch('openUserReportingModal', this.user.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,26 +4,26 @@
|
||||||
<div class='user-info'>
|
<div class='user-info'>
|
||||||
<div class='container'>
|
<div class='container'>
|
||||||
<router-link :to="userProfileLink(user)">
|
<router-link :to="userProfileLink(user)">
|
||||||
<UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/>
|
<UserAvatar :betterShadow="betterShadow" :user="user"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="name-and-screen-name">
|
<div class="user-summary">
|
||||||
<div class="top-line">
|
<div class="top-line">
|
||||||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||||
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
||||||
<i class="button-icon icon-pencil usersettings" :title="$t('tool_tip.user_settings')"></i>
|
<i class="button-icon icon-wrench usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||||
</router-link>
|
</router-link>
|
||||||
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
|
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
|
||||||
<i class="icon-link-ext usersettings"></i>
|
<i class="icon-link-ext usersettings"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
<div class="bottom-line">
|
||||||
<span class="handle">@{{user.screen_name}}
|
<router-link class="user-screen-name" :to="userProfileLink(user)">@{{user.screen_name}}</router-link>
|
||||||
<span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span>
|
<span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span>
|
||||||
</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
<span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
||||||
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||||
</router-link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-meta">
|
<div class="user-meta">
|
||||||
|
@ -99,6 +99,14 @@
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class='block' v-if='isOtherUser && loggedIn'>
|
||||||
|
<span>
|
||||||
|
<button @click="reportUser">
|
||||||
|
{{ $t('user_card.report') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,7 +168,7 @@
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
|
||||||
.emoji {
|
&.emoji {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
@ -224,7 +232,7 @@
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-and-screen-name {
|
.user-summary {
|
||||||
display: block;
|
display: block;
|
||||||
margin-left: 0.6em;
|
margin-left: 0.6em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -241,6 +249,7 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
object-fit: contain
|
object-fit: contain
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-line {
|
.top-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -261,15 +270,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-screen-name {
|
.bottom-line {
|
||||||
color: $fallback--lightText;
|
display: flex;
|
||||||
color: var(--lightText, $fallback--lightText);
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: light;
|
font-weight: light;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding-right: 0.1em;
|
|
||||||
width: 100%;
|
.user-screen-name {
|
||||||
display: flex;
|
min-width: 1px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
color: $fallback--lightText;
|
||||||
|
color: var(--lightText, $fallback--lightText);
|
||||||
|
}
|
||||||
|
|
||||||
.dailyAvg {
|
.dailyAvg {
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
|
@ -280,15 +293,9 @@
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle {
|
|
||||||
min-width: 1px;
|
|
||||||
flex: 0 1 auto;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO use proper colors
|
// TODO use proper colors
|
||||||
.staff {
|
.staff {
|
||||||
|
flex: none;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnText, $fallback--text);
|
color: var(--btnText, $fallback--text);
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import LoginForm from '../login_form/login_form.vue'
|
import AuthForm from '../auth_form/auth_form.js'
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
const UserPanel = {
|
const UserPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
user () { return this.$store.state.users.currentUser }
|
signedIn () { return this.user },
|
||||||
|
...mapState({ user: state => state.users.currentUser })
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
LoginForm,
|
AuthForm,
|
||||||
PostStatusForm,
|
PostStatusForm,
|
||||||
UserCard
|
UserCard
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="user-panel">
|
<div class="user-panel">
|
||||||
<div v-if='user' class="panel panel-default" style="overflow: visible;">
|
|
||||||
|
<div v-if="signedIn" key="user-panel" class="panel panel-default signed-in">
|
||||||
<UserCard :user="user" :hideBio="true" rounded="top"/>
|
<UserCard :user="user" :hideBio="true" rounded="top"/>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
<post-status-form v-if='user'></post-status-form>
|
<post-status-form v-if='user'></post-status-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<login-form v-if='!user'></login-form>
|
<auth-form v-else key="user-panel"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./user_panel.js"></script>
|
<script src="./user_panel.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user-panel .signed-in {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,47 +1,38 @@
|
||||||
import { compose } from 'vue-compose'
|
|
||||||
import get from 'lodash/get'
|
import get from 'lodash/get'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import FollowCard from '../follow_card/follow_card.vue'
|
import FollowCard from '../follow_card/follow_card.vue'
|
||||||
import Timeline from '../timeline/timeline.vue'
|
import Timeline from '../timeline/timeline.vue'
|
||||||
|
import Conversation from '../conversation/conversation.vue'
|
||||||
|
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||||
|
import List from '../list/list.vue'
|
||||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||||
import withList from '../../hocs/with_list/with_list'
|
|
||||||
|
|
||||||
const FollowerList = compose(
|
const FollowerList = withLoadMore({
|
||||||
withLoadMore({
|
fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
|
||||||
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
|
select: (props, $store) => get($store.getters.findUser(props.userId), 'followerIds', []).map(id => $store.getters.findUser(id)),
|
||||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'followers', []),
|
destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||||
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
childPropName: 'items',
|
||||||
childPropName: 'entries',
|
|
||||||
additionalPropNames: ['userId']
|
additionalPropNames: ['userId']
|
||||||
}),
|
})(List)
|
||||||
withList({ getEntryProps: user => ({ user }) })
|
|
||||||
)(FollowCard)
|
|
||||||
|
|
||||||
const FriendList = compose(
|
const FriendList = withLoadMore({
|
||||||
withLoadMore({
|
fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
|
||||||
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
|
select: (props, $store) => get($store.getters.findUser(props.userId), 'friendIds', []).map(id => $store.getters.findUser(id)),
|
||||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []),
|
destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||||
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
childPropName: 'items',
|
||||||
childPropName: 'entries',
|
|
||||||
additionalPropNames: ['userId']
|
additionalPropNames: ['userId']
|
||||||
}),
|
})(List)
|
||||||
withList({ getEntryProps: user => ({ user }) })
|
|
||||||
)(FollowCard)
|
|
||||||
|
|
||||||
const UserProfile = {
|
const UserProfile = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
error: false,
|
error: false,
|
||||||
fetchedUserId: null
|
userId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
if (!this.user.id) {
|
const routeParams = this.$route.params
|
||||||
this.fetchUserId()
|
this.load(routeParams.name || routeParams.id)
|
||||||
.then(() => this.startUp())
|
|
||||||
} else {
|
|
||||||
this.startUp()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.cleanUp()
|
this.cleanUp()
|
||||||
|
@ -56,29 +47,12 @@ const UserProfile = {
|
||||||
media () {
|
media () {
|
||||||
return this.$store.state.statuses.timelines.media
|
return this.$store.state.statuses.timelines.media
|
||||||
},
|
},
|
||||||
userId () {
|
|
||||||
return this.$route.params.id || this.user.id || this.fetchedUserId
|
|
||||||
},
|
|
||||||
userName () {
|
|
||||||
return this.$route.params.name || this.user.screen_name
|
|
||||||
},
|
|
||||||
isUs () {
|
isUs () {
|
||||||
return this.userId && this.$store.state.users.currentUser.id &&
|
return this.userId && this.$store.state.users.currentUser.id &&
|
||||||
this.userId === this.$store.state.users.currentUser.id
|
this.userId === this.$store.state.users.currentUser.id
|
||||||
},
|
},
|
||||||
userInStore () {
|
|
||||||
const routeParams = this.$route.params
|
|
||||||
// This needs fetchedUserId so that computed will be refreshed when user is fetched
|
|
||||||
return this.$store.getters.findUser(this.fetchedUserId || routeParams.name || routeParams.id)
|
|
||||||
},
|
|
||||||
user () {
|
user () {
|
||||||
if (this.timeline.statuses[0]) {
|
return this.$store.getters.findUser(this.userId)
|
||||||
return this.timeline.statuses[0].user
|
|
||||||
}
|
|
||||||
if (this.userInStore) {
|
|
||||||
return this.userInStore
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
},
|
},
|
||||||
isExternal () {
|
isExternal () {
|
||||||
return this.$route.name === 'external-user-profile'
|
return this.$route.name === 'external-user-profile'
|
||||||
|
@ -91,22 +65,18 @@ const UserProfile = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startFetchFavorites () {
|
load (userNameOrId) {
|
||||||
if (this.isUs) {
|
// Check if user data is already loaded in store
|
||||||
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId })
|
const user = this.$store.getters.findUser(userNameOrId)
|
||||||
}
|
if (user) {
|
||||||
},
|
this.userId = user.id
|
||||||
fetchUserId () {
|
this.fetchTimelines()
|
||||||
let fetchPromise
|
|
||||||
if (this.userId && !this.$route.params.name) {
|
|
||||||
fetchPromise = this.$store.dispatch('fetchUser', this.userId)
|
|
||||||
} else {
|
} else {
|
||||||
fetchPromise = this.$store.dispatch('fetchUser', this.userName)
|
this.$store.dispatch('fetchUser', userNameOrId)
|
||||||
.then(({ id }) => {
|
.then(({ id }) => {
|
||||||
this.fetchedUserId = id
|
this.userId = id
|
||||||
|
this.fetchTimelines()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
return fetchPromise
|
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
const errorMessage = get(reason, 'error.error')
|
const errorMessage = get(reason, 'error.error')
|
||||||
if (errorMessage === 'No user with such user_id') { // Known error
|
if (errorMessage === 'No user with such user_id') { // Known error
|
||||||
|
@ -117,15 +87,18 @@ const UserProfile = {
|
||||||
this.error = this.$t('user_profile.profile_loading_error')
|
this.error = this.$t('user_profile.profile_loading_error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => this.startUp())
|
|
||||||
},
|
|
||||||
startUp () {
|
|
||||||
if (this.userId) {
|
|
||||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId })
|
|
||||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId })
|
|
||||||
this.startFetchFavorites()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fetchTimelines () {
|
||||||
|
const userId = this.userId
|
||||||
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
|
||||||
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
|
||||||
|
if (this.isUs) {
|
||||||
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
|
||||||
|
}
|
||||||
|
// Fetch all pinned statuses immediately
|
||||||
|
this.$store.dispatch('fetchPinnedStatuses', userId)
|
||||||
|
},
|
||||||
cleanUp () {
|
cleanUp () {
|
||||||
this.$store.dispatch('stopFetching', 'user')
|
this.$store.dispatch('stopFetching', 'user')
|
||||||
this.$store.dispatch('stopFetching', 'favorites')
|
this.$store.dispatch('stopFetching', 'favorites')
|
||||||
|
@ -136,18 +109,16 @@ const UserProfile = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
// userId can be undefined if we don't know it yet
|
'$route.params.id': function (newVal) {
|
||||||
userId (newVal) {
|
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.cleanUp()
|
this.cleanUp()
|
||||||
this.startUp()
|
this.load(newVal)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userName () {
|
'$route.params.name': function (newVal) {
|
||||||
if (this.$route.params.name) {
|
if (newVal) {
|
||||||
this.fetchUserId()
|
|
||||||
this.cleanUp()
|
this.cleanUp()
|
||||||
this.startUp()
|
this.load(newVal)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
$route () {
|
$route () {
|
||||||
|
@ -158,7 +129,10 @@ const UserProfile = {
|
||||||
UserCard,
|
UserCard,
|
||||||
Timeline,
|
Timeline,
|
||||||
FollowerList,
|
FollowerList,
|
||||||
FriendList
|
FriendList,
|
||||||
|
ModerationTools,
|
||||||
|
FollowCard,
|
||||||
|
Conversation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="user.id" class="user-profile panel panel-default">
|
<div v-if="user" class="user-profile panel panel-default">
|
||||||
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
|
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
|
||||||
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
|
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
|
||||||
|
<div :label="$t('user_card.statuses')" :disabled="!user.statuses_count">
|
||||||
|
<div class="timeline">
|
||||||
|
<template v-for="statusId in user.pinnedStatuseIds">
|
||||||
|
<Conversation
|
||||||
|
v-if="timeline.statusesObject[statusId]"
|
||||||
|
class="status-fadein"
|
||||||
|
:key="statusId"
|
||||||
|
:statusoid="timeline.statusesObject[statusId]"
|
||||||
|
:collapsable="true"
|
||||||
|
:showPinned="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<Timeline
|
<Timeline
|
||||||
:label="$t('user_card.statuses')"
|
|
||||||
:disabled="!user.statuses_count"
|
|
||||||
:count="user.statuses_count"
|
:count="user.statuses_count"
|
||||||
:embedded="true"
|
:embedded="true"
|
||||||
:title="$t('user_profile.timeline_title')"
|
:title="$t('user_profile.timeline_title')"
|
||||||
|
@ -13,11 +24,20 @@
|
||||||
:timeline-name="'user'"
|
:timeline-name="'user'"
|
||||||
:user-id="userId"
|
:user-id="userId"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
||||||
<FriendList :userId="userId" />
|
<FriendList :userId="userId">
|
||||||
|
<template slot="item" slot-scope="{item}">
|
||||||
|
<FollowCard :user="item" />
|
||||||
|
</template>
|
||||||
|
</FriendList>
|
||||||
</div>
|
</div>
|
||||||
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
|
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
|
||||||
<FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
|
<FollowerList :userId="userId">
|
||||||
|
<template slot="item" slot-scope="{item}">
|
||||||
|
<FollowCard :user="item" :noFollowsYou="isUs" />
|
||||||
|
</template>
|
||||||
|
</FollowerList>
|
||||||
</div>
|
</div>
|
||||||
<Timeline
|
<Timeline
|
||||||
:label="$t('user_card.media')"
|
:label="$t('user_card.media')"
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
|
||||||
|
import Status from '../status/status.vue'
|
||||||
|
import List from '../list/list.vue'
|
||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const UserReportingModal = {
|
||||||
|
components: {
|
||||||
|
Status,
|
||||||
|
List,
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
comment: '',
|
||||||
|
forward: false,
|
||||||
|
statusIdsToReport: [],
|
||||||
|
processing: false,
|
||||||
|
error: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
isOpen () {
|
||||||
|
return this.isLoggedIn && this.$store.state.reports.modalActivated
|
||||||
|
},
|
||||||
|
userId () {
|
||||||
|
return this.$store.state.reports.userId
|
||||||
|
},
|
||||||
|
user () {
|
||||||
|
return this.$store.getters.findUser(this.userId)
|
||||||
|
},
|
||||||
|
remoteInstance () {
|
||||||
|
return !this.user.is_local && this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1)
|
||||||
|
},
|
||||||
|
statuses () {
|
||||||
|
return this.$store.state.reports.statuses
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
userId: 'resetState'
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetState () {
|
||||||
|
// Reset state
|
||||||
|
this.comment = ''
|
||||||
|
this.forward = false
|
||||||
|
this.statusIdsToReport = []
|
||||||
|
this.processing = false
|
||||||
|
this.error = false
|
||||||
|
},
|
||||||
|
closeModal () {
|
||||||
|
this.$store.dispatch('closeUserReportingModal')
|
||||||
|
},
|
||||||
|
reportUser () {
|
||||||
|
this.processing = true
|
||||||
|
this.error = false
|
||||||
|
const params = {
|
||||||
|
userId: this.userId,
|
||||||
|
comment: this.comment,
|
||||||
|
forward: this.forward,
|
||||||
|
statusIds: this.statusIdsToReport
|
||||||
|
}
|
||||||
|
this.$store.state.api.backendInteractor.reportUser(params)
|
||||||
|
.then(() => {
|
||||||
|
this.processing = false
|
||||||
|
this.resetState()
|
||||||
|
this.closeModal()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.processing = false
|
||||||
|
this.error = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearError () {
|
||||||
|
this.error = false
|
||||||
|
},
|
||||||
|
isChecked (statusId) {
|
||||||
|
return this.statusIdsToReport.indexOf(statusId) !== -1
|
||||||
|
},
|
||||||
|
toggleStatus (checked, statusId) {
|
||||||
|
if (checked === this.isChecked(statusId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
this.statusIdsToReport.push(statusId)
|
||||||
|
} else {
|
||||||
|
this.statusIdsToReport.splice(this.statusIdsToReport.indexOf(statusId), 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resize (e) {
|
||||||
|
const target = e.target || e
|
||||||
|
if (!(target instanceof window.Element)) { return }
|
||||||
|
// Auto is needed to make textbox shrink when removing lines
|
||||||
|
target.style.height = 'auto'
|
||||||
|
target.style.height = `${target.scrollHeight}px`
|
||||||
|
if (target.value === '') {
|
||||||
|
target.style.height = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserReportingModal
|
|
@ -0,0 +1,157 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal-view" @click="closeModal" v-if="isOpen">
|
||||||
|
<div class="user-reporting-panel panel" @click.stop="">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="title">{{$t('user_reporting.title', [user.screen_name])}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="user-reporting-panel-left">
|
||||||
|
<div>
|
||||||
|
<p>{{$t('user_reporting.add_comment_description')}}</p>
|
||||||
|
<textarea
|
||||||
|
v-model="comment"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="$t('user_reporting.additional_comments')"
|
||||||
|
rows="1"
|
||||||
|
@input="resize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="!user.is_local">
|
||||||
|
<p>{{$t('user_reporting.forward_description')}}</p>
|
||||||
|
<Checkbox v-model="forward">{{$t('user_reporting.forward_to', [remoteInstance])}}</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-default" @click="reportUser" :disabled="processing">{{$t('user_reporting.submit')}}</button>
|
||||||
|
<div class="alert error" v-if="error">
|
||||||
|
{{$t('user_reporting.generic_error')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-reporting-panel-right">
|
||||||
|
<List :items="statuses">
|
||||||
|
<template slot="item" slot-scope="{item}">
|
||||||
|
<div class="status-fadein user-reporting-panel-sitem">
|
||||||
|
<Status :inConversation="false" :focused="false" :statusoid="item" />
|
||||||
|
<Checkbox :checked="isChecked(item.id)" @change="checked => toggleStatus(checked, item.id)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./user_reporting_modal.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.user-reporting-panel {
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 700px;
|
||||||
|
min-height: 20vh;
|
||||||
|
max-height: 80vh;
|
||||||
|
|
||||||
|
.panel-heading {
|
||||||
|
.title {
|
||||||
|
text-align: center;
|
||||||
|
// TODO: Consider making these as default of panel
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
border-top: 1px solid;
|
||||||
|
border-color: $fallback--border;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
padding: 1.1em 0.7em 0.7em;
|
||||||
|
line-height: 1.4em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
line-height: 16px;
|
||||||
|
resize: none;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: min-height 200ms 100ms;
|
||||||
|
min-height: 44px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-width: 10em;
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin: 1em 0 0 0;
|
||||||
|
line-height: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-sitem {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> .status-el {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .checkbox {
|
||||||
|
margin: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 801px) {
|
||||||
|
.panel-body {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
width: 50%;
|
||||||
|
max-width: 320px;
|
||||||
|
border-right: 1px solid;
|
||||||
|
border-color: $fallback--border;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
padding: 1.1em;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
width: 50%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,6 @@
|
||||||
import FollowCard from '../follow_card/follow_card.vue'
|
import FollowCard from '../follow_card/follow_card.vue'
|
||||||
import userSearchApi from '../../services/new_api/user_search.js'
|
import map from 'lodash/map'
|
||||||
|
|
||||||
const userSearch = {
|
const userSearch = {
|
||||||
components: {
|
components: {
|
||||||
FollowCard
|
FollowCard
|
||||||
|
@ -10,10 +11,15 @@ const userSearch = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
username: '',
|
username: '',
|
||||||
users: [],
|
userIds: [],
|
||||||
loading: false
|
loading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
users () {
|
||||||
|
return this.userIds.map(userId => this.$store.getters.findUser(userId))
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.search(this.query)
|
this.search(this.query)
|
||||||
},
|
},
|
||||||
|
@ -33,10 +39,10 @@ const userSearch = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.loading = true
|
this.loading = true
|
||||||
userSearchApi.search({query, store: this.$store})
|
this.$store.dispatch('searchUsers', query)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.users = res
|
this.userIds = map(res, 'id')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<i class="icon-spin3 animate-spin"/>
|
<i class="icon-spin3 animate-spin"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="panel-body">
|
<div v-else class="panel-body">
|
||||||
<FollowCard v-for="user in users" :key="user.id" :user="user"/>
|
<FollowCard v-for="user in users" :key="user.id" :user="user" class="list-item"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
const Confirm = {
|
||||||
|
props: ['disabled'],
|
||||||
|
data: () => ({}),
|
||||||
|
methods: {
|
||||||
|
confirm () { this.$emit('confirm') },
|
||||||
|
cancel () { this.$emit('cancel') }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Confirm
|
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot></slot>
|
||||||
|
<button class="btn btn-default" @click="confirm" :disabled="disabled">
|
||||||
|
{{$t('general.confirm')}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" @click="cancel" :disabled="disabled">
|
||||||
|
{{$t('general.cancel')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./confirm.js">
|
||||||
|
</script>
|
|
@ -0,0 +1,152 @@
|
||||||
|
import RecoveryCodes from './mfa_backup_codes.vue'
|
||||||
|
import TOTP from './mfa_totp.vue'
|
||||||
|
import Confirm from './confirm.vue'
|
||||||
|
import VueQrcode from '@chenfengyuan/vue-qrcode'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
const Mfa = {
|
||||||
|
data: () => ({
|
||||||
|
settings: { // current settings of MFA
|
||||||
|
enabled: false,
|
||||||
|
totp: false
|
||||||
|
},
|
||||||
|
setupState: { // setup mfa
|
||||||
|
state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
|
||||||
|
setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
|
||||||
|
},
|
||||||
|
backupCodes: {
|
||||||
|
getNewCodes: false,
|
||||||
|
inProgress: false, // progress of fetch codes
|
||||||
|
codes: []
|
||||||
|
},
|
||||||
|
otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
|
||||||
|
provisioning_uri: '',
|
||||||
|
key: ''
|
||||||
|
},
|
||||||
|
currentPassword: null,
|
||||||
|
otpConfirmToken: null,
|
||||||
|
error: null,
|
||||||
|
readyInit: false
|
||||||
|
}),
|
||||||
|
components: {
|
||||||
|
'recovery-codes': RecoveryCodes,
|
||||||
|
'totp-item': TOTP,
|
||||||
|
'qrcode': VueQrcode,
|
||||||
|
'confirm': Confirm
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canSetupOTP () {
|
||||||
|
return (
|
||||||
|
(this.setupInProgress && this.backupCodesPrepared) ||
|
||||||
|
this.settings.enabled
|
||||||
|
) && !this.settings.totp && !this.setupOTPInProgress
|
||||||
|
},
|
||||||
|
setupInProgress () {
|
||||||
|
return this.setupState.state !== '' && this.setupState.state !== 'complete'
|
||||||
|
},
|
||||||
|
setupOTPInProgress () {
|
||||||
|
return this.setupState.state === 'setupOTP' && !this.completedOTP
|
||||||
|
},
|
||||||
|
prepareOTP () {
|
||||||
|
return this.setupState.setupOTPState === 'prepare'
|
||||||
|
},
|
||||||
|
confirmOTP () {
|
||||||
|
return this.setupState.setupOTPState === 'confirm'
|
||||||
|
},
|
||||||
|
completedOTP () {
|
||||||
|
return this.setupState.setupOTPState === 'completed'
|
||||||
|
},
|
||||||
|
backupCodesPrepared () {
|
||||||
|
return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
|
||||||
|
},
|
||||||
|
confirmNewBackupCodes () {
|
||||||
|
return this.backupCodes.getNewCodes
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
backendInteractor: (state) => state.api.backendInteractor
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
activateOTP () {
|
||||||
|
if (!this.settings.enabled) {
|
||||||
|
this.setupState.state = 'getBackupcodes'
|
||||||
|
this.fetchBackupCodes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchBackupCodes () {
|
||||||
|
this.backupCodes.inProgress = true
|
||||||
|
this.backupCodes.codes = []
|
||||||
|
|
||||||
|
return this.backendInteractor.generateMfaBackupCodes()
|
||||||
|
.then((res) => {
|
||||||
|
this.backupCodes.codes = res.codes
|
||||||
|
this.backupCodes.inProgress = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getBackupCodes () { // get a new backup codes
|
||||||
|
this.backupCodes.getNewCodes = true
|
||||||
|
},
|
||||||
|
confirmBackupCodes () { // confirm getting new backup codes
|
||||||
|
this.fetchBackupCodes().then((res) => {
|
||||||
|
this.backupCodes.getNewCodes = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cancelBackupCodes () { // cancel confirm form of new backup codes
|
||||||
|
this.backupCodes.getNewCodes = false
|
||||||
|
},
|
||||||
|
|
||||||
|
// Setup OTP
|
||||||
|
setupOTP () { // prepare setup OTP
|
||||||
|
this.setupState.state = 'setupOTP'
|
||||||
|
this.setupState.setupOTPState = 'prepare'
|
||||||
|
this.backendInteractor.mfaSetupOTP()
|
||||||
|
.then((res) => {
|
||||||
|
this.otpSettings = res
|
||||||
|
this.setupState.setupOTPState = 'confirm'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
doConfirmOTP () { // handler confirm enable OTP
|
||||||
|
this.error = null
|
||||||
|
this.backendInteractor.mfaConfirmOTP({
|
||||||
|
token: this.otpConfirmToken,
|
||||||
|
password: this.currentPassword
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
this.error = res.error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.completeSetup()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
completeSetup () {
|
||||||
|
this.setupState.setupOTPState = 'complete'
|
||||||
|
this.setupState.state = 'complete'
|
||||||
|
this.currentPassword = null
|
||||||
|
this.error = null
|
||||||
|
this.fetchSettings()
|
||||||
|
},
|
||||||
|
cancelSetup () { // cancel setup
|
||||||
|
this.setupState.setupOTPState = ''
|
||||||
|
this.setupState.state = ''
|
||||||
|
this.currentPassword = null
|
||||||
|
this.error = null
|
||||||
|
},
|
||||||
|
// end Setup OTP
|
||||||
|
|
||||||
|
// fetch settings from server
|
||||||
|
async fetchSettings () {
|
||||||
|
let result = await this.backendInteractor.fetchSettingsMFA()
|
||||||
|
this.settings = result.settings
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.fetchSettings().then(() => {
|
||||||
|
this.readyInit = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Mfa
|
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<div class="setting-item mfa-settings" v-if="readyInit">
|
||||||
|
|
||||||
|
<div class="mfa-heading">
|
||||||
|
<h2>{{$t('settings.mfa.title')}}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="setting-item" v-if="!setupInProgress">
|
||||||
|
<!-- Enabled methods -->
|
||||||
|
<h3>{{$t('settings.mfa.authentication_methods')}}</h3>
|
||||||
|
<totp-item :settings="settings" @deactivate="fetchSettings" @activate="activateOTP"/>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div v-if="settings.enabled"> <!-- backup codes block-->
|
||||||
|
<recovery-codes :backup-codes="backupCodes" v-if="!confirmNewBackupCodes" />
|
||||||
|
<button class="btn btn-default" @click="getBackupCodes" v-if="!confirmNewBackupCodes">
|
||||||
|
{{$t('settings.mfa.generate_new_recovery_codes')}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="confirmNewBackupCodes">
|
||||||
|
<confirm @confirm="confirmBackupCodes" @cancel="cancelBackupCodes"
|
||||||
|
:disabled="backupCodes.inProgress">
|
||||||
|
<p class="warning">{{$t('settings.mfa.warning_of_generate_new_codes')}}</p>
|
||||||
|
</confirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="setupInProgress"> <!-- setup block-->
|
||||||
|
|
||||||
|
<h3>{{$t('settings.mfa.setup_otp')}}</h3>
|
||||||
|
|
||||||
|
<recovery-codes :backup-codes="backupCodes" v-if="!setupOTPInProgress"/>
|
||||||
|
|
||||||
|
|
||||||
|
<button class="btn btn-default" @click="cancelSetup" v-if="canSetupOTP">
|
||||||
|
{{$t('general.cancel')}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-default" v-if="canSetupOTP" @click="setupOTP">
|
||||||
|
{{$t('settings.mfa.setup_otp')}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<template v-if="setupOTPInProgress">
|
||||||
|
<i v-if="prepareOTP">{{$t('settings.mfa.wait_pre_setup_otp')}}</i>
|
||||||
|
|
||||||
|
<div v-if="confirmOTP">
|
||||||
|
<div class="setup-otp">
|
||||||
|
<div class="qr-code">
|
||||||
|
<h4>{{$t('settings.mfa.scan.title')}}</h4>
|
||||||
|
<p>{{$t('settings.mfa.scan.desc')}}</p>
|
||||||
|
<qrcode :value="otpSettings.provisioning_uri" :options="{ width: 200 }"></qrcode>
|
||||||
|
<p>
|
||||||
|
{{$t('settings.mfa.scan.secret_code')}}:
|
||||||
|
{{otpSettings.key}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="verify">
|
||||||
|
<h4>{{$t('general.verify')}}</h4>
|
||||||
|
<p>{{$t('settings.mfa.verify.desc')}}</p>
|
||||||
|
<input type="text" v-model="otpConfirmToken">
|
||||||
|
|
||||||
|
<p>{{$t('settings.enter_current_password_to_confirm')}}:</p>
|
||||||
|
<input type="password" v-model="currentPassword">
|
||||||
|
<div class="confirm-otp-actions">
|
||||||
|
<button class="btn btn-default" @click="doConfirmOTP">
|
||||||
|
{{$t('settings.mfa.confirm_and_enable')}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" @click="cancelSetup">
|
||||||
|
{{$t('general.cancel')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="alert error" v-if="error">{{error}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./mfa.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
.warning {
|
||||||
|
color: $fallback--cOrange;
|
||||||
|
color: var(--cOrange, $fallback--cOrange);
|
||||||
|
}
|
||||||
|
.mfa-settings {
|
||||||
|
.mfa-heading, .method-item {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-otp {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.qr-code {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
.verify { flex: 1; }
|
||||||
|
.error { margin: 4px 0 0 0; }
|
||||||
|
.confirm-otp-actions {
|
||||||
|
button {
|
||||||
|
width: 15em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue