merge develop in, fix lots of things
This commit is contained in:
commit
82802c6a8d
130 changed files with 6972 additions and 2788 deletions
33
.eslintrc.js
33
.eslintrc.js
|
@ -1,14 +1,17 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: 'standard',
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'html'
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
|
@ -17,6 +20,28 @@ module.exports = {
|
|||
// allow async-await
|
||||
'generator-star-spacing': 0,
|
||||
// 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, // ????
|
||||
'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.
|
||||
# Official framework image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/node/tags/
|
||||
image: node:7
|
||||
image: node:8
|
||||
|
||||
stages:
|
||||
- lint
|
||||
|
@ -16,7 +16,12 @@ lint:
|
|||
|
||||
test:
|
||||
stage: test
|
||||
variables:
|
||||
APT_CACHE_DIR: apt-cache
|
||||
script:
|
||||
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
- firefox --version
|
||||
- yarn
|
||||
- npm run unit
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
var path = require('path')
|
||||
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) {
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
|
@ -11,51 +12,51 @@ exports.assetsPath = function (_path) {
|
|||
|
||||
exports.cssLoaders = function (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
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return ExtractTextPlugin.extract('vue-style-loader', sourceLoader)
|
||||
return [MiniCssExtractPlugin.loader].concat(loaders)
|
||||
} else {
|
||||
return ['vue-style-loader', sourceLoader].join('!')
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
|
||||
return {
|
||||
css: generateLoaders(['css']),
|
||||
postcss: generateLoaders(['css']),
|
||||
less: generateLoaders(['css', 'less']),
|
||||
sass: generateLoaders(['css', 'sass?indentedSyntax']),
|
||||
scss: generateLoaders(['css', 'sass']),
|
||||
stylus: generateLoaders(['css', 'stylus']),
|
||||
styl: generateLoaders(['css', 'stylus'])
|
||||
return [
|
||||
{
|
||||
test: /\.(post)?css$/,
|
||||
use: generateLoaders(['css-loader']),
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
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)
|
||||
exports.styleLoaders = function (options) {
|
||||
var output = []
|
||||
var loaders = exports.cssLoaders(options)
|
||||
for (var extension in loaders) {
|
||||
var loader = loaders[extension]
|
||||
output.push({
|
||||
test: new RegExp('\\.' + extension + '$'),
|
||||
loader: loader
|
||||
})
|
||||
}
|
||||
return output
|
||||
return exports.cssLoaders(options)
|
||||
}
|
||||
|
|
|
@ -20,9 +20,16 @@ module.exports = {
|
|||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.vue'],
|
||||
fallback: [path.join(__dirname, '../node_modules')],
|
||||
extensions: ['.js', '.vue'],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.runtime.common',
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
|
@ -30,67 +37,53 @@ module.exports = {
|
|||
'components': path.resolve(__dirname, '../src/components')
|
||||
}
|
||||
},
|
||||
resolveLoader: {
|
||||
fallback: [path.join(__dirname, '../node_modules')]
|
||||
},
|
||||
module: {
|
||||
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
||||
preLoaders: [
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'eslint',
|
||||
enforce: 'pre',
|
||||
test: /\.(js|vue)$/,
|
||||
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$/,
|
||||
loader: 'vue'
|
||||
use: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
loader: 'babel',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules\/(?!tributejs)/
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json'
|
||||
exclude: /node_modules\/(?!tributejs)/,
|
||||
use: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url',
|
||||
query: {
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: 'url',
|
||||
query: {
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
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: [
|
||||
|
|
|
@ -12,8 +12,9 @@ Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
|||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
},
|
||||
mode: 'development',
|
||||
// eval-source-map is faster for development
|
||||
devtool: '#eval-source-map',
|
||||
plugins: [
|
||||
|
@ -23,9 +24,7 @@ module.exports = merge(baseWebpackConfig, {
|
|||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
|
|
|
@ -4,7 +4,7 @@ var utils = require('./utils')
|
|||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
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 env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
|
@ -13,23 +13,23 @@ var env = process.env.NODE_ENV === 'testing'
|
|||
let commitHash = require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString();
|
||||
console.log(commitHash)
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
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,
|
||||
optimization: {
|
||||
minimize: true,
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
}
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
|
||||
},
|
||||
vue: {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true
|
||||
})
|
||||
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||
|
@ -38,14 +38,10 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||
'DEV_OVERRIDES': JSON.stringify(undefined)
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
// 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.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
|
@ -67,25 +63,11 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
chunksSortMode: 'dependency'
|
||||
}),
|
||||
// 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
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
chunks: ['vendor']
|
||||
})
|
||||
// new webpack.optimize.SplitChunksPlugin({
|
||||
// name: ['app', 'vendor']
|
||||
// }),
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
|
|
56
package.json
56
package.json
|
@ -11,7 +11,8 @@
|
|||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"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": {
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
|
@ -21,15 +22,13 @@
|
|||
"diff": "^3.0.1",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"localforage": "^1.5.0",
|
||||
"node-sass": "^3.10.1",
|
||||
"object-path": "^0.11.3",
|
||||
"phoenix": "^1.3.0",
|
||||
"popper.js": "^1.14.7",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"sass-loader": "^4.0.2",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"vue": "^2.5.13",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-compose": "^0.7.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-popperjs": "^2.0.3",
|
||||
"vue-router": "^3.0.1",
|
||||
|
@ -46,7 +45,7 @@
|
|||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"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-transform-runtime": "^6.0.0",
|
||||
"babel-plugin-transform-vue-jsx": "3",
|
||||
|
@ -59,52 +58,55 @@
|
|||
"chromedriver": "^2.21.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"cross-spawn": "^4.0.2",
|
||||
"css-loader": "^0.25.0",
|
||||
"eslint": "^3.7.1",
|
||||
"eslint-config-standard": "^6.1.0",
|
||||
"css-loader": "^0.28.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
"eslint-loader": "^1.5.0",
|
||||
"eslint-plugin-html": "^1.5.5",
|
||||
"eslint-plugin-promise": "^2.0.1",
|
||||
"eslint-plugin-standard": "^2.0.1",
|
||||
"eslint-loader": "^2.1.0",
|
||||
"eslint-plugin-import": "^2.13.0",
|
||||
"eslint-plugin-node": "^7.0.0",
|
||||
"eslint-plugin-promise": "^4.0.0",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.13.3",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^0.9.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"function-bind": "^1.0.2",
|
||||
"html-webpack-plugin": "^2.8.1",
|
||||
"html-webpack-plugin": "^3.0.0",
|
||||
"http-proxy-middleware": "^0.17.2",
|
||||
"inject-loader": "^2.0.1",
|
||||
"iso-639-1": "^2.0.3",
|
||||
"isparta-loader": "^2.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^1.3.0",
|
||||
"karma": "^3.0.0",
|
||||
"karma-coverage": "^1.1.1",
|
||||
"karma-mocha": "^1.2.0",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-sinon-chai": "^1.2.0",
|
||||
"karma-firefox-launcher": "^1.1.0",
|
||||
"karma-sinon-chai": "^2.0.2",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.26",
|
||||
"karma-webpack": "^1.7.0",
|
||||
"karma-webpack": "^4.0.0-rc.3",
|
||||
"lodash": "^4.16.4",
|
||||
"lolex": "^1.4.0",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"mocha": "^3.1.0",
|
||||
"nightwatch": "^0.9.8",
|
||||
"opn": "^4.0.2",
|
||||
"ora": "^0.3.0",
|
||||
"phantomjs-prebuilt": "^2.1.3",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass": "^1.17.3",
|
||||
"sass-loader": "git://github.com/webpack-contrib/sass-loader",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "^5.3.0",
|
||||
"serviceworker-webpack-plugin": "0.2.3",
|
||||
"serviceworker-webpack-plugin": "^1.0.0",
|
||||
"shelljs": "^0.7.4",
|
||||
"sinon": "^1.17.3",
|
||||
"sinon": "^2.1.0",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"url-loader": "^0.5.7",
|
||||
"vue-loader": "^11.1.0",
|
||||
"vue-style-loader": "^2.0.0",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dev-middleware": "^1.8.3",
|
||||
"url-loader": "^1.1.2",
|
||||
"vue-loader": "^14.0.0",
|
||||
"vue-style-loader": "^4.0.0",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-dev-middleware": "^3.6.0",
|
||||
"webpack-hot-middleware": "^2.12.2",
|
||||
"webpack-merge": "^0.14.1"
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@ import MediaModal from './components/media_modal/media_modal.vue'
|
|||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue'
|
||||
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 {
|
||||
|
@ -26,7 +27,8 @@ export default {
|
|||
MediaModal,
|
||||
SideDrawer,
|
||||
MobilePostStatusModal,
|
||||
MobileNav
|
||||
MobileNav,
|
||||
UserReportingModal
|
||||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline',
|
||||
|
|
93
src/App.scss
93
src/App.scss
|
@ -379,6 +379,7 @@ main-router {
|
|||
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
flex: none;
|
||||
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
|
||||
background-size: cover;
|
||||
|
@ -624,21 +625,6 @@ nav {
|
|||
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 {
|
||||
padding: .5em;
|
||||
border: 1px solid $fallback--faint;
|
||||
|
@ -647,6 +633,19 @@ nav {
|
|||
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 {
|
||||
from {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
|
@ -726,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 {
|
||||
text-align: center;
|
||||
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div v-if="" class="container" id="content">
|
||||
<div class="sidebar-flexer mobile-hidden" v-if="!isMobileLayout">
|
||||
<div class="container" id="content">
|
||||
<div class="sidebar-flexer mobile-hidden">
|
||||
<div class="sidebar-bounds">
|
||||
<div class="sidebar-scroller">
|
||||
<div class="sidebar">
|
||||
<user-panel></user-panel>
|
||||
<div v-if="!isMobileLayout">
|
||||
<nav-panel></nav-panel>
|
||||
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
|
||||
<features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
|
||||
|
@ -33,6 +34,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div v-if="!currentUser" class="login-hint panel panel-default">
|
||||
<router-link :to="{ name: 'login' }" class="panel-body">
|
||||
|
@ -46,6 +48,7 @@
|
|||
<media-modal></media-modal>
|
||||
</div>
|
||||
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
|
||||
<UserReportingModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import PublicAndExternalTimeline from 'components/public_and_external_timeline/p
|
|||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import TagTimeline from 'components/tag_timeline/tag_timeline.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 UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Settings from 'components/settings/settings.vue'
|
||||
|
@ -34,7 +34,7 @@ export default (store) => {
|
|||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ 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: 'settings', path: '/settings', component: Settings },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
|
|
52
src/components/autosuggest/autosuggest.js
Normal file
52
src/components/autosuggest/autosuggest.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
45
src/components/autosuggest/autosuggest.vue
Normal file
45
src/components/autosuggest/autosuggest.vue
Normal file
|
@ -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>
|
21
src/components/avatar_list/avatar_list.js
Normal file
21
src/components/avatar_list/avatar_list.js
Normal file
|
@ -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
|
38
src/components/avatar_list/avatar_list.vue
Normal file
38
src/components/avatar_list/avatar_list.vue
Normal file
|
@ -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>
|
||||
<div class="basic-user-card">
|
||||
<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>
|
||||
<div class="basic-user-card-expanded-content" v-if="userExpanded">
|
||||
<UserCard :user="user" :rounded="true" :bordered="true"/>
|
||||
|
@ -24,19 +28,11 @@
|
|||
<script src="./basic_user_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.basic-user-card {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
margin: 0;
|
||||
padding-top: 0.6em;
|
||||
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);
|
||||
padding: 0.6em 1em;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
|
@ -52,15 +48,16 @@
|
|||
width: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
&-user-name-value,
|
||||
&-screen-name {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&-expanded-content {
|
||||
flex: 1;
|
||||
|
|
75
src/components/checkbox/checkbox.vue
Normal file
75
src/components/checkbox/checkbox.vue
Normal file
|
@ -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 { set } from 'vue'
|
||||
import { reduce, filter, findIndex, clone } from 'lodash'
|
||||
import Status from '../status/status.vue'
|
||||
|
||||
const sortById = (a, b) => {
|
||||
|
@ -36,14 +35,14 @@ const conversation = {
|
|||
data () {
|
||||
return {
|
||||
highlight: null,
|
||||
expanded: false,
|
||||
converationStatusIds: []
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
'collapsable',
|
||||
'isPage'
|
||||
'isPage',
|
||||
'showPinned'
|
||||
],
|
||||
created () {
|
||||
if (this.isPage) {
|
||||
|
@ -54,15 +53,6 @@ const conversation = {
|
|||
status () {
|
||||
return this.statusoid
|
||||
},
|
||||
idsToShow () {
|
||||
if (this.converationStatusIds.length > 0) {
|
||||
return this.converationStatusIds
|
||||
} else if (this.statusId) {
|
||||
return [this.statusId]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
},
|
||||
statusId () {
|
||||
if (this.statusoid.retweeted_status) {
|
||||
return this.statusoid.retweeted_status.id
|
||||
|
@ -70,6 +60,13 @@ const conversation = {
|
|||
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 () {
|
||||
if (!this.status) {
|
||||
return []
|
||||
|
@ -79,12 +76,7 @@ const conversation = {
|
|||
return [this.status]
|
||||
}
|
||||
|
||||
const statusesObject = this.$store.state.statuses.allStatusesObject
|
||||
const conversation = this.idsToShow.reduce((acc, id) => {
|
||||
acc.push(statusesObject[id])
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
|
||||
const statusIndex = findIndex(conversation, { id: this.statusId })
|
||||
if (statusIndex !== -1) {
|
||||
conversation[statusIndex] = this.status
|
||||
|
@ -131,10 +123,6 @@ const conversation = {
|
|||
.then(({ancestors, descendants}) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
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))
|
||||
} else {
|
||||
|
@ -152,6 +140,7 @@ const conversation = {
|
|||
},
|
||||
setHighlight (id) {
|
||||
this.highlight = id
|
||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
},
|
||||
getHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
|
|
|
@ -11,9 +11,10 @@
|
|||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
:key="status.id"
|
||||
:inlineExpanded="collapsable"
|
||||
:inlineExpanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable='!isExpanded'
|
||||
:showPinned="showPinned"
|
||||
:focused="focused(status.id)"
|
||||
:inConversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
|
|
@ -1,21 +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 () {
|
||||
if (!this.currentUser) { return }
|
||||
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
return superuser || 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>
|
48
src/components/exporter/exporter.js
Normal file
48
src/components/exporter/exporter.js
Normal file
|
@ -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
|
20
src/components/exporter/exporter.vue
Normal file
20
src/components/exporter/exporter.vue
Normal file
|
@ -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>
|
64
src/components/extra_buttons/extra_buttons.js
Normal file
64
src/components/extra_buttons/extra_buttons.js
Normal file
|
@ -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
|
48
src/components/extra_buttons/extra_buttons.vue
Normal file
48
src/components/extra_buttons/extra_buttons.vue
Normal file
|
@ -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>
|
|
@ -10,8 +10,7 @@ const FollowCard = {
|
|||
data () {
|
||||
return {
|
||||
inProgress: false,
|
||||
requestSent: false,
|
||||
updated: false
|
||||
requestSent: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -19,10 +18,8 @@ const FollowCard = {
|
|||
RemoteFollow
|
||||
},
|
||||
computed: {
|
||||
isMe () { return this.$store.state.users.currentUser.id === this.user.id },
|
||||
following () { return this.updated ? this.updated.following : this.user.following },
|
||||
showFollow () {
|
||||
return !this.following || this.updated && !this.updated.following
|
||||
isMe () {
|
||||
return this.$store.state.users.currentUser.id === this.user.id
|
||||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
|
@ -31,17 +28,15 @@ const FollowCard = {
|
|||
methods: {
|
||||
followUser () {
|
||||
this.inProgress = true
|
||||
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
|
||||
requestFollow(this.user, this.$store).then(({ sent }) => {
|
||||
this.inProgress = false
|
||||
this.requestSent = sent
|
||||
this.updated = updated
|
||||
})
|
||||
},
|
||||
unfollowUser () {
|
||||
this.inProgress = true
|
||||
requestUnfollow(this.user, this.$store).then(({ updated }) => {
|
||||
requestUnfollow(this.user, this.$store).then(() => {
|
||||
this.inProgress = false
|
||||
this.updated = updated
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
<span class="faint" v-if="!noFollowsYou && user.follows_you">
|
||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</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" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="showFollow && loggedIn"
|
||||
v-if="!user.following"
|
||||
class="btn btn-default follow-card-follow-button"
|
||||
@click="followUser"
|
||||
:disabled="inProgress"
|
||||
|
@ -24,7 +27,7 @@
|
|||
{{ $t('user_card.follow') }}
|
||||
</template>
|
||||
</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">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
|
@ -32,6 +35,7 @@
|
|||
{{ $t('user_card.follow_unfollow') }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{$t('nav.friend_requests')}}
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -70,22 +70,10 @@ const ImageCropper = {
|
|||
this.dataUrl = undefined
|
||||
this.$emit('close')
|
||||
},
|
||||
submit () {
|
||||
submit (cropping = true) {
|
||||
this.submitting = true
|
||||
this.avatarUploadError = null
|
||||
this.submitHandler(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)
|
||||
this.submitHandler(cropping && this.cropper, this.file)
|
||||
.then(() => this.destroy())
|
||||
.catch((err) => {
|
||||
this.submitError = err
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
|
||||
</div>
|
||||
<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="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>
|
||||
</div>
|
||||
<div class="alert error" v-if="submitError">
|
||||
|
|
53
src/components/importer/importer.js
Normal file
53
src/components/importer/importer.js
Normal file
|
@ -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
|
28
src/components/importer/importer.vue
Normal file
28
src/components/importer/importer.vue
Normal file
|
@ -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>
|
25
src/components/interactions/interactions.js
Normal file
25
src/components/interactions/interactions.js
Normal file
|
@ -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
|
25
src/components/interactions/interactions.vue
Normal file
25
src/components/interactions/interactions.vue
Normal file
|
@ -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 () {
|
||||
return _.map(this.languageCodes, ISO6391.getName)
|
||||
return _.map(this.languageCodes, this.getLanguageName)
|
||||
},
|
||||
|
||||
language: {
|
||||
|
@ -36,6 +36,17 @@
|
|||
this.$i18n.locale = val
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
const specialLanguageNames = {
|
||||
'ja': 'Japanese (やさしいにほんご)',
|
||||
'ja_pedantic': 'Japanese (日本語)',
|
||||
'zh': 'Chinese (简体中文)'
|
||||
}
|
||||
return specialLanguageNames[code] || ISO6391.getName(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
42
src/components/list/list.vue
Normal file
42
src/components/list/list.vue
Normal file
|
@ -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>
|
|
@ -50,6 +50,10 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.6em;
|
||||
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
width: 10em;
|
||||
|
@ -66,9 +70,30 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
.error {
|
||||
text-align: center;
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.media-modal-view {
|
||||
z-index: 1001;
|
||||
|
||||
&:hover {
|
||||
.modal-view-button-arrow {
|
||||
opacity: 0.75;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
|
||||
<script src="./mentions.js"></script>
|
||||
|
|
|
@ -63,6 +63,11 @@ const MobileNav = {
|
|||
},
|
||||
markNotificationsAsSeen () {
|
||||
this.$refs.notifications.markAsSeen()
|
||||
},
|
||||
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||
if (this.$store.state.config.autoLoad && scrollTop + clientHeight >= scrollHeight) {
|
||||
this.$refs.notifications.fetchOlderNotifications()
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<nav class='nav-bar container' id="nav">
|
||||
<div class='mobile-inner-nav' @click="scrollToTop()">
|
||||
<div class='item'>
|
||||
|
@ -14,12 +15,12 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<SideDrawer ref="sideDrawer" :logout="logout"/>
|
||||
</nav>
|
||||
<div v-if="currentUser"
|
||||
class="mobile-notifications-drawer"
|
||||
:class="{ 'closed': !notificationsOpen }"
|
||||
@touchstart="notificationsTouchStart"
|
||||
@touchmove="notificationsTouchMove"
|
||||
@touchstart.stop="notificationsTouchStart"
|
||||
@touchmove.stop="notificationsTouchMove"
|
||||
>
|
||||
<div class="mobile-notifications-header">
|
||||
<span class="title">{{$t('notifications.notifications')}}</span>
|
||||
|
@ -27,12 +28,13 @@
|
|||
<i class="button-icon icon-cancel"/>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="currentUser" class="mobile-notifications">
|
||||
<Notifications ref="notifications" noHeading="true"/>
|
||||
<div class="mobile-notifications" @scroll="onScroll">
|
||||
<Notifications ref="notifications" :noHeading="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<SideDrawer ref="sideDrawer" :logout="logout"/>
|
||||
<MobilePostStatusModal />
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mobile_nav.js"></script>
|
||||
|
@ -79,6 +81,8 @@
|
|||
transition-property: transform;
|
||||
transition-duration: 0.25s;
|
||||
transform: translateX(0);
|
||||
z-index: 1001;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&.closed {
|
||||
transform: translateX(100%);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const MobilePostStatusModal = {
|
||||
components: {
|
||||
|
@ -16,11 +16,15 @@ const MobilePostStatusModal = {
|
|||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
if (this.autohideFloatingPostButton) {
|
||||
this.activateFloatingPostButtonAutohide()
|
||||
}
|
||||
window.addEventListener('resize', this.handleOSK)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
if (this.autohideFloatingPostButton) {
|
||||
this.deactivateFloatingPostButtonAutohide()
|
||||
}
|
||||
window.removeEventListener('resize', this.handleOSK)
|
||||
},
|
||||
computed: {
|
||||
|
@ -28,10 +32,30 @@ const MobilePostStatusModal = {
|
|||
return this.$store.state.users.currentUser
|
||||
},
|
||||
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: {
|
||||
activateFloatingPostButtonAutohide () {
|
||||
window.addEventListener('scroll', this.handleScrollStart)
|
||||
window.addEventListener('scroll', this.handleScrollEnd)
|
||||
},
|
||||
deactivateFloatingPostButtonAutohide () {
|
||||
window.removeEventListener('scroll', this.handleScrollStart)
|
||||
window.removeEventListener('scroll', this.handleScrollEnd)
|
||||
},
|
||||
openPostForm () {
|
||||
this.postFormOpen = true
|
||||
this.hidden = true
|
||||
|
@ -65,26 +89,19 @@ const MobilePostStatusModal = {
|
|||
this.inputActive = false
|
||||
}
|
||||
},
|
||||
handleScroll: throttle(function () {
|
||||
const scrollAmount = window.scrollY - this.oldScrollPos
|
||||
const scrollingDown = scrollAmount > 0
|
||||
|
||||
if (scrollingDown !== this.scrollingDown) {
|
||||
this.amountScrolled = 0
|
||||
this.scrollingDown = scrollingDown
|
||||
if (!scrollingDown) {
|
||||
handleScrollStart: debounce(function () {
|
||||
if (window.scrollY > this.oldScrollPos) {
|
||||
this.hidden = true
|
||||
} else {
|
||||
this.hidden = false
|
||||
}
|
||||
} else if (scrollingDown) {
|
||||
this.amountScrolled += scrollAmount
|
||||
if (this.amountScrolled > 100 && !this.hidden) {
|
||||
this.hidden = true
|
||||
}
|
||||
}
|
||||
|
||||
this.oldScrollPos = window.scrollY
|
||||
this.scrollingDown = scrollingDown
|
||||
}, 100)
|
||||
}, 100, {leading: true, trailing: false}),
|
||||
|
||||
handleScrollEnd: debounce(function () {
|
||||
this.hidden = false
|
||||
this.oldScrollPos = window.scrollY
|
||||
}, 100, {leading: false, trailing: true})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -86,56 +86,6 @@
|
|||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
|
||||
.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%;
|
||||
|
||||
&:hover {
|
||||
// TODO: improve the look on breeze themes
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-checkbox {
|
||||
float: right;
|
||||
min-width: 22px;
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
</router-link>
|
||||
</li>
|
||||
<li v-if='currentUser'>
|
||||
<router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("nav.mentions") }}
|
||||
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("nav.interactions") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if='currentUser'>
|
||||
|
|
|
@ -23,26 +23,26 @@ const Notification = {
|
|||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
getUser (notification) {
|
||||
return this.$store.state.users.usersObject[notification.action.user.id]
|
||||
return this.$store.state.users.usersObject[notification.from_profile.id]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userClass () {
|
||||
return highlightClass(this.notification.action.user)
|
||||
return highlightClass(this.notification.from_profile)
|
||||
},
|
||||
userStyle () {
|
||||
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])
|
||||
},
|
||||
userInStore () {
|
||||
return this.$store.getters.findUser(this.notification.action.user.id)
|
||||
return this.$store.getters.findUser(this.notification.from_profile.id)
|
||||
},
|
||||
user () {
|
||||
if (this.userInStore) {
|
||||
return this.userInStore
|
||||
}
|
||||
return {}
|
||||
return this.notification.from_profile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<template>
|
||||
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
||||
<status
|
||||
v-if="notification.type === 'mention'"
|
||||
:compact="true"
|
||||
:statusoid="notification.status"
|
||||
>
|
||||
</status>
|
||||
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else>
|
||||
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
|
||||
<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>
|
||||
<div class='notification-right'>
|
||||
<UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />
|
||||
<span class="notification-details">
|
||||
<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-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</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.from_profile.screen_name">{{ notification.from_profile.name }}</span>
|
||||
<span v-if="notification.type === 'like'">
|
||||
<i class="fa icon-star lit"></i>
|
||||
<small>{{$t('notifications.favorited_you')}}</small>
|
||||
|
@ -23,19 +28,24 @@
|
|||
<small>{{$t('notifications.followed_you')}}</small>
|
||||
</span>
|
||||
</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">
|
||||
<timeago :since="notification.action.created_at" :auto-update="240"></timeago>
|
||||
<timeago :since="notification.created_at" :auto-update="240"></timeago>
|
||||
</router-link>
|
||||
</div>
|
||||
</span>
|
||||
<div class="follow-text" v-if="notification.type === 'follow'">
|
||||
<router-link :to="userProfileLink(notification.action.user)">
|
||||
@{{notification.action.user.screen_name}}
|
||||
<router-link :to="userProfileLink(notification.from_profile)">
|
||||
@{{notification.from_profile.screen_name}}
|
||||
</router-link>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,15 +7,24 @@ import {
|
|||
} from '../../services/notification_utils/notification_utils.js'
|
||||
|
||||
const Notifications = {
|
||||
props: [
|
||||
'noHeading'
|
||||
],
|
||||
props: {
|
||||
// Disables display of panel header
|
||||
noHeading: Boolean,
|
||||
// Disables panel styles, unread mark, potentially other notification-related actions
|
||||
// meant for "Interactions" timeline
|
||||
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 () {
|
||||
return {
|
||||
bottomedOut: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mainClass () {
|
||||
return this.minimalMode ? '' : 'panel panel-default'
|
||||
},
|
||||
notifications () {
|
||||
return notificationsFromStore(this.$store)
|
||||
},
|
||||
|
@ -26,7 +35,7 @@ const Notifications = {
|
|||
return unseenNotificationsFromStore(this.$store)
|
||||
},
|
||||
visibleNotifications () {
|
||||
return visibleNotificationsFromStore(this.$store)
|
||||
return visibleNotificationsFromStore(this.$store, this.filterMode)
|
||||
},
|
||||
unseenCount () {
|
||||
return this.unseenNotifications.length
|
||||
|
@ -49,9 +58,13 @@ const Notifications = {
|
|||
},
|
||||
methods: {
|
||||
markAsSeen () {
|
||||
this.$store.dispatch('markNotificationsAsSeen', this.visibleNotifications)
|
||||
this.$store.dispatch('markNotificationsAsSeen')
|
||||
},
|
||||
fetchOlderNotifications () {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
store.commit('setNotificationsLoading', { value: true })
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.notifications {
|
||||
&:not(.minimal) {
|
||||
// a bit of a hack to allow scrolling below notifications
|
||||
padding-bottom: 15em;
|
||||
}
|
||||
|
||||
.loadmore-error {
|
||||
color: $fallback--text;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="notifications">
|
||||
<div class="panel panel-default">
|
||||
<div :class="{ minimal: minimalMode }" class="notifications">
|
||||
<div :class="mainClass">
|
||||
<div v-if="!noHeading" class="panel-heading">
|
||||
<div class="title">
|
||||
{{$t('notifications.notifications')}}
|
||||
|
@ -12,7 +12,7 @@
|
|||
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
|
||||
</div>
|
||||
<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>
|
||||
<notification :notification="notification"></notification>
|
||||
</div>
|
||||
|
@ -22,7 +22,9 @@
|
|||
{{$t('notifications.no_more_notifications')}}
|
||||
</div>
|
||||
<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>
|
||||
<div v-else class="new-status-notification text-center panel-footer">
|
||||
<i class="icon-spin3 animate-spin"/>
|
||||
|
|
|
@ -27,7 +27,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
handlePollUpdate (poll) {
|
||||
this.poll = poll
|
||||
// this.poll = poll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<div class="votes">
|
||||
<div
|
||||
class="poll-option"
|
||||
v-for="(pollOption, index) in poll.votes"
|
||||
v-for="(pollOption, index) in poll.options"
|
||||
:key="index">
|
||||
<div class="col">{{percentageForOption(pollOption.count)}}%</div>
|
||||
<div class="col">{{pollOption.name}}</div>
|
||||
<div class="col"><progress :max="totalVotesCount" :value="pollOption.count"></progress></div>
|
||||
<div class="col">{{percentageForOption(pollOption.votes_count)}}%</div>
|
||||
<div class="col">{{pollOption.title}}</div>
|
||||
<div class="col"><progress :max="totalVotesCount" :value="pollOption.votes_count"></progress></div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
|
@ -25,9 +25,12 @@
|
|||
export default {
|
||||
name: 'PollResults',
|
||||
props: ['poll'],
|
||||
created () {
|
||||
console.log(this.poll)
|
||||
},
|
||||
computed: {
|
||||
totalVotesCount () {
|
||||
return this.poll.votes.reduce((acc, vote) => { return acc + vote.count }, 0)
|
||||
return this.poll.votes_count
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -36,6 +39,7 @@ export default {
|
|||
},
|
||||
async fetchPoll (pollID) {
|
||||
const poll = await this.$store.state.api.backendInteractor.fetchPoll(pollID)
|
||||
console.log(poll)
|
||||
this.$emit('poll-refreshed', poll)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :id="pollVoteId" class="poll-vote" v-bind:class="containerClass">
|
||||
<div class="poll-vote" v-bind:class="containerClass">
|
||||
<div
|
||||
class="poll-choice"
|
||||
v-for="(pollOption, index) in poll.options"
|
||||
|
@ -10,7 +10,8 @@
|
|||
:id="optionID(index)"
|
||||
:value="pollOption.title"
|
||||
name="choice"
|
||||
@change="onChoice">
|
||||
v-model="checks[index]"
|
||||
>
|
||||
<label :for="optionID(index)">{{pollOption.title}}</label>
|
||||
</div>
|
||||
<button class="btn btn-default poll-vote-button" @click="onVote">{{$t('polls.vote')}}</button>
|
||||
|
@ -24,7 +25,7 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
choices: []
|
||||
checks: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -32,23 +33,17 @@ export default {
|
|||
return {
|
||||
loading: this.loading
|
||||
}
|
||||
},
|
||||
pollVoteId: function () {
|
||||
return `poll-vote-${this.poll.id}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
optionID (index) {
|
||||
return `pollOption${this.poll.id}#${index}`
|
||||
},
|
||||
async onChoice (e) {
|
||||
// TODO
|
||||
},
|
||||
async onVote () {
|
||||
this.loading = true
|
||||
|
||||
const pollID = this.poll.id
|
||||
const poll = await this.$store.state.api.backendInteractor.vote(pollID, this.choices)
|
||||
const choices = this.checks.filter(_=>_).map((entry, index) => index)
|
||||
const poll = await this.$store.state.api.backendInteractor.vote(this.poll.id, choices)
|
||||
|
||||
this.loading = false
|
||||
this.$emit('user-has-voted', poll)
|
||||
|
|
|
@ -68,3 +68,60 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -190,7 +190,10 @@ const PostStatusForm = {
|
|||
return this.$store.state.instance.safeDM
|
||||
},
|
||||
pollsAvailable () {
|
||||
return this.$store.state.instance.pollsAvailable
|
||||
return true // this.$store.state.instance.pollsAvailable
|
||||
},
|
||||
hideScopeNotice () {
|
||||
return this.$store.state.config.hideScopeNotice
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -354,6 +357,9 @@ const PostStatusForm = {
|
|||
},
|
||||
togglePollForm () {
|
||||
this.pollFormVisible = !this.pollFormVisible
|
||||
},
|
||||
dismissScopeNotice () {
|
||||
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,25 @@
|
|||
class="visibility-notice">
|
||||
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
|
||||
</i18n>
|
||||
<p v-if="newStatus.visibility === 'direct'" class="visibility-notice">
|
||||
<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>
|
||||
|
@ -40,7 +58,7 @@
|
|||
>
|
||||
</textarea>
|
||||
<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">
|
||||
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
|
||||
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
|
||||
|
@ -49,7 +67,7 @@
|
|||
</select>
|
||||
<i class="icon-down-open"></i>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<scope-selector
|
||||
:showAll="showAllScopes"
|
||||
|
@ -142,10 +160,11 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row-reverse;
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-status-form, .login {
|
||||
.post-status-form {
|
||||
.form-bottom {
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
|
@ -240,7 +259,7 @@
|
|||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.3em 0.5em 0.6em;
|
||||
padding: 0.25em 0.5em 0.5em;
|
||||
line-height:24px;
|
||||
}
|
||||
|
||||
|
|
35
src/components/progress_button/progress_button.vue
Normal file
35
src/components/progress_button/progress_button.vue
Normal file
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="!showNothing">
|
||||
<div v-if="!showNothing" class="scope-selector">
|
||||
<i class="icon-mail-alt"
|
||||
:class="css.direct"
|
||||
:title="$t('post_status.scope.direct')"
|
||||
|
@ -28,3 +28,19 @@
|
|||
</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>
|
||||
|
|
66
src/components/selectable_list/selectable_list.js
Normal file
66
src/components/selectable_list/selectable_list.js
Normal file
|
@ -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
|
63
src/components/selectable_list/selectable_list.vue
Normal file
63
src/components/selectable_list/selectable_list.vue
Normal file
|
@ -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,
|
||||
pauseOnUnfocusedLocal: user.pauseOnUnfocused,
|
||||
hoverPreviewLocal: user.hoverPreview,
|
||||
autohideFloatingPostButtonLocal: user.autohideFloatingPostButton,
|
||||
|
||||
hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
|
||||
? instance.hideMutedPosts
|
||||
|
@ -183,6 +184,9 @@ const settings = {
|
|||
hoverPreviewLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
|
||||
},
|
||||
autohideFloatingPostButtonLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'autohideFloatingPostButton', value })
|
||||
},
|
||||
muteWordsString (value) {
|
||||
value = filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
this.$store.dispatch('setOption', { name: 'muteWords', value })
|
||||
|
|
|
@ -42,9 +42,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
|
||||
<label for="collapseMessageWithSubject">
|
||||
{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}
|
||||
</label>
|
||||
<label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="streaming" v-model="streamingLocal">
|
||||
|
@ -124,6 +122,10 @@
|
|||
{{$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>
|
||||
</div>
|
||||
|
||||
|
@ -301,69 +303,3 @@
|
|||
|
||||
<script src="./settings.js">
|
||||
</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>
|
||||
|
|
|
@ -26,6 +26,11 @@
|
|||
{{ $t("nav.dms") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("nav.interactions") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||
import DeleteButton from '../delete_button/delete_button.vue'
|
||||
import Poll from '../poll/poll.vue'
|
||||
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Gallery from '../gallery/gallery.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 fileType from 'src/services/file_type/file_type.service'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.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 = {
|
||||
name: 'Status',
|
||||
|
@ -26,18 +27,19 @@ const Status = {
|
|||
'replies',
|
||||
'isPreview',
|
||||
'noHeading',
|
||||
'inlineExpanded'
|
||||
'inlineExpanded',
|
||||
'showPinned'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
replying: false,
|
||||
expanded: false,
|
||||
unmuted: false,
|
||||
userExpanded: false,
|
||||
preview: null,
|
||||
showPreview: false,
|
||||
showingTall: this.inConversation && this.focused,
|
||||
showingLongSubject: false,
|
||||
error: null,
|
||||
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
|
||||
? !this.$store.state.instance.collapseMessageWithSubject
|
||||
: !this.$store.state.config.collapseMessageWithSubject,
|
||||
|
@ -98,6 +100,10 @@ const Status = {
|
|||
return this.statusoid
|
||||
}
|
||||
},
|
||||
statusFromGlobalRepository () {
|
||||
// NOTE: Consider to replace status with statusFromGlobalRepository
|
||||
return this.$store.state.statuses.allStatusesObject[this.status.id]
|
||||
},
|
||||
loggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
|
@ -157,7 +163,7 @@ const Status = {
|
|||
if (this.$store.state.config.replyVisibility === 'all') {
|
||||
return false
|
||||
}
|
||||
if (this.inlineExpanded || this.expanded || this.inConversation || !this.isReply) {
|
||||
if (this.inConversation || !this.isReply) {
|
||||
return false
|
||||
}
|
||||
if (this.status.user.id === this.$store.state.users.currentUser.id) {
|
||||
|
@ -171,7 +177,7 @@ const Status = {
|
|||
if (this.status.user.id === this.status.attentions[i].id) {
|
||||
continue
|
||||
}
|
||||
if (checkFollowing && this.status.attentions[i].following) {
|
||||
if (checkFollowing && this.$store.getters.findUser(this.status.attentions[i].id).following) {
|
||||
return false
|
||||
}
|
||||
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
|
||||
|
@ -258,19 +264,34 @@ const Status = {
|
|||
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: {
|
||||
Attachment,
|
||||
FavoriteButton,
|
||||
RetweetButton,
|
||||
DeleteButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
Poll,
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
Gallery,
|
||||
LinkPreview
|
||||
LinkPreview,
|
||||
AvatarList
|
||||
},
|
||||
methods: {
|
||||
visibilityIcon (visibility) {
|
||||
|
@ -285,6 +306,12 @@ const Status = {
|
|||
return 'icon-globe'
|
||||
}
|
||||
},
|
||||
showError (error) {
|
||||
this.error = error
|
||||
},
|
||||
clearError () {
|
||||
this.error = undefined
|
||||
},
|
||||
linkClicked (event) {
|
||||
let { target } = event
|
||||
if (target.tagName === 'SPAN') {
|
||||
|
@ -315,6 +342,7 @@ const Status = {
|
|||
}
|
||||
},
|
||||
toggleReplying () {
|
||||
console.log(this.status)
|
||||
this.replying = !this.replying
|
||||
},
|
||||
gotoOriginal (id) {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<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">
|
||||
<div class="media status container muted">
|
||||
<small>
|
||||
|
@ -12,8 +16,12 @@
|
|||
</div>
|
||||
</template>
|
||||
<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">
|
||||
<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">
|
||||
<span class="user-name">
|
||||
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
|
||||
|
@ -24,10 +32,10 @@
|
|||
</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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="status-body">
|
||||
|
@ -91,8 +99,13 @@
|
|||
</div>
|
||||
|
||||
<div v-if="showPreview" class="status-preview-container">
|
||||
<status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status>
|
||||
<div class="status-preview status-preview-loading" v-else>
|
||||
<status class="status-preview"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -137,19 +150,37 @@
|
|||
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
|
||||
</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="loggedIn">
|
||||
<i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-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 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"/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -179,6 +210,13 @@ $status-margin: 0.75em;
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.status-pin {
|
||||
padding: $status-margin $status-margin 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status-preview {
|
||||
position: absolute;
|
||||
max-width: 95%;
|
||||
|
@ -222,7 +260,6 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.status-el {
|
||||
hyphens: auto;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
@ -551,15 +588,13 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.icon-reply:hover {
|
||||
.button-icon.icon-reply {
|
||||
&:not(.button-icon-disabled):hover,
|
||||
&.button-icon-active {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-reply.icon-reply-active {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
}
|
||||
|
||||
.status:hover .animated.avatar {
|
||||
|
@ -599,16 +634,11 @@ a.unmute {
|
|||
margin-left: auto;
|
||||
}
|
||||
|
||||
.reply-left {
|
||||
flex: 0;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.reply-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline > {
|
||||
.timeline :not(.panel-disabled) > {
|
||||
.status-el:last-child {
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
|
@ -616,6 +646,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) {
|
||||
.status-el {
|
||||
.retweet-info {
|
||||
|
|
|
@ -4,15 +4,18 @@ import './tab_switcher.scss'
|
|||
|
||||
export default Vue.component('tab-switcher', {
|
||||
name: 'TabSwitcher',
|
||||
props: ['renderOnlyFocused'],
|
||||
props: ['renderOnlyFocused', 'onSwitch'],
|
||||
data () {
|
||||
return {
|
||||
active: this.$slots.default.findIndex(_ => _.tag)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activateTab (index) {
|
||||
activateTab (index, dataset) {
|
||||
return () => {
|
||||
if (typeof this.onSwitch === 'function') {
|
||||
this.onSwitch.call(null, index, this.$slots.default[index].elm.dataset)
|
||||
}
|
||||
this.active = index
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +40,11 @@ export default Vue.component('tab-switcher', {
|
|||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ import StillImage from '../still-image/still-image.vue'
|
|||
|
||||
const UserAvatar = {
|
||||
props: [
|
||||
'src',
|
||||
'user',
|
||||
'betterShadow',
|
||||
'compact'
|
||||
],
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<StillImage
|
||||
class="avatar"
|
||||
:alt="user.screen_name"
|
||||
:title="user.screen_name"
|
||||
:src="user.profile_image_url_original"
|
||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||
:src="imgSrc"
|
||||
:imageLoadError="imageLoadError"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -151,6 +151,9 @@ export default {
|
|||
},
|
||||
userProfileLink (user) {
|
||||
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='container'>
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/>
|
||||
<UserAvatar :betterShadow="betterShadow" :user="user"/>
|
||||
</router-link>
|
||||
<div class="name-and-screen-name">
|
||||
<div class="user-summary">
|
||||
<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-else>{{user.name}}</div>
|
||||
<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>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||
<span class="handle">@{{user.screen_name}}
|
||||
<div class="bottom-line">
|
||||
<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><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>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-meta">
|
||||
|
@ -99,8 +99,14 @@
|
|||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'>
|
||||
</ModerationTools>
|
||||
<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>
|
||||
|
@ -162,7 +168,7 @@
|
|||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
.emoji {
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
@ -226,7 +232,7 @@
|
|||
opacity: .8;
|
||||
}
|
||||
|
||||
.name-and-screen-name {
|
||||
.user-summary {
|
||||
display: block;
|
||||
margin-left: 0.6em;
|
||||
text-align: left;
|
||||
|
@ -243,6 +249,7 @@
|
|||
vertical-align: middle;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
.top-line {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -263,15 +270,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user-screen-name {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
display: inline-block;
|
||||
.bottom-line {
|
||||
display: flex;
|
||||
font-weight: light;
|
||||
font-size: 15px;
|
||||
padding-right: 0.1em;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.user-screen-name {
|
||||
min-width: 1px;
|
||||
flex: 0 1 auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
}
|
||||
|
||||
.dailyAvg {
|
||||
min-width: 1px;
|
||||
|
@ -282,15 +293,9 @@
|
|||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
.handle {
|
||||
min-width: 1px;
|
||||
flex: 0 1 auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// TODO use proper colors
|
||||
.staff {
|
||||
flex: none;
|
||||
text-transform: capitalize;
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
|
|
|
@ -1,48 +1,38 @@
|
|||
import { compose } from 'vue-compose'
|
||||
import get from 'lodash/get'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import FollowCard from '../follow_card/follow_card.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 withList from '../../hocs/with_list/with_list'
|
||||
|
||||
const FollowerList = compose(
|
||||
withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'followers', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||
childPropName: 'entries',
|
||||
const FollowerList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'followerIds', []).map(id => $store.getters.findUser(id)),
|
||||
destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||
childPropName: 'items',
|
||||
additionalPropNames: ['userId']
|
||||
}),
|
||||
withList({ getEntryProps: user => ({ user }) })
|
||||
)(FollowCard)
|
||||
})(List)
|
||||
|
||||
const FriendList = compose(
|
||||
withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||
childPropName: 'entries',
|
||||
const FriendList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'friendIds', []).map(id => $store.getters.findUser(id)),
|
||||
destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||
childPropName: 'items',
|
||||
additionalPropNames: ['userId']
|
||||
}),
|
||||
withList({ getEntryProps: user => ({ user }) })
|
||||
)(FollowCard)
|
||||
})(List)
|
||||
|
||||
const UserProfile = {
|
||||
data () {
|
||||
return {
|
||||
error: false,
|
||||
fetchedUserId: null
|
||||
userId: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (!this.user.id) {
|
||||
this.fetchUserId()
|
||||
.then(() => this.startUp())
|
||||
} else {
|
||||
this.startUp()
|
||||
}
|
||||
const routeParams = this.$route.params
|
||||
this.load(routeParams.name || routeParams.id)
|
||||
},
|
||||
destroyed () {
|
||||
this.cleanUp()
|
||||
|
@ -57,26 +47,12 @@ const UserProfile = {
|
|||
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 () {
|
||||
return 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 () {
|
||||
if (this.userInStore) {
|
||||
return this.userInStore
|
||||
}
|
||||
return {}
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
isExternal () {
|
||||
return this.$route.name === 'external-user-profile'
|
||||
|
@ -89,22 +65,18 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
startFetchFavorites () {
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId: this.userId })
|
||||
}
|
||||
},
|
||||
fetchUserId () {
|
||||
let fetchPromise
|
||||
if (this.userId && !this.$route.params.name) {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userId)
|
||||
load (userNameOrId) {
|
||||
// Check if user data is already loaded in store
|
||||
const user = this.$store.getters.findUser(userNameOrId)
|
||||
if (user) {
|
||||
this.userId = user.id
|
||||
this.fetchTimelines()
|
||||
} else {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userName)
|
||||
this.$store.dispatch('fetchUser', userNameOrId)
|
||||
.then(({ id }) => {
|
||||
this.fetchedUserId = id
|
||||
this.userId = id
|
||||
this.fetchTimelines()
|
||||
})
|
||||
}
|
||||
return fetchPromise
|
||||
.catch((reason) => {
|
||||
const errorMessage = get(reason, 'error.error')
|
||||
if (errorMessage === 'No user with such user_id') { // Known error
|
||||
|
@ -115,15 +87,18 @@ const UserProfile = {
|
|||
this.error = this.$t('user_profile.profile_loading_error')
|
||||
}
|
||||
})
|
||||
.then(() => this.startUp())
|
||||
},
|
||||
startUp () {
|
||||
if (this.userId) {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId: this.userId })
|
||||
this.$store.dispatch('startFetchingTimeline', { 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 () {
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
|
@ -134,18 +109,16 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
// userId can be undefined if we don't know it yet
|
||||
userId (newVal) {
|
||||
'$route.params.id': function (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
this.load(newVal)
|
||||
}
|
||||
},
|
||||
userName () {
|
||||
if (this.$route.params.name) {
|
||||
this.fetchUserId()
|
||||
'$route.params.name': function (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
this.load(newVal)
|
||||
}
|
||||
},
|
||||
$route () {
|
||||
|
@ -157,7 +130,9 @@ const UserProfile = {
|
|||
Timeline,
|
||||
FollowerList,
|
||||
FriendList,
|
||||
ModerationTools
|
||||
ModerationTools,
|
||||
FollowCard,
|
||||
Conversation
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
<template>
|
||||
<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"/>
|
||||
<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
|
||||
:label="$t('user_card.statuses')"
|
||||
:disabled="!user.statuses_count"
|
||||
:count="user.statuses_count"
|
||||
:embedded="true"
|
||||
:title="$t('user_profile.timeline_title')"
|
||||
|
@ -13,11 +24,20 @@
|
|||
:timeline-name="'user'"
|
||||
:user-id="userId"
|
||||
/>
|
||||
</div>
|
||||
<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 :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>
|
||||
<Timeline
|
||||
:label="$t('user_card.media')"
|
||||
|
|
106
src/components/user_reporting_modal/user_reporting_modal.js
Normal file
106
src/components/user_reporting_modal/user_reporting_modal.js
Normal file
|
@ -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
|
157
src/components/user_reporting_modal/user_reporting_modal.vue
Normal file
157
src/components/user_reporting_modal/user_reporting_modal.vue
Normal file
|
@ -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 userSearchApi from '../../services/new_api/user_search.js'
|
||||
import map from 'lodash/map'
|
||||
|
||||
const userSearch = {
|
||||
components: {
|
||||
FollowCard
|
||||
|
@ -10,10 +11,15 @@ const userSearch = {
|
|||
data () {
|
||||
return {
|
||||
username: '',
|
||||
users: [],
|
||||
userIds: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
users () {
|
||||
return this.userIds.map(userId => this.$store.getters.findUser(userId))
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.search(this.query)
|
||||
},
|
||||
|
@ -33,10 +39,10 @@ const userSearch = {
|
|||
return
|
||||
}
|
||||
this.loading = true
|
||||
userSearchApi.search({query, store: this.$store})
|
||||
this.$store.dispatch('searchUsers', query)
|
||||
.then((res) => {
|
||||
this.loading = false
|
||||
this.users = res
|
||||
this.userIds = map(res, 'id')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<i class="icon-spin3 animate-spin"/>
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { compose } from 'vue-compose'
|
||||
import unescape from 'lodash/unescape'
|
||||
import get from 'lodash/get'
|
||||
import map from 'lodash/map'
|
||||
import reject from 'lodash/reject'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import ImageCropper from '../image_cropper/image_cropper.vue'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
|
@ -8,27 +9,26 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
|
|||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
import BlockCard from '../block_card/block_card.vue'
|
||||
import MuteCard from '../mute_card/mute_card.vue'
|
||||
import SelectableList from '../selectable_list/selectable_list.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import EmojiInput from '../emoji-input/emoji-input.vue'
|
||||
import Autosuggest from '../autosuggest/autosuggest.vue'
|
||||
import Importer from '../importer/importer.vue'
|
||||
import Exporter from '../exporter/exporter.vue'
|
||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||
import withList from '../../hocs/with_list/with_list'
|
||||
import userSearchApi from '../../services/new_api/user_search.js'
|
||||
|
||||
const BlockList = compose(
|
||||
withSubscription({
|
||||
const BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||
childPropName: 'entries'
|
||||
}),
|
||||
withList({ getEntryProps: userId => ({ userId }) })
|
||||
)(BlockCard)
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MuteList = compose(
|
||||
withSubscription({
|
||||
const MuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
childPropName: 'entries'
|
||||
}),
|
||||
withList({ getEntryProps: userId => ({ userId }) })
|
||||
)(MuteCard)
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const UserSettings = {
|
||||
data () {
|
||||
|
@ -42,14 +42,9 @@ const UserSettings = {
|
|||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
followList: null,
|
||||
followImportError: false,
|
||||
followsImported: false,
|
||||
enableFollowsExport: true,
|
||||
pickAvatarBtnVisible: true,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
followListUploading: false,
|
||||
bannerPreview: null,
|
||||
backgroundPreview: null,
|
||||
bannerUploadError: null,
|
||||
|
@ -60,7 +55,8 @@ const UserSettings = {
|
|||
changePasswordInputs: [ '', '', '' ],
|
||||
changedPassword: false,
|
||||
changePasswordError: false,
|
||||
activeTab: 'profile'
|
||||
activeTab: 'profile',
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -73,7 +69,13 @@ const UserSettings = {
|
|||
ImageCropper,
|
||||
BlockList,
|
||||
MuteList,
|
||||
EmojiInput
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
ProgressButton,
|
||||
Importer,
|
||||
Exporter
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
@ -108,39 +110,29 @@ const UserSettings = {
|
|||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
const name = this.newName
|
||||
const description = this.newBio
|
||||
const locked = this.newLocked
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
const default_scope = this.newDefaultScope
|
||||
const no_rich_text = this.newNoRichText
|
||||
const hide_follows = this.hideFollows
|
||||
const hide_followers = this.hideFollowers
|
||||
const show_role = this.showRole
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
params: {
|
||||
name,
|
||||
description,
|
||||
locked,
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
default_scope,
|
||||
no_rich_text,
|
||||
hide_follows,
|
||||
hide_followers,
|
||||
show_role
|
||||
display_name: this.newName,
|
||||
default_scope: this.newDefaultScope,
|
||||
no_rich_text: this.newNoRichText,
|
||||
hide_follows: this.hideFollows,
|
||||
hide_followers: this.hideFollowers,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
}}).then((user) => {
|
||||
if (!user.error) {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
}
|
||||
})
|
||||
},
|
||||
updateNotificationSettings () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateNotificationSettings({ settings: this.notificationSettings })
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
|
@ -158,23 +150,29 @@ const UserSettings = {
|
|||
reader.onload = ({target}) => {
|
||||
const img = target.result
|
||||
this[slot + 'Preview'] = img
|
||||
this[slot] = file
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar (cropper, file) {
|
||||
let img
|
||||
if (cropper) {
|
||||
img = cropper.getCroppedCanvas().toDataURL(file.type)
|
||||
} else {
|
||||
img = file
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar) {
|
||||
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
resolve()
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
|
||||
})
|
||||
}
|
||||
|
||||
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
|
||||
if (!user.error) {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
if (cropper) {
|
||||
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
||||
} else {
|
||||
throw new Error(this.$t('upload.error.base') + user.error)
|
||||
updateAvatar(file)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -184,30 +182,17 @@ const UserSettings = {
|
|||
submitBanner () {
|
||||
if (!this.bannerPreview) { return }
|
||||
|
||||
let banner = this.bannerPreview
|
||||
// eslint-disable-next-line no-undef
|
||||
let imginfo = new Image()
|
||||
/* eslint-disable camelcase */
|
||||
let offset_top, offset_left, width, height
|
||||
imginfo.src = banner
|
||||
width = imginfo.width
|
||||
height = imginfo.height
|
||||
offset_top = 0
|
||||
offset_left = 0
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => {
|
||||
if (!data.error) {
|
||||
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
||||
clone.cover_photo = data.url
|
||||
this.$store.commit('addNewUsers', [clone])
|
||||
this.$store.commit('setCurrentUser', clone)
|
||||
this.$store.state.api.backendInteractor.updateBanner({banner: this.banner})
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.bannerPreview = null
|
||||
} else {
|
||||
this.bannerUploadError = this.$t('upload.error.base') + data.error
|
||||
}
|
||||
this.bannerUploading = false
|
||||
})
|
||||
/* eslint-enable camelcase */
|
||||
.catch((err) => {
|
||||
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
|
||||
})
|
||||
.then(() => { this.bannerUploading = false })
|
||||
},
|
||||
submitBg () {
|
||||
if (!this.backgroundPreview) { return }
|
||||
|
@ -234,62 +219,41 @@ const UserSettings = {
|
|||
this.backgroundUploading = false
|
||||
})
|
||||
},
|
||||
importFollows () {
|
||||
this.followListUploading = true
|
||||
const followList = this.followList
|
||||
this.$store.state.api.backendInteractor.followImport({params: followList})
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows(file)
|
||||
.then((status) => {
|
||||
if (status) {
|
||||
this.followsImported = true
|
||||
} else {
|
||||
this.followImportError = true
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
this.followListUploading = false
|
||||
})
|
||||
},
|
||||
/* This function takes an Array of Users
|
||||
* and outputs a file with all the addresses for the user to download
|
||||
*/
|
||||
exportPeople (users, filename) {
|
||||
// Get all the friends addresses
|
||||
var UserAddresses = users.map(function (user) {
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks(file)
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent (users) {
|
||||
// Get addresses
|
||||
return users.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
user.screen_name += '@' + location.hostname
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
// Make the user download the file
|
||||
var fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(UserAddresses))
|
||||
fileToDownload.setAttribute('download', filename)
|
||||
fileToDownload.style.display = 'none'
|
||||
document.body.appendChild(fileToDownload)
|
||||
fileToDownload.click()
|
||||
document.body.removeChild(fileToDownload)
|
||||
},
|
||||
exportFollows () {
|
||||
this.enableFollowsExport = false
|
||||
this.$store.state.api.backendInteractor
|
||||
.exportFriends({
|
||||
id: this.$store.state.users.currentUser.id
|
||||
})
|
||||
.then((friendList) => {
|
||||
this.exportPeople(friendList, 'friends.csv')
|
||||
setTimeout(() => { this.enableFollowsExport = true }, 2000)
|
||||
})
|
||||
getFollowsContent () {
|
||||
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
followListChange () {
|
||||
// eslint-disable-next-line no-undef
|
||||
let formData = new FormData()
|
||||
formData.append('list', this.$refs.followlist.files[0])
|
||||
this.followList = formData
|
||||
},
|
||||
dismissImported () {
|
||||
this.followsImported = false
|
||||
this.followImportError = false
|
||||
getBlocksContent () {
|
||||
return this.$store.state.api.backendInteractor.fetchBlocks()
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
confirmDelete () {
|
||||
this.deletingAccount = true
|
||||
|
@ -334,6 +298,40 @@ const UserSettings = {
|
|||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
}
|
||||
},
|
||||
filterUnblockedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const user = this.$store.getters.findUser(userId)
|
||||
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const user = this.$store.getters.findUser(userId)
|
||||
return !user || user.muted || user.id === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
queryUserIds (query) {
|
||||
return userSearchApi.search({query, store: this.$store})
|
||||
.then((users) => {
|
||||
this.$store.dispatch('addNewUsers', users)
|
||||
return map(users, 'id')
|
||||
})
|
||||
},
|
||||
blockUsers (ids) {
|
||||
return this.$store.dispatch('blockUsers', ids)
|
||||
},
|
||||
unblockUsers (ids) {
|
||||
return this.$store.dispatch('unblockUsers', ids)
|
||||
},
|
||||
muteUsers (ids) {
|
||||
return this.$store.dispatch('muteUsers', ids)
|
||||
},
|
||||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
identity (value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,43 +167,110 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.notifications')" v-if="pleromaBackend">
|
||||
<div class="setting-item">
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{$t('settings.notification_setting')}}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<input type="checkbox" id="notification-setting-follows" v-model="notificationSettings.follows">
|
||||
<label for="notification-setting-follows">
|
||||
{{$t('settings.notification_setting_follows')}}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="notification-setting-followers" v-model="notificationSettings.followers">
|
||||
<label for="notification-setting-followers">
|
||||
{{$t('settings.notification_setting_followers')}}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="notification-setting-non-follows" v-model="notificationSettings.non_follows">
|
||||
<label for="notification-setting-non-follows">
|
||||
{{$t('settings.notification_setting_non_follows')}}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="notification-setting-non-followers" v-model="notificationSettings.non_followers">
|
||||
<label for="notification-setting-non-followers">
|
||||
{{$t('settings.notification_setting_non_followers')}}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>{{$t('settings.notification_mutes')}}</p>
|
||||
<p>{{$t('settings.notification_blocks')}}</p>
|
||||
<button class="btn btn-default" @click="updateNotificationSettings">{{$t('general.submit')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend">
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.follow_import')}}</h2>
|
||||
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
|
||||
<form>
|
||||
<input type="file" ref="followlist" v-on:change="followListChange" />
|
||||
</form>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
|
||||
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
|
||||
<div v-if="followsImported">
|
||||
<i class="icon-cross" @click="dismissImported"></i>
|
||||
<p>{{$t('settings.follows_imported')}}</p>
|
||||
<Importer :submitHandler="importFollows" :successMessage="$t('settings.follows_imported')" :errorMessage="$t('settings.follow_import_error')" />
|
||||
</div>
|
||||
<div v-else-if="followImportError">
|
||||
<i class="icon-cross" @click="dismissImported"></i>
|
||||
<p>{{$t('settings.follow_import_error')}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item" v-if="enableFollowsExport">
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.follow_export')}}</h2>
|
||||
<button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button>
|
||||
<Exporter :getContent="getFollowsContent" filename="friends.csv" :exportButtonLabel="$t('settings.follow_export_button')" />
|
||||
</div>
|
||||
<div class="setting-item" v-else>
|
||||
<h2>{{$t('settings.follow_export_processing')}}</h2>
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.block_import')}}</h2>
|
||||
<p>{{$t('settings.import_blocks_from_a_csv_file')}}</p>
|
||||
<Importer :submitHandler="importBlocks" :successMessage="$t('settings.blocks_imported')" :errorMessage="$t('settings.block_import_error')" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.block_export')}}</h2>
|
||||
<Exporter :getContent="getBlocksContent" filename="blocks.csv" :exportButtonLabel="$t('settings.block_export_button')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.blocks_tab')">
|
||||
<block-list :refresh="true">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest :filter="filterUnblockedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')">
|
||||
<BlockCard slot-scope="row" :userId="row.item"/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<BlockList :refresh="true" :getKey="identity">
|
||||
<template slot="header" slot-scope="{selected}">
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => blockUsers(selected)">
|
||||
{{ $t('user_card.block') }}
|
||||
<template slot="progress">{{ $t('user_card.block_progress') }}</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unblockUsers(selected)">
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template slot="progress">{{ $t('user_card.unblock_progress') }}</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="item" slot-scope="{item}"><BlockCard :userId="item" /></template>
|
||||
<template slot="empty">{{$t('settings.no_blocks')}}</template>
|
||||
</block-list>
|
||||
</BlockList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<mute-list :refresh="true">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest :filter="filterUnMutedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')">
|
||||
<MuteCard slot-scope="row" :userId="row.item"/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList :refresh="true" :getKey="identity">
|
||||
<template slot="header" slot-scope="{selected}">
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => muteUsers(selected)">
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">{{ $t('user_card.mute_progress') }}</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unmuteUsers(selected)">
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template slot="progress">{{ $t('user_card.unmute_progress') }}</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="item" slot-scope="{item}"><MuteCard :userId="item" /></template>
|
||||
<template slot="empty">{{$t('settings.no_mutes')}}</template>
|
||||
</mute-list>
|
||||
</MuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
|
@ -221,6 +288,10 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
input[type=file] {
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
|
@ -262,5 +333,19 @@
|
|||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&-usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&-bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{$t('who_to_follow.who_to_follow')}}
|
||||
</div>
|
||||
<div 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>
|
||||
</template>
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import map from 'lodash/map'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import './with_list.scss'
|
||||
|
||||
const defaultEntryPropsGetter = entry => ({ entry })
|
||||
const defaultKeyGetter = entry => entry.id
|
||||
|
||||
const withList = ({
|
||||
getEntryProps = defaultEntryPropsGetter, // function to accept entry and index values and return props to be passed into the item component
|
||||
getKey = defaultKeyGetter // funciton to accept entry and index values and return key prop value
|
||||
}) => (ItemComponent) => (
|
||||
Vue.component('withList', {
|
||||
props: [
|
||||
'entries', // array of entry
|
||||
'entryProps', // additional props to be passed into each entry
|
||||
'entryListeners' // additional event listeners to be passed into each entry
|
||||
],
|
||||
render (createElement) {
|
||||
return (
|
||||
<div class="with-list">
|
||||
{map(this.entries, (entry, index) => {
|
||||
const props = {
|
||||
key: getKey(entry, index),
|
||||
props: {
|
||||
...this.$props.entryProps,
|
||||
...getEntryProps(entry, index)
|
||||
},
|
||||
on: this.$props.entryListeners
|
||||
}
|
||||
return <ItemComponent {...props} />
|
||||
})}
|
||||
{isEmpty(this.entries) && this.$slots.empty && <div class="with-list-empty-content faint">{this.$slots.empty}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export default withList
|
|
@ -1,6 +0,0 @@
|
|||
.with-list {
|
||||
&-empty-content {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.with-load-more {
|
||||
&-footer {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-top: 1px solid;
|
||||
border-top-color: $fallback--border;
|
||||
border-top-color: var(--border, $fallback--border);
|
||||
|
||||
.error {
|
||||
font-size: 14px;
|
||||
|
|
|
@ -19,7 +19,7 @@ if (typeof arg === 'undefined') {
|
|||
console.log('')
|
||||
console.log('There are no other arguments or options. Make an issue if you encounter a bug or want')
|
||||
console.log('some feature to be implemented. Merge requests are welcome as well.')
|
||||
return
|
||||
process.exit()
|
||||
}
|
||||
|
||||
const english = require('./en.json')
|
|
@ -73,7 +73,8 @@
|
|||
"content_type": {
|
||||
"text/plain": "Prostý text",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Předmět (volitelný)",
|
||||
"default": "Právě jsem přistál v L.A.",
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
"chat": {
|
||||
"title": "Chat"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Export",
|
||||
"processing": "Processing, you'll soon be asked to download your file"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "Chat",
|
||||
"gopher": "Gopher",
|
||||
|
@ -31,6 +35,11 @@
|
|||
"save_without_cropping": "Save without cropping",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "Submit",
|
||||
"success": "Imported successfully.",
|
||||
"error": "An error occured while importing this file."
|
||||
},
|
||||
"login": {
|
||||
"login": "Log in",
|
||||
"description": "Log in with OAuth",
|
||||
|
@ -51,6 +60,7 @@
|
|||
"chat": "Local Chat",
|
||||
"friend_requests": "Follow Requests",
|
||||
"mentions": "Mentions",
|
||||
"interactions": "Interactions",
|
||||
"dms": "Direct Messages",
|
||||
"public_tl": "Public Timeline",
|
||||
"timeline": "Timeline",
|
||||
|
@ -86,6 +96,11 @@
|
|||
"604800": "7 days"
|
||||
}
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Repeats and Favorites",
|
||||
"follows": "New follows",
|
||||
"load_older": "Load older interactions"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Post new status",
|
||||
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
|
||||
|
@ -94,13 +109,19 @@
|
|||
"content_type": {
|
||||
"text/plain": "Plain text",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Subject (optional)",
|
||||
"default": "Just landed in L.A.",
|
||||
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
|
||||
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
|
||||
"posting": "Posting",
|
||||
"scope_notice": {
|
||||
"public": "This post will be visible to everyone",
|
||||
"private": "This post will be visible to your followers only",
|
||||
"unlisted": "This post will not be visible in Public Timeline and The Whole Known Network"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "Direct - Post to mentioned users only",
|
||||
"private": "Followers-only - Post to followers only",
|
||||
|
@ -129,6 +150,9 @@
|
|||
"password_confirmation_match": "should be the same as password"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Select all"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "App name",
|
||||
"attachmentRadius": "Attachments",
|
||||
|
@ -139,6 +163,11 @@
|
|||
"avatarRadius": "Avatars",
|
||||
"background": "Background",
|
||||
"bio": "Bio",
|
||||
"block_export": "Block export",
|
||||
"block_export_button": "Export your blocks to a csv file",
|
||||
"block_import": "Block import",
|
||||
"block_import_error": "Error importing blocks",
|
||||
"blocks_imported": "Blocks imported! Processing them will take a while.",
|
||||
"blocks_tab": "Blocks",
|
||||
"btnRadius": "Buttons",
|
||||
"cBlue": "Blue (Reply, follow)",
|
||||
|
@ -166,7 +195,6 @@
|
|||
"filtering_explanation": "All statuses containing these words will be muted, one per line",
|
||||
"follow_export": "Follow export",
|
||||
"follow_export_button": "Export your follows to a csv file",
|
||||
"follow_export_processing": "Processing, you'll soon be asked to download your file",
|
||||
"follow_import": "Follow import",
|
||||
"follow_import_error": "Error importing followers",
|
||||
"follows_imported": "Follows imported! Processing them will take a while.",
|
||||
|
@ -182,6 +210,7 @@
|
|||
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
||||
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
||||
"hide_filtered_statuses": "Hide filtered statuses",
|
||||
"import_blocks_from_a_csv_file": "Import blocks from a csv file",
|
||||
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
||||
"import_theme": "Load preset",
|
||||
"inputRadius": "Input fields",
|
||||
|
@ -232,8 +261,11 @@
|
|||
"reply_visibility_all": "Show all replies",
|
||||
"reply_visibility_following": "Only show replies directed at me or users I'm following",
|
||||
"reply_visibility_self": "Only show replies directed at me",
|
||||
"autohide_floating_post_button": "Automatically hide New Post button (mobile)",
|
||||
"saving_err": "Error saving settings",
|
||||
"saving_ok": "Settings saved",
|
||||
"search_user_to_block": "Search whom you want to block",
|
||||
"search_user_to_mute": "Search whom you want to mute",
|
||||
"security_tab": "Security",
|
||||
"scope_copy": "Copy scope when replying (DMs are always copied)",
|
||||
"minimal_scopes_mode": "Minimize post scope selection options",
|
||||
|
@ -262,6 +294,13 @@
|
|||
"true": "yes"
|
||||
},
|
||||
"notifications": "Notifications",
|
||||
"notification_setting": "Receive notifications from:",
|
||||
"notification_setting_follows": "Users you follow",
|
||||
"notification_setting_non_follows": "Users you do not follow",
|
||||
"notification_setting_followers": "Users who follow you",
|
||||
"notification_setting_non_followers": "Users who do not follow you",
|
||||
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
|
||||
"notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
|
||||
"enable_web_push_notifications": "Enable web push notifications",
|
||||
"style": {
|
||||
"switcher": {
|
||||
|
@ -391,6 +430,13 @@
|
|||
"no_statuses": "No statuses"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "Favorites",
|
||||
"repeats": "Repeats",
|
||||
"delete": "Delete status",
|
||||
"pin": "Pin on profile",
|
||||
"unpin": "Unpin from profile",
|
||||
"pinned": "Pinned",
|
||||
"delete_confirm": "Do you really want to delete this status?",
|
||||
"reply_to": "Reply to",
|
||||
"replies_list": "Replies:"
|
||||
},
|
||||
|
@ -415,6 +461,7 @@
|
|||
"muted": "Muted",
|
||||
"per_day": "per day",
|
||||
"remote_follow": "Remote follow",
|
||||
"report": "Report",
|
||||
"statuses": "Statuses",
|
||||
"unblock": "Unblock",
|
||||
"unblock_progress": "Unblocking...",
|
||||
|
@ -447,6 +494,15 @@
|
|||
"profile_does_not_exist": "Sorry, this profile does not exist.",
|
||||
"profile_loading_error": "Sorry, there was an error loading this profile."
|
||||
},
|
||||
"user_reporting": {
|
||||
"title": "Reporting {0}",
|
||||
"add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
|
||||
"additional_comments": "Additional comments",
|
||||
"forward_description": "The account is from another server. Send a copy of the report there as well?",
|
||||
"forward_to": "Forward to {0}",
|
||||
"submit": "Submit",
|
||||
"generic_error": "An error occurred while processing your request."
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "More",
|
||||
"who_to_follow": "Who to follow"
|
||||
|
|
116
src/i18n/es.json
116
src/i18n/es.json
|
@ -2,6 +2,10 @@
|
|||
"chat": {
|
||||
"title": "Chat"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Exportar",
|
||||
"processing": "Procesando. Pronto se te pedirá que descargues tu archivo"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "Chat",
|
||||
"gopher": "Gopher",
|
||||
|
@ -19,7 +23,22 @@
|
|||
"apply": "Aplicar",
|
||||
"submit": "Enviar",
|
||||
"more": "Más",
|
||||
"generic_error": "Ha ocurrido un error"
|
||||
"generic_error": "Ha ocurrido un error",
|
||||
"optional": "opcional",
|
||||
"show_more": "Mostrar más",
|
||||
"show_less": "Mostrar menos",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Recortar la foto",
|
||||
"save": "Guardar",
|
||||
"save_without_cropping": "Guardar sin recortar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "Enviar",
|
||||
"success": "Importado con éxito",
|
||||
"error": "Se ha producido un error al importar el archivo."
|
||||
},
|
||||
"login": {
|
||||
"login": "Identificación",
|
||||
|
@ -30,6 +49,10 @@
|
|||
"register": "Registrar",
|
||||
"username": "Usuario",
|
||||
"hint": "Inicia sesión para unirte a la discusión"
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "Anterior",
|
||||
"next": "Siguiente"
|
||||
},
|
||||
"nav": {
|
||||
"about": "Sobre",
|
||||
|
@ -61,15 +84,19 @@
|
|||
"account_not_locked_warning_link": "bloqueada",
|
||||
"attachments_sensitive": "Contenido sensible",
|
||||
"content_type": {
|
||||
"text/plain": "Texto Plano"
|
||||
"text/plain": "Texto Plano",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Tema (opcional)",
|
||||
"default": "Acabo de aterrizar en L.A.",
|
||||
"direct_warning": "Esta entrada solo será visible para los usuarios mencionados.",
|
||||
"direct_warning": "Esta publicación solo será visible para los usuarios mencionados.",
|
||||
"direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.",
|
||||
"posting": "Publicando",
|
||||
"scope": {
|
||||
"direct": "Directo - Solo para los usuarios mencionados.",
|
||||
"private": "Solo-Seguidores - Solo tus seguidores leeran la entrada",
|
||||
"private": "Solo-Seguidores - Solo tus seguidores leeran la publicación",
|
||||
"public": "Público - Entradas visibles en las Líneas Temporales Públicas",
|
||||
"unlisted": "Sin Listar - Entradas no visibles en las Líneas Temporales Públicas"
|
||||
}
|
||||
|
@ -83,6 +110,9 @@
|
|||
"token": "Token de invitación",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "Click en la imagen para obtener un nuevo captca",
|
||||
"username_placeholder": "p.ej. lain",
|
||||
"fullname_placeholder": "p.ej. Lain Iwakura",
|
||||
"bio_placeholder": "e.g.\nHola, soy un ejemplo.\nAquí puedes poner algo representativo tuyo... o no.",
|
||||
"validations": {
|
||||
"username_required": "no puede estar vacío",
|
||||
"fullname_required": "no puede estar vacío",
|
||||
|
@ -91,8 +121,12 @@
|
|||
"password_confirmation_required": "no puede estar vacío",
|
||||
"password_confirmation_match": "la contraseña no coincide"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Seleccionarlo todo"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "Nombre de la aplicación",
|
||||
"attachmentRadius": "Adjuntos",
|
||||
"attachments": "Adjuntos",
|
||||
"autoload": "Activar carga automática al llegar al final de la página",
|
||||
|
@ -101,6 +135,12 @@
|
|||
"avatarRadius": "Avatares",
|
||||
"background": "Fondo",
|
||||
"bio": "Biografía",
|
||||
"block_export": "Exportar usuarios bloqueados",
|
||||
"block_export_button": "Exporta la lista de tus usarios bloqueados a un archivo csv",
|
||||
"block_import": "Importar usuarios bloqueados",
|
||||
"block_import_error": "Error importando la lista de usuarios bloqueados",
|
||||
"blocks_imported": "¡Lista de usuarios bloqueados importada! El procesado puede tardar un poco.",
|
||||
"blocks_tab": "Bloqueados",
|
||||
"btnRadius": "Botones",
|
||||
"cBlue": "Azul (Responder, seguir)",
|
||||
"cGreen": "Verde (Retweet)",
|
||||
|
@ -127,7 +167,6 @@
|
|||
"filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea",
|
||||
"follow_export": "Exportar personas que tú sigues",
|
||||
"follow_export_button": "Exporta tus seguidores a un archivo csv",
|
||||
"follow_export_processing": "Procesando, en breve se te preguntará para guardar el archivo",
|
||||
"follow_import": "Importar personas que tú sigues",
|
||||
"follow_import_error": "Error al importal el archivo",
|
||||
"follows_imported": "¡Importado! Procesarlos llevará tiempo.",
|
||||
|
@ -135,12 +174,15 @@
|
|||
"general": "General",
|
||||
"hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones",
|
||||
"hide_attachments_in_tl": "Ocultar adjuntos en la línea temporal",
|
||||
"hide_muted_posts": "Ocultar las publicaciones de los usuarios silenciados",
|
||||
"max_thumbnails": "Cantidad máxima de miniaturas por publicación",
|
||||
"hide_isp": "Ocultar el panel específico de la instancia",
|
||||
"preload_images": "Precargar las imágenes",
|
||||
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
|
||||
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
|
||||
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
|
||||
"hide_filtered_statuses": "Ocultar estados filtrados",
|
||||
"import_blocks_from_a_csv_file": "Importar lista de usuarios bloqueados dese un archivo csv",
|
||||
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
|
||||
"import_theme": "Importar tema",
|
||||
"inputRadius": "Campos de entrada",
|
||||
|
@ -155,6 +197,7 @@
|
|||
"lock_account_description": "Restringir el acceso a tu cuenta solo a seguidores admitidos",
|
||||
"loop_video": "Vídeos en bucle",
|
||||
"loop_video_silent_only": "Bucle solo en vídeos sin sonido (p.ej. \"gifs\" de Mastodon)",
|
||||
"mutes_tab": "Silenciados",
|
||||
"play_videos_in_modal": "Reproducir los vídeos directamente en el visor de medios",
|
||||
"use_contain_fit": "No recortar los adjuntos en miniaturas",
|
||||
"name": "Nombre",
|
||||
|
@ -166,6 +209,8 @@
|
|||
"notification_visibility_mentions": "Menciones",
|
||||
"notification_visibility_repeats": "Repeticiones (Repeats)",
|
||||
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
|
||||
"no_blocks": "No hay usuarios bloqueados",
|
||||
"no_mutes": "No hay usuarios sinlenciados",
|
||||
"hide_follows_description": "No mostrar a quién sigo",
|
||||
"hide_followers_description": "No mostrar quién me sigue",
|
||||
"show_admin_badge": "Mostrar la placa de administrador en mi perfil",
|
||||
|
@ -190,8 +235,11 @@
|
|||
"reply_visibility_self": "Solo mostrar réplicas para mí",
|
||||
"saving_err": "Error al guardar los ajustes",
|
||||
"saving_ok": "Ajustes guardados",
|
||||
"search_user_to_block": "Buscar usuarios a bloquear",
|
||||
"search_user_to_mute": "Buscar usuarios a silenciar",
|
||||
"security_tab": "Seguridad",
|
||||
"scope_copy": "Copiar la visibilidad cuando contestamos (En los mensajes directos (MDs) siempre se copia)",
|
||||
"scope_copy": "Copiar la visibilidad de la publicación cuando contestamos (En los mensajes directos (MDs) siempre se copia)",
|
||||
"minimal_scopes_mode": "Minimizar las opciones de publicación",
|
||||
"set_new_avatar": "Cambiar avatar",
|
||||
"set_new_profile_background": "Cambiar fondo del perfil",
|
||||
"set_new_profile_banner": "Cambiar cabecera del perfil",
|
||||
|
@ -210,6 +258,7 @@
|
|||
"theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación, use el botón \"Borrar todo\" para deshacer los cambios.",
|
||||
"theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón para obtener información detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.",
|
||||
"tooltipRadius": "Información/alertas",
|
||||
"upload_a_photo": "Subir una foto",
|
||||
"user_settings": "Ajustes de Usuario",
|
||||
"values": {
|
||||
"false": "no",
|
||||
|
@ -325,6 +374,11 @@
|
|||
"checkbox": "He revisado los términos y condiciones",
|
||||
"link": "un bonito enlace"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"title": "Versión",
|
||||
"backend_version": "Versión del Backend",
|
||||
"frontend_version": "Versión del Frontend"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
@ -336,7 +390,14 @@
|
|||
"repeated": "repetida",
|
||||
"show_new": "Mostrar lo nuevo",
|
||||
"up_to_date": "Actualizado",
|
||||
"no_more_statuses": "No hay más estados"
|
||||
"no_more_statuses": "No hay más estados",
|
||||
"no_statuses": "Sin estados"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "Favoritos",
|
||||
"repeats": "Repetidos",
|
||||
"reply_to": "Responder a",
|
||||
"replies_list": "Respuestas:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Aprovar",
|
||||
|
@ -359,10 +420,47 @@
|
|||
"muted": "Silenciado",
|
||||
"per_day": "por día",
|
||||
"remote_follow": "Seguir",
|
||||
"statuses": "Estados"
|
||||
"report": "Reportar",
|
||||
"statuses": "Estados",
|
||||
"unblock": "Desbloquear",
|
||||
"unblock_progress": "Desbloqueando...",
|
||||
"block_progress": "Bloqueando...",
|
||||
"unmute": "Desenmudecer",
|
||||
"unmute_progress": "Sesenmudeciendo...",
|
||||
"mute_progress": "Silenciando...",
|
||||
"admin_menu": {
|
||||
"moderation": "Moderación",
|
||||
"grant_admin": "Conceder permisos de Administrador",
|
||||
"revoke_admin": "Revocar permisos de Administrador",
|
||||
"grant_moderator": "Conceder permisos de Moderador",
|
||||
"revoke_moderator": "Revocar permisos de Moderador",
|
||||
"activate_account": "Activar cuenta",
|
||||
"deactivate_account": "Desactivar cuenta",
|
||||
"delete_account": "Borrar cuenta",
|
||||
"force_nsfw": "Marcar todas las publicaciones como NSFW (no es seguro/apropiado para el trabajo)",
|
||||
"strip_media": "Eliminar archivos multimedia de las publicaciones",
|
||||
"force_unlisted": "Forzar que se publique en el modo -Sin Listar-",
|
||||
"sandbox": "Forzar que se publique solo para tus seguidores",
|
||||
"disable_remote_subscription": "No permitir que usuarios de instancias remotas te siga.",
|
||||
"disable_any_subscription": "No permitir que ningún usuario te siga",
|
||||
"quarantine": "No permitir publicaciones de usuarios de instancias remotas",
|
||||
"delete_user": "Borrar usuario",
|
||||
"delete_user_confirmation": "¿Estás completamente seguro? Esta acción no se puede deshacer."
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Linea temporal del usuario"
|
||||
"timeline_title": "Linea temporal del usuario",
|
||||
"profile_does_not_exist": "Lo sentimos, este perfil no existe.",
|
||||
"profile_loading_error": "Lo sentimos, hubo un error al cargar este perfil."
|
||||
},
|
||||
"user_reporting": {
|
||||
"title": "Reportando a {0}",
|
||||
"add_comment_description": "El informe será enviado a los moderadores de su instancia. Puedes proporcionar una explicación de por qué estás reportando esta cuenta a continuación:",
|
||||
"additional_comments": "Comentarios adicionales",
|
||||
"forward_description": "La cuenta es de otro servidor. ¿Enviar una copia del informe allí también?",
|
||||
"forward_to": "Reenviar a {0}",
|
||||
"submit": "Enviar",
|
||||
"generic_error": "Se produjo un error al procesar la solicitud."
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "Más",
|
||||
|
|
|
@ -222,6 +222,8 @@
|
|||
"no_more_statuses": "Ei enempää viestejä"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "Tykkäykset",
|
||||
"repeats": "Toistot",
|
||||
"reply_to": "Vastaus",
|
||||
"replies_list": "Vastaukset:"
|
||||
},
|
||||
|
|
225
src/i18n/he.json
225
src/i18n/he.json
|
@ -2,6 +2,10 @@
|
|||
"chat": {
|
||||
"title": "צ'אט"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "ייצוא",
|
||||
"processing": "מעבד, בקרוב תופיע אפשרות להוריד את הקובץ"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "צ'אט",
|
||||
"gopher": "גופר",
|
||||
|
@ -17,23 +21,53 @@
|
|||
},
|
||||
"general": {
|
||||
"apply": "החל",
|
||||
"submit": "שלח"
|
||||
"submit": "שלח",
|
||||
"more": "עוד",
|
||||
"generic_error": "קרתה שגיאה",
|
||||
"optional": "לבחירה",
|
||||
"show_more": "הראה עוד",
|
||||
"show_less": "הראה פחות",
|
||||
"cancel": "בטל"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "חתוך תמונה",
|
||||
"save": "שמור",
|
||||
"save_without_cropping": "שמור בלי לחתוך",
|
||||
"cancel": "בטל"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "שלח",
|
||||
"success": "ייובא בהצלחה.",
|
||||
"error": "אירעתה שגיאה בזמן ייבוא קובץ זה."
|
||||
},
|
||||
"login": {
|
||||
"login": "התחבר",
|
||||
"description": "היכנס עם OAuth",
|
||||
"logout": "התנתק",
|
||||
"password": "סיסמה",
|
||||
"placeholder": "למשל lain",
|
||||
"register": "הירשם",
|
||||
"username": "שם המשתמש"
|
||||
"username": "שם המשתמש",
|
||||
"hint": "הירשם על מנת להצטרף לדיון"
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "הקודם",
|
||||
"next": "הבא"
|
||||
},
|
||||
"nav": {
|
||||
"about": "על-אודות",
|
||||
"back": "חזור",
|
||||
"chat": "צ'אט מקומי",
|
||||
"friend_requests": "בקשות עקיבה",
|
||||
"mentions": "אזכורים",
|
||||
"interactions": "אינטרקציות",
|
||||
"dms": "הודעות ישירות",
|
||||
"public_tl": "ציר הזמן הציבורי",
|
||||
"timeline": "ציר הזמן",
|
||||
"twkn": "כל הרשת הידועה"
|
||||
"twkn": "כל הרשת הידועה",
|
||||
"user_search": "חיפוש משתמש",
|
||||
"who_to_follow": "אחרי מי לעקוב",
|
||||
"preferences": "העדפות"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "סטאטוס לא ידוע, מחפש...",
|
||||
|
@ -42,19 +76,35 @@
|
|||
"load_older": "טען התראות ישנות",
|
||||
"notifications": "התראות",
|
||||
"read": "קרא!",
|
||||
"repeated_you": "חזר על הסטטוס שלך"
|
||||
"repeated_you": "חזר על הסטטוס שלך",
|
||||
"no_more_notifications": "לא עוד התראות"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "חזרות ומועדפים",
|
||||
"follows": "עוקבים חדשים",
|
||||
"load_older": "טען אינטרקציות ישנות"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "פרסם סטאטוס חדש",
|
||||
"account_not_locked_warning": "המשתמש שלך אינו {0}. כל אחד יכול לעקוב אחריך ולראות את ההודעות לעוקבים-בלבד שלך.",
|
||||
"account_not_locked_warning_link": "נעול",
|
||||
"attachments_sensitive": "סמן מסמכים מצורפים כלא בטוחים לצפייה",
|
||||
"content_type": {
|
||||
"text/plain": "טקסט פשוט"
|
||||
"text/plain": "טקסט פשוט",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "נושא (נתון לבחירה)",
|
||||
"default": "הרגע נחת ב-ל.א.",
|
||||
"direct_warning": "הודעה זו תהיה זמינה רק לאנשים המוזכרים.",
|
||||
"direct_warning_to_all": "הודעה זו תהיה נראית לכל המשתמשים המוזכרים.",
|
||||
"direct_warning_to_first_only": "הודעה זו תהיה נראית לכל המשתמשים במוזכרים בתחילת ההודעה בלבד.",
|
||||
"posting": "מפרסם",
|
||||
"scope_notice": {
|
||||
"public": "הודעה זו תהיה נראית לכולם",
|
||||
"private": "הודעה זו תהיה נראית לעוקבים שלך בלבד",
|
||||
"unlisted": "הודעה זו לא תהיה נראית בציר זמן הציבורי או בכל הרשת הידועה"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "ישיר - שלח לאנשים המוזכרים בלבד",
|
||||
"private": "עוקבים-בלבד - שלח לעוקבים בלבד",
|
||||
|
@ -68,9 +118,26 @@
|
|||
"fullname": "שם תצוגה",
|
||||
"password_confirm": "אישור סיסמה",
|
||||
"registration": "הרשמה",
|
||||
"token": "טוקן הזמנה"
|
||||
"token": "טוקן הזמנה",
|
||||
"captcha": "אימות אנוש",
|
||||
"new_captcha": "לחץ על התמונה על מנת לקבל אימות אנוש חדש",
|
||||
"username_placeholder": "למשל lain",
|
||||
"fullname_placeholder": "למשל Lain Iwakura",
|
||||
"bio_placeholder": "למשל\nהיי, אני ליין.\nאני ילדת אנימה שגרה בפרוורי יפן. אולי אתם מכירים אותי מהWired.",
|
||||
"validations": {
|
||||
"username_required": "לא יכול להישאר ריק",
|
||||
"fullname_required": "לא יכול להישאר ריק",
|
||||
"email_required": "לא יכול להישאר ריק",
|
||||
"password_required": "לא יכול להישאר ריק",
|
||||
"password_confirmation_required": "לא יכול להישאר ריק",
|
||||
"password_confirmation_match": "צריך להיות דומה לסיסמה"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "בחר הכל"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "שם האפליקציה",
|
||||
"attachmentRadius": "צירופים",
|
||||
"attachments": "צירופים",
|
||||
"autoload": "החל טעינה אוטומטית בגלילה לתחתית הדף",
|
||||
|
@ -79,6 +146,12 @@
|
|||
"avatarRadius": "תמונות פרופיל",
|
||||
"background": "רקע",
|
||||
"bio": "אודות",
|
||||
"block_export": "ייצוא חסימות",
|
||||
"block_export_button": "ייצוא חסימות אל קובץ csv",
|
||||
"block_import": "ייבוא חסימות",
|
||||
"block_import_error": "שגיאה בייבוא החסימות",
|
||||
"blocks_imported": "החסימות יובאו! ייקח מעט זמן לעבד אותן.",
|
||||
"blocks_tab": "חסימות",
|
||||
"btnRadius": "כפתורים",
|
||||
"cBlue": "כחול (תגובה, עקיבה)",
|
||||
"cGreen": "ירוק (חזרה)",
|
||||
|
@ -88,6 +161,7 @@
|
|||
"change_password_error": "הייתה בעיה בשינוי סיסמתך.",
|
||||
"changed_password": "סיסמה שונתה בהצלחה!",
|
||||
"collapse_subject": "מזער הודעות עם נושאים",
|
||||
"composing": "מרכיב",
|
||||
"confirm_new_password": "אשר סיסמה",
|
||||
"current_avatar": "תמונת הפרופיל הנוכחית שלך",
|
||||
"current_password": "סיסמה נוכחית",
|
||||
|
@ -98,21 +172,35 @@
|
|||
"delete_account_description": "מחק לצמיתות את המשתמש שלך ואת כל הודעותיך.",
|
||||
"delete_account_error": "הייתה בעיה במחיקת המשתמש. אם זה ממשיך, אנא עדכן את מנהל השרת שלך.",
|
||||
"delete_account_instructions": "הכנס את סיסמתך בקלט למטה על מנת לאשר מחיקת משתמש.",
|
||||
"avatar_size_instruction": "הגודל המינימלי המומלץ לתמונות פרופיל הוא 150x150 פיקסלים.",
|
||||
"export_theme": "שמור ערכים",
|
||||
"filtering": "סינון",
|
||||
"filtering_explanation": "כל הסטטוסים הכוללים את המילים הללו יושתקו, אחד לשורה",
|
||||
"follow_export": "יצוא עקיבות",
|
||||
"follow_export_button": "ייצא את הנעקבים שלך לקובץ csv",
|
||||
"follow_export_processing": "טוען. בקרוב תתבקש להוריד את הקובץ את הקובץ שלך",
|
||||
"follow_import": "יבוא עקיבות",
|
||||
"follow_import_error": "שגיאה בייבוא נעקבים.",
|
||||
"follows_imported": "נעקבים יובאו! ייקח זמן מה לעבד אותם.",
|
||||
"foreground": "חזית",
|
||||
"general": "כללי",
|
||||
"hide_attachments_in_convo": "החבא צירופים בשיחות",
|
||||
"hide_attachments_in_tl": "החבא צירופים בציר הזמן",
|
||||
"hide_muted_posts": "הסתר הודעות של משתמשים מושתקים",
|
||||
"max_thumbnails": "מספר מירבי של תמונות ממוזערות להודעה",
|
||||
"hide_isp": "הסתר פאנל-צד",
|
||||
"preload_images": "טען תמונות מראש",
|
||||
"use_one_click_nsfw": "פתח תמונות לא-בטוחות-לעבודה עם לחיצה אחת בלבד",
|
||||
"hide_post_stats": "הסתר נתוני הודעה (למשל, מספר החזרות)",
|
||||
"hide_user_stats": "הסתר נתוני משתמש (למשל, מספר העוקבים)",
|
||||
"hide_filtered_statuses": "מסתר סטטוסים מסוננים",
|
||||
"import_blocks_from_a_csv_file": "ייבא חסימות מקובץ csv",
|
||||
"import_followers_from_a_csv_file": "ייבא את הנעקבים שלך מקובץ csv",
|
||||
"import_theme": "טען ערכים",
|
||||
"inputRadius": "שדות קלט",
|
||||
"checkboxRadius": "תיבות סימון",
|
||||
"instance_default": "(default: {value})",
|
||||
"instance_default_simple": "(default)",
|
||||
"interface": "ממשק",
|
||||
"interfaceLanguage": "שפת הממשק",
|
||||
"invalid_theme_imported": "הקובץ הנבחר אינו תמה הנתמכת ע\"י פלרומה. שום שינויים לא נעשו לתמה שלך.",
|
||||
"limited_availability": "לא זמין בדפדפן שלך",
|
||||
|
@ -120,6 +208,9 @@
|
|||
"lock_account_description": "הגבל את המשתמש לעוקבים מאושרים בלבד",
|
||||
"loop_video": "נגן סרטונים ללא הפסקה",
|
||||
"loop_video_silent_only": "נגן רק סרטונים חסרי קול ללא הפסקה",
|
||||
"mutes_tab": "השתקות",
|
||||
"play_videos_in_modal": "נגן סרטונים ישירות בנגן המדיה",
|
||||
"use_contain_fit": "אל תחתוך את הצירוף בתמונות הממוזערות",
|
||||
"name": "שם",
|
||||
"name_bio": "שם ואודות",
|
||||
"new_password": "סיסמה חדשה",
|
||||
|
@ -128,6 +219,13 @@
|
|||
"notification_visibility_likes": "לייקים",
|
||||
"notification_visibility_mentions": "אזכורים",
|
||||
"notification_visibility_repeats": "חזרות",
|
||||
"no_rich_text_description": "הסר פורמט טקסט עשיר מכל ההודעות",
|
||||
"no_blocks": "ללא חסימות",
|
||||
"no_mutes": "ללא השתקות",
|
||||
"hide_follows_description": "אל תראה אחרי מי אני עוקב",
|
||||
"hide_followers_description": "אל תראה מי עוקב אחרי",
|
||||
"show_admin_badge": "הראה סמל מנהל בפרופיל שלי",
|
||||
"show_moderator_badge": "הראה סמל צוות בפרופיל שלי",
|
||||
"nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
|
||||
"oauth_tokens": "אסימוני OAuth",
|
||||
"token": "אסימון",
|
||||
|
@ -146,18 +244,43 @@
|
|||
"reply_visibility_all": "הראה את כל התגובות",
|
||||
"reply_visibility_following": "הראה תגובות שמופנות אליי או לעקובים שלי בלבד",
|
||||
"reply_visibility_self": "הראה תגובות שמופנות אליי בלבד",
|
||||
"autohide_floating_post_button": "החבא אוטומטית את הכפתור הודעה חדשה (נייד)",
|
||||
"saving_err": "שגיאה בשמירת הגדרות",
|
||||
"saving_ok": "הגדרות נשמרו",
|
||||
"search_user_to_block": "חפש משתמש לחסימה",
|
||||
"search_user_to_mute": "חפש משתמש להשתקה",
|
||||
"security_tab": "ביטחון",
|
||||
"scope_copy": "העתק תחום הודעה בתגובה להודעה (הודעות ישירות תמיד מועתקות)",
|
||||
"minimal_scopes_mode": "צמצם אפשרויות בחירה לתחום הודעה",
|
||||
"set_new_avatar": "קבע תמונת פרופיל חדשה",
|
||||
"set_new_profile_background": "קבע רקע פרופיל חדש",
|
||||
"set_new_profile_banner": "קבע כרזת פרופיל חדשה",
|
||||
"settings": "הגדרות",
|
||||
"subject_input_always_show": "תמיד הראה את שדה הנושא",
|
||||
"subject_line_behavior": "העתק נושא בתגובה",
|
||||
"subject_line_email": "כמו אימייל: \"re: נושא\"",
|
||||
"subject_line_mastodon": "כמו מסטודון: העתק כפי שזה",
|
||||
"subject_line_noop": "אל תעתיק",
|
||||
"post_status_content_type": "שלח את סוג תוכן ההודעה",
|
||||
"stop_gifs": "נגן-בעת-ריחוף GIFs",
|
||||
"streaming": "החל זרימת הודעות אוטומטית בעת גלילה למעלה הדף",
|
||||
"text": "טקסט",
|
||||
"theme": "תמה",
|
||||
"theme_help": "השתמש בקודי צבע הקס (#אדום-אדום-ירוק-ירוק-כחול-כחול) על מנת להתאים אישית את תמת הצבע שלך.",
|
||||
"tooltipRadius": "טולטיפ \\ התראות",
|
||||
"user_settings": "הגדרות משתמש"
|
||||
"upload_a_photo": "העלה תמונה",
|
||||
"user_settings": "הגדרות משתמש",
|
||||
"values": {
|
||||
"false": "לא",
|
||||
"true": "כן"
|
||||
},
|
||||
"notifications": "התראות",
|
||||
"enable_web_push_notifications": "אפשר התראות web push",
|
||||
"version": {
|
||||
"title": "גרסה",
|
||||
"backend_version": "גרסת קצה אחורי",
|
||||
"frontend_version": "גרסת קצה קדמי"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "מוטט",
|
||||
|
@ -167,29 +290,107 @@
|
|||
"no_retweet_hint": "ההודעה מסומנת כ\"לעוקבים-בלבד\" ולא ניתן לחזור עליה",
|
||||
"repeated": "חזר",
|
||||
"show_new": "הראה חדש",
|
||||
"up_to_date": "עדכני"
|
||||
"up_to_date": "עדכני",
|
||||
"no_more_statuses": "אין עוד סטטוסים",
|
||||
"no_statuses": "אין סטטוסים"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "מועדפים",
|
||||
"repeats": "חזרות",
|
||||
"delete": "מחק סטטוס",
|
||||
"pin": "הצמד לפרופיל",
|
||||
"unpin": "הסר הצמדה מהפרופיל",
|
||||
"pinned": "מוצמד",
|
||||
"delete_confirm": "האם באמת למחוק סטטוס זה?",
|
||||
"reply_to": "הגב ל",
|
||||
"replies_list": "תגובות:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "אשר",
|
||||
"block": "חסימה",
|
||||
"blocked": "חסום!",
|
||||
"deny": "דחה",
|
||||
"favorites": "מועדפים",
|
||||
"follow": "עקוב",
|
||||
"follow_sent": "בקשה נשלחה!",
|
||||
"follow_progress": "מבקש...",
|
||||
"follow_again": "שלח בקשה שוב?",
|
||||
"follow_unfollow": "בטל עקיבה",
|
||||
"followees": "נעקבים",
|
||||
"followers": "עוקבים",
|
||||
"following": "עוקב!",
|
||||
"follows_you": "עוקב אחריך!",
|
||||
"its_you": "זה אתה!",
|
||||
"media": "מדיה",
|
||||
"mute": "השתק",
|
||||
"muted": "מושתק",
|
||||
"per_day": "ליום",
|
||||
"remote_follow": "עקיבה מרחוק",
|
||||
"statuses": "סטטוסים"
|
||||
"report": "דווח",
|
||||
"statuses": "סטטוסים",
|
||||
"unblock": "הסר חסימה",
|
||||
"unblock_progress": "מסיר חסימה...",
|
||||
"block_progress": "חוסם...",
|
||||
"unmute": "הסר השתקה",
|
||||
"unmute_progress": "מסיר השתקה...",
|
||||
"mute_progress": "משתיק...",
|
||||
"admin_menu": {
|
||||
"moderation": "ניהול (צוות)",
|
||||
"grant_admin": "הפוך למנהל",
|
||||
"revoke_admin": "הסר מנהל",
|
||||
"grant_moderator": "הפוך לצוות",
|
||||
"revoke_moderator": "הסר צוות",
|
||||
"activate_account": "הפעל משתמש",
|
||||
"deactivate_account": "השבת משתמש",
|
||||
"delete_account": "מחק משתמש",
|
||||
"force_nsfw": "סמן את כל ההודעות בתור לא-מתאימות-לעבודה",
|
||||
"strip_media": "הסר מדיה מההודעות",
|
||||
"force_unlisted": "הפוך הודעות ללא רשומות",
|
||||
"sandbox": "הפוך הודעות לנראות לעוקבים-בלבד",
|
||||
"disable_remote_subscription": "אל תאפשר עקיבה של המשתמש מאינסטנס אחר",
|
||||
"disable_any_subscription": "אל תאפשר עקיבה של המשתמש בכלל",
|
||||
"quarantine": "אל תאפשר פדרציה של ההודעות של המשתמש",
|
||||
"delete_user": "מחק משתמש",
|
||||
"delete_user_confirmation": "בטוח? פעולה זו הינה בלתי הפיכה."
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "ציר זמן המשתמש"
|
||||
"timeline_title": "ציר זמן המשתמש",
|
||||
"profile_does_not_exist": "סליחה, פרופיל זה אינו קיים.",
|
||||
"profile_loading_error": "סליחה, הייתה שגיאה בטעינת הפרופיל."
|
||||
},
|
||||
"user_reporting": {
|
||||
"title": "מדווח על {0}",
|
||||
"add_comment_description": "הדיווח ישלח לצוות האינסטנס. אפשר להסביר למה הנך מדווחים על משתמש זה למטה:",
|
||||
"additional_comments": "תגובות נוספות",
|
||||
"forward_description": "המשתמש משרת אחר. לשלוח לשם עותק של הדיווח?",
|
||||
"forward_to": "העבר ל {0}",
|
||||
"submit": "הגש",
|
||||
"generic_error": "קרתה שגיאה בעת עיבוד הבקשה."
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "עוד",
|
||||
"who_to_follow": "אחרי מי לעקוב"
|
||||
},
|
||||
"tool_tip": {
|
||||
"media_upload": "העלה מדיה",
|
||||
"repeat": "חזור",
|
||||
"reply": "הגב",
|
||||
"favorite": "מועדף",
|
||||
"user_settings": "הגדרות משתמש"
|
||||
},
|
||||
"upload":{
|
||||
"error": {
|
||||
"base": "העלאה נכשלה.",
|
||||
"file_too_big": "קובץ גדול מדי [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "נסה שוב אחר כך"
|
||||
},
|
||||
"file_size_units": {
|
||||
"B": "B",
|
||||
"KiB": "KiB",
|
||||
"MiB": "MiB",
|
||||
"GiB": "GiB",
|
||||
"TiB": "TiB"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
392
src/i18n/ja_pedantic.json
Normal file
392
src/i18n/ja_pedantic.json
Normal file
|
@ -0,0 +1,392 @@
|
|||
{
|
||||
"chat": {
|
||||
"title": "チャット"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "チャット",
|
||||
"gopher": "Gopher",
|
||||
"media_proxy": "メディアプロクシ",
|
||||
"scope_options": "公開範囲選択",
|
||||
"text_limit": "文字の数",
|
||||
"title": "有効な機能",
|
||||
"who_to_follow": "おすすめユーザー"
|
||||
},
|
||||
"finder": {
|
||||
"error_fetching_user": "ユーザー検索がエラーになりました。",
|
||||
"find_user": "ユーザーを探す"
|
||||
},
|
||||
"general": {
|
||||
"apply": "適用",
|
||||
"submit": "送信",
|
||||
"more": "続き",
|
||||
"generic_error": "エラーになりました"
|
||||
},
|
||||
"login": {
|
||||
"login": "ログイン",
|
||||
"description": "OAuthでログイン",
|
||||
"logout": "ログアウト",
|
||||
"password": "パスワード",
|
||||
"placeholder": "例: lain",
|
||||
"register": "登録",
|
||||
"username": "ユーザー名",
|
||||
"hint": "会話に加わるには、ログインしてください"
|
||||
},
|
||||
"nav": {
|
||||
"about": "このインスタンスについて",
|
||||
"back": "戻る",
|
||||
"chat": "ローカルチャット",
|
||||
"friend_requests": "フォローリクエスト",
|
||||
"mentions": "通知",
|
||||
"dms": "ダイレクトメッセージ",
|
||||
"public_tl": "パブリックタイムライン",
|
||||
"timeline": "タイムライン",
|
||||
"twkn": "接続しているすべてのネットワーク",
|
||||
"user_search": "ユーザーを探す",
|
||||
"who_to_follow": "おすすめユーザー",
|
||||
"preferences": "設定"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "ステータスが見つかりません。探しています...",
|
||||
"favorited_you": "あなたのステータスがお気に入りされました",
|
||||
"followed_you": "フォローされました",
|
||||
"load_older": "古い通知をみる",
|
||||
"notifications": "通知",
|
||||
"read": "読んだ!",
|
||||
"repeated_you": "あなたのステータスがリピートされました",
|
||||
"no_more_notifications": "通知はありません"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "投稿する",
|
||||
"account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でも、フォロワー限定のステータスを読むことができます。",
|
||||
"account_not_locked_warning_link": "ロックされたアカウント",
|
||||
"attachments_sensitive": "ファイルをNSFWにする",
|
||||
"content_type": {
|
||||
"text/plain": "プレーンテキスト"
|
||||
},
|
||||
"content_warning": "説明 (省略可)",
|
||||
"default": "羽田空港に着きました。",
|
||||
"direct_warning": "このステータスは、メンションされたユーザーだけが、読むことができます。",
|
||||
"posting": "投稿",
|
||||
"scope": {
|
||||
"direct": "ダイレクト: メンションされたユーザーのみに届きます。",
|
||||
"private": "フォロワーげんてい: フォロワーのみに届きます。",
|
||||
"public": "パブリック: パブリックタイムラインに届きます。",
|
||||
"unlisted": "アンリステッド: パブリックタイムラインに届きません。"
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
"bio": "プロフィール",
|
||||
"email": "Eメール",
|
||||
"fullname": "スクリーンネーム",
|
||||
"password_confirm": "パスワードの確認",
|
||||
"registration": "登録",
|
||||
"token": "招待トークン",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "文字が読めないときは、画像をクリックすると、新しい画像になります",
|
||||
"validations": {
|
||||
"username_required": "必須",
|
||||
"fullname_required": "必須",
|
||||
"email_required": "必須",
|
||||
"password_required": "必須",
|
||||
"password_confirmation_required": "必須",
|
||||
"password_confirmation_match": "パスワードが違います"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "ファイル",
|
||||
"attachments": "ファイル",
|
||||
"autoload": "下にスクロールしたとき、自動的に読み込む。",
|
||||
"avatar": "アバター",
|
||||
"avatarAltRadius": "通知のアバター",
|
||||
"avatarRadius": "アバター",
|
||||
"background": "バックグラウンド",
|
||||
"bio": "プロフィール",
|
||||
"btnRadius": "ボタン",
|
||||
"cBlue": "返信とフォロー",
|
||||
"cGreen": "リピート",
|
||||
"cOrange": "お気に入り",
|
||||
"cRed": "キャンセル",
|
||||
"change_password": "パスワードを変える",
|
||||
"change_password_error": "パスワードを変えることが、できなかったかもしれません。",
|
||||
"changed_password": "パスワードが、変わりました!",
|
||||
"collapse_subject": "説明のある投稿をたたむ",
|
||||
"composing": "投稿",
|
||||
"confirm_new_password": "新しいパスワードの確認",
|
||||
"current_avatar": "現在のアバター",
|
||||
"current_password": "現在のパスワード",
|
||||
"current_profile_banner": "現在のプロフィールバナー",
|
||||
"data_import_export_tab": "インポートとエクスポート",
|
||||
"default_vis": "デフォルトの公開範囲",
|
||||
"delete_account": "アカウントを消す",
|
||||
"delete_account_description": "あなたのアカウントとメッセージが、消えます。",
|
||||
"delete_account_error": "アカウントを消すことが、できなかったかもしれません。インスタンスの管理者に、連絡してください。",
|
||||
"delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。",
|
||||
"avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。",
|
||||
"export_theme": "保存",
|
||||
"filtering": "フィルタリング",
|
||||
"filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください。",
|
||||
"follow_export": "フォローのエクスポート",
|
||||
"follow_export_button": "エクスポート",
|
||||
"follow_export_processing": "お待ちください。まもなくファイルをダウンロードできます。",
|
||||
"follow_import": "フォローインポート",
|
||||
"follow_import_error": "フォローのインポートがエラーになりました。",
|
||||
"follows_imported": "フォローがインポートされました! 少し時間がかかるかもしれません。",
|
||||
"foreground": "フォアグラウンド",
|
||||
"general": "全般",
|
||||
"hide_attachments_in_convo": "スレッドのファイルを隠す",
|
||||
"hide_attachments_in_tl": "タイムラインのファイルを隠す",
|
||||
"hide_isp": "インスタンス固有パネルを隠す",
|
||||
"preload_images": "画像を先読みする",
|
||||
"use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
|
||||
"hide_post_stats": "投稿の統計を隠す (例: お気に入りの数)",
|
||||
"hide_user_stats": "ユーザーの統計を隠す (例: フォロワーの数)",
|
||||
"hide_filtered_statuses": "フィルターされた投稿を隠す",
|
||||
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
||||
"import_theme": "ロード",
|
||||
"inputRadius": "インプットフィールド",
|
||||
"checkboxRadius": "チェックボックス",
|
||||
"instance_default": "(デフォルト: {value})",
|
||||
"instance_default_simple": "(デフォルト)",
|
||||
"interface": "インターフェース",
|
||||
"interfaceLanguage": "インターフェースの言語",
|
||||
"invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマは変更されませんでした。",
|
||||
"limited_availability": "あなたのブラウザではできません",
|
||||
"links": "リンク",
|
||||
"lock_account_description": "あなたが認めた人だけ、あなたのアカウントをフォローできる",
|
||||
"loop_video": "ビデオを繰り返す",
|
||||
"loop_video_silent_only": "音のないビデオだけ繰り返す",
|
||||
"play_videos_in_modal": "ビデオをメディアビューアーで見る",
|
||||
"use_contain_fit": "画像のサムネイルを、切り抜かない",
|
||||
"name": "名前",
|
||||
"name_bio": "名前とプロフィール",
|
||||
"new_password": "新しいパスワード",
|
||||
"notification_visibility": "表示する通知",
|
||||
"notification_visibility_follows": "フォロー",
|
||||
"notification_visibility_likes": "お気に入り",
|
||||
"notification_visibility_mentions": "メンション",
|
||||
"notification_visibility_repeats": "リピート",
|
||||
"no_rich_text_description": "リッチテキストを使わない",
|
||||
"hide_follows_description": "フォローしている人を見せない",
|
||||
"hide_followers_description": "フォロワーを見せない",
|
||||
"show_admin_badge": "管理者のバッジを見せる",
|
||||
"show_moderator_badge": "モデレーターのバッジを見せる",
|
||||
"nsfw_clickthrough": "NSFWなファイルを隠す",
|
||||
"oauth_tokens": "OAuthトークン",
|
||||
"token": "トークン",
|
||||
"refresh_token": "トークンを更新",
|
||||
"valid_until": "まで有効",
|
||||
"revoke_token": "取り消す",
|
||||
"panelRadius": "パネル",
|
||||
"pause_on_unfocused": "タブにフォーカスがないときストリーミングを止める",
|
||||
"presets": "プリセット",
|
||||
"profile_background": "プロフィールのバックグラウンド",
|
||||
"profile_banner": "プロフィールバナー",
|
||||
"profile_tab": "プロフィール",
|
||||
"radii_help": "インターフェースの丸さを設定する。",
|
||||
"replies_in_timeline": "タイムラインのリプライ",
|
||||
"reply_link_preview": "カーソルを重ねたとき、リプライのプレビューを見る",
|
||||
"reply_visibility_all": "すべてのリプライを見る",
|
||||
"reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る",
|
||||
"reply_visibility_self": "私に宛てられたリプライを見る",
|
||||
"saving_err": "設定を保存できませんでした",
|
||||
"saving_ok": "設定を保存しました",
|
||||
"security_tab": "セキュリティ",
|
||||
"scope_copy": "返信するとき、公開範囲をコピーする (DMの公開範囲は、常にコピーされます)",
|
||||
"set_new_avatar": "新しいアバターを設定する",
|
||||
"set_new_profile_background": "新しいプロフィールのバックグラウンドを設定する",
|
||||
"set_new_profile_banner": "新しいプロフィールバナーを設定する",
|
||||
"settings": "設定",
|
||||
"subject_input_always_show": "サブジェクトフィールドをいつでも表示する",
|
||||
"subject_line_behavior": "返信するときサブジェクトをコピーする",
|
||||
"subject_line_email": "メール風: \"re: サブジェクト\"",
|
||||
"subject_line_mastodon": "マストドン風: そのままコピー",
|
||||
"subject_line_noop": "コピーしない",
|
||||
"post_status_content_type": "投稿のコンテントタイプ",
|
||||
"stop_gifs": "カーソルを重ねたとき、GIFを動かす",
|
||||
"streaming": "上までスクロールしたとき、自動的にストリーミングする",
|
||||
"text": "文字",
|
||||
"theme": "テーマ",
|
||||
"theme_help": "カラーテーマをカスタマイズできます",
|
||||
"theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、色と透明度をオーバーライドできます。「すべてクリア」ボタンを押すと、すべてのオーバーライドをやめます。",
|
||||
"theme_help_v2_2": "バックグラウンドとテキストのコントラストを表すアイコンがあります。マウスをホバーすると、詳しい説明が出ます。透明な色を使っているときは、最悪の場合のコントラストが示されます。",
|
||||
"tooltipRadius": "ツールチップとアラート",
|
||||
"user_settings": "ユーザー設定",
|
||||
"values": {
|
||||
"false": "いいえ",
|
||||
"true": "はい"
|
||||
},
|
||||
"notifications": "通知",
|
||||
"enable_web_push_notifications": "ウェブプッシュ通知を許可する",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "色を残す",
|
||||
"keep_shadows": "影を残す",
|
||||
"keep_opacity": "透明度を残す",
|
||||
"keep_roundness": "丸さを残す",
|
||||
"keep_fonts": "フォントを残す",
|
||||
"save_load_hint": "「残す」オプションをONにすると、テーマを選んだときとロードしたとき、現在の設定を残します。また、テーマをエクスポートするとき、これらのオプションを維持します。すべてのチェックボックスをOFFにすると、テーマをエクスポートしたとき、すべての設定を保存します。",
|
||||
"reset": "リセット",
|
||||
"clear_all": "すべてクリア",
|
||||
"clear_opacity": "透明度をクリア"
|
||||
},
|
||||
"common": {
|
||||
"color": "色",
|
||||
"opacity": "透明度",
|
||||
"contrast": {
|
||||
"hint": "コントラストは {ratio} です。{level}。({context})",
|
||||
"level": {
|
||||
"aa": "AAレベルガイドライン (ミニマル) を満たします",
|
||||
"aaa": "AAAレベルガイドライン (レコメンデッド) を満たします。",
|
||||
"bad": "ガイドラインを満たしません。"
|
||||
},
|
||||
"context": {
|
||||
"18pt": "大きい (18ポイント以上) テキスト",
|
||||
"text": "テキスト"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_colors": {
|
||||
"_tab_label": "共通",
|
||||
"main": "共通の色",
|
||||
"foreground_hint": "「詳細」タブで、もっと細かく設定できます",
|
||||
"rgbo": "アイコンとアクセントとバッジ"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"_tab_label": "詳細",
|
||||
"alert": "アラートのバックグラウンド",
|
||||
"alert_error": "エラー",
|
||||
"badge": "バッジのバックグラウンド",
|
||||
"badge_notification": "通知",
|
||||
"panel_header": "パネルヘッダー",
|
||||
"top_bar": "トップバー",
|
||||
"borders": "境界",
|
||||
"buttons": "ボタン",
|
||||
"inputs": "インプットフィールド",
|
||||
"faint_text": "薄いテキスト"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "丸さ"
|
||||
},
|
||||
"shadows": {
|
||||
"_tab_label": "光と影",
|
||||
"component": "コンポーネント",
|
||||
"override": "オーバーライド",
|
||||
"shadow_id": "影 #{value}",
|
||||
"blur": "ぼかし",
|
||||
"spread": "広がり",
|
||||
"inset": "内側",
|
||||
"hint": "影の設定では、色の値として --variable を使うことができます。これはCSS3変数です。ただし、透明度の設定は、効かなくなります。",
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "ブラウザーがサポートしていれば、常に {0} が使われます。",
|
||||
"drop_shadow_syntax": "{0} は、{1} パラメーターと {2} キーワードをサポートしていません。",
|
||||
"avatar_inset": "内側の影と外側の影を同時に使うと、透明なアバターの表示が乱れます。",
|
||||
"spread_zero": "広がりが 0 よりも大きな影は、0 と同じです。",
|
||||
"inset_classic": "内側の影は {0} を使います。"
|
||||
},
|
||||
"components": {
|
||||
"panel": "パネル",
|
||||
"panelHeader": "パネルヘッダー",
|
||||
"topBar": "トップバー",
|
||||
"avatar": "ユーザーアバター (プロフィール)",
|
||||
"avatarStatus": "ユーザーアバター (投稿)",
|
||||
"popup": "ポップアップとツールチップ",
|
||||
"button": "ボタン",
|
||||
"buttonHover": "ボタン (ホバー)",
|
||||
"buttonPressed": "ボタン (押されているとき)",
|
||||
"buttonPressedHover": "ボタン (ホバー、かつ、押されているとき)",
|
||||
"input": "インプットフィールド"
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"_tab_label": "フォント",
|
||||
"help": "「カスタム」を選んだときは、システムにあるフォントの名前を、正しく入力してください。",
|
||||
"components": {
|
||||
"interface": "インターフェース",
|
||||
"input": "インプットフィールド",
|
||||
"post": "投稿",
|
||||
"postCode": "等幅 (投稿がリッチテキストであるとき)"
|
||||
},
|
||||
"family": "フォント名",
|
||||
"size": "大きさ (px)",
|
||||
"weight": "太さ",
|
||||
"custom": "カスタム"
|
||||
},
|
||||
"preview": {
|
||||
"header": "プレビュー",
|
||||
"content": "本文",
|
||||
"error": "エラーの例",
|
||||
"button": "ボタン",
|
||||
"text": "これは{0}と{1}の例です。",
|
||||
"mono": "monospace",
|
||||
"input": "羽田空港に着きました。",
|
||||
"faint_link": "とても助けになるマニュアル",
|
||||
"fine_print": "私たちの{0}を、読まないでください!",
|
||||
"header_faint": "エラーではありません",
|
||||
"checkbox": "利用規約を読みました",
|
||||
"link": "ハイパーリンク"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "たたむ",
|
||||
"conversation": "スレッド",
|
||||
"error_fetching": "読み込みがエラーになりました",
|
||||
"load_older": "古いステータス",
|
||||
"no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
|
||||
"repeated": "リピート",
|
||||
"show_new": "読み込み",
|
||||
"up_to_date": "最新",
|
||||
"no_more_statuses": "これで終わりです"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "受け入れ",
|
||||
"block": "ブロック",
|
||||
"blocked": "ブロックしています!",
|
||||
"deny": "お断り",
|
||||
"favorites": "お気に入り",
|
||||
"follow": "フォロー",
|
||||
"follow_sent": "リクエストを送りました!",
|
||||
"follow_progress": "リクエストしています…",
|
||||
"follow_again": "再びリクエストを送りますか?",
|
||||
"follow_unfollow": "フォローをやめる",
|
||||
"followees": "フォロー",
|
||||
"followers": "フォロワー",
|
||||
"following": "フォローしています!",
|
||||
"follows_you": "フォローされました!",
|
||||
"its_you": "これはあなたです!",
|
||||
"media": "メディア",
|
||||
"mute": "ミュート",
|
||||
"muted": "ミュートしています!",
|
||||
"per_day": "/日",
|
||||
"remote_follow": "リモートフォロー",
|
||||
"statuses": "ステータス"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "ユーザータイムライン"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "詳細",
|
||||
"who_to_follow": "おすすめユーザー"
|
||||
},
|
||||
"tool_tip": {
|
||||
"media_upload": "メディアをアップロード",
|
||||
"repeat": "リピート",
|
||||
"reply": "返信",
|
||||
"favorite": "お気に入り",
|
||||
"user_settings": "ユーザー設定"
|
||||
},
|
||||
"upload":{
|
||||
"error": {
|
||||
"base": "アップロードに失敗しました。",
|
||||
"file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
|
||||
"default": "しばらくしてから試してください"
|
||||
},
|
||||
"file_size_units": {
|
||||
"B": "B",
|
||||
"KiB": "KiB",
|
||||
"MiB": "MiB",
|
||||
"GiB": "GiB",
|
||||
"TiB": "TiB"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ const messages = {
|
|||
hu: require('./hu.json'),
|
||||
it: require('./it.json'),
|
||||
ja: require('./ja.json'),
|
||||
ja_pedantic: require('./ja_pedantic.json'),
|
||||
ko: require('./ko.json'),
|
||||
nb: require('./nb.json'),
|
||||
nl: require('./nl.json'),
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"chat": {
|
||||
"title": "Messatjariá"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Exportar",
|
||||
"processing": "Tractament, vos demandarem lèu de telecargar lo fichièr"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "Chat",
|
||||
|
@ -20,13 +24,21 @@
|
|||
"submit": "Mandar",
|
||||
"more": "Mai",
|
||||
"generic_error": "Una error s’es producha",
|
||||
"optional": "opcional"
|
||||
"optional": "opcional",
|
||||
"show_more": "Mostrar mai",
|
||||
"show_less": "Mostrar mens",
|
||||
"cancel": "Anullar"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Talhar l’imatge",
|
||||
"save": "Salvar",
|
||||
"save_without_cropping": "Salvar sens talhada",
|
||||
"cancel": "Anullar"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "Mandar",
|
||||
"success": "Corrèctament importat.",
|
||||
"error": "Una error s’es producha pendent l’importacion d’aqueste fichièr."
|
||||
},
|
||||
"login": {
|
||||
"login": "Connexion",
|
||||
|
@ -74,11 +86,13 @@
|
|||
"content_type": {
|
||||
"text/plain": "Tèxte brut",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Avís de contengut (opcional)",
|
||||
"default": "Escrivètz aquí vòstre estatut.",
|
||||
"direct_warning": "Aquesta publicacion serà pas que visibla pels utilizaires mencionats.",
|
||||
"direct_warning_to_all": "Aquesta publicacion serà pas que visibla pels utilizaires mencionats.",
|
||||
"direct_warning_to_first_only": "Aquesta publicacion serà pas que visibla pels utilizaires mencionats a la debuta del messatge.",
|
||||
"posting": "Mandadís",
|
||||
"scope": {
|
||||
"direct": "Dirècte - Publicar pels utilizaires mencionats solament",
|
||||
|
@ -108,6 +122,9 @@
|
|||
"password_confirmation_match": "deu èsser lo meteis senhal"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "O seleccionar tot"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "Nom de l’aplicacion",
|
||||
"attachmentRadius": "Pèças juntas",
|
||||
|
@ -118,6 +135,11 @@
|
|||
"avatarRadius": "Avatars",
|
||||
"background": "Rèire plan",
|
||||
"bio": "Biografia",
|
||||
"block_export": "Exportar los blocatges",
|
||||
"block_export_button": "Exportar los blocatges dins un fichièr csv",
|
||||
"block_import": "Impòrt de blocatges",
|
||||
"block_import_error": "Error en importar los blocatges",
|
||||
"blocks_imported": "Blocatges importats ! Lo tractament tardarà un pauc.",
|
||||
"blocks_tab": "Blocatges",
|
||||
"btnRadius": "Botons",
|
||||
"cBlue": "Blau (Respondre, seguir)",
|
||||
|
@ -145,7 +167,6 @@
|
|||
"filtering_explanation": "Totes los estatuts amb aqueles mots seràn en silenci, un mot per linha",
|
||||
"follow_export": "Exportar los abonaments",
|
||||
"follow_export_button": "Exportar vòstres abonaments dins un fichièr csv",
|
||||
"follow_export_processing": "Tractament, vos demandarem lèu de telecargar lo fichièr",
|
||||
"follow_import": "Importar los abonaments",
|
||||
"follow_import_error": "Error en important los seguidors",
|
||||
"follows_imported": "Seguidors importats. Lo tractament pòt trigar una estona.",
|
||||
|
@ -180,6 +201,7 @@
|
|||
"use_contain_fit": "Talhar pas las pèças juntas per las vinhetas",
|
||||
"name": "Nom",
|
||||
"name_bio": "Nom & Bio",
|
||||
|
||||
"new_password": "Nòu senhal",
|
||||
"notification_visibility_follows": "Abonaments",
|
||||
"notification_visibility_likes": "Aimar",
|
||||
|
@ -213,8 +235,11 @@
|
|||
"reply_visibility_self": "Mostrar pas que las responsas que me son destinadas",
|
||||
"saving_err": "Error en enregistrant los paramètres",
|
||||
"saving_ok": "Paramètres enregistrats",
|
||||
"scope_copy": "Copiar lo nivèl de confidencialitat per las responsas (Totjorn aissí pels Messatges Dirèctes)",
|
||||
"search_user_to_block": "Cercatz qual volètz blocar",
|
||||
"search_user_to_mute": "Cercatz qual volètz rescondre",
|
||||
"security_tab": "Seguretat",
|
||||
"scope_copy": "Copiar lo nivèl de confidencialitat per las responsas (Totjorn aissí pels Messatges Dirèctes)",
|
||||
"minimal_scopes_mode": "Minimizar lo nombre d’opcions per publicacion",
|
||||
"set_new_avatar": "Definir un nòu avatar",
|
||||
"set_new_profile_background": "Definir un nòu fons de perfil",
|
||||
"set_new_profile_banner": "Definir una nòva bandièra de perfil",
|
||||
|
@ -349,6 +374,11 @@
|
|||
"checkbox": "Ai legit los tèrmes e condicions d’utilizacion",
|
||||
"link": "un pichon ligam simpatic"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"title": "Version",
|
||||
"backend_version": "Version Backend",
|
||||
"frontend_version": "Version Frontend"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
@ -364,6 +394,8 @@
|
|||
"no_statuses": "Cap d’estatuts"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "Li a agradat",
|
||||
"repeats": "A repetit",
|
||||
"reply_to": "Respond a",
|
||||
"replies_list": "Responsas :"
|
||||
},
|
||||
|
@ -394,7 +426,26 @@
|
|||
"block_progress": "Blocatge...",
|
||||
"unmute": "Tornar mostrar",
|
||||
"unmute_progress": "Afichatge...",
|
||||
"mute_progress": "A amagar..."
|
||||
"mute_progress": "A amagar...",
|
||||
"admin_menu": {
|
||||
"moderation": "Moderacion",
|
||||
"grant_admin": "Passar Admin",
|
||||
"revoke_admin": "Revocar Admin",
|
||||
"grant_moderator": "Passar Moderator",
|
||||
"revoke_moderator": "Revocar Moderator",
|
||||
"activate_account": "Activar lo compte",
|
||||
"deactivate_account": "Desactivar lo compte",
|
||||
"delete_account": "Suprimir lo compte",
|
||||
"force_nsfw": "Marcar totas las publicacions coma sensiblas",
|
||||
"strip_media": "Tirar los mèdias de las publicacions",
|
||||
"force_unlisted": "Forçar las publicacions en pas-listadas",
|
||||
"sandbox": "Forçar las publicacions en seguidors solament",
|
||||
"disable_remote_subscription": "Desactivar lo seguiment d’utilizaire d’instàncias alonhadas",
|
||||
"disable_any_subscription": "Desactivar tot seguiment",
|
||||
"quarantine": "Defendre la federacion de las publicacions de l’utilizaire",
|
||||
"delete_user": "Suprimir l’utilizaire",
|
||||
"delete_user_confirmation": "Volètz vertadièrament far aquò ? Aquesta accion se pòt pas anullar."
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Flux utilizaire",
|
||||
|
|
|
@ -74,7 +74,8 @@
|
|||
"content_type": {
|
||||
"text/plain": "Czysty tekst",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Temat (nieobowiązkowy)",
|
||||
"default": "Właśnie wróciłem z kościoła",
|
||||
|
@ -362,7 +363,7 @@
|
|||
"error_fetching": "Błąd pobierania",
|
||||
"load_older": "Załaduj starsze statusy",
|
||||
"no_retweet_hint": "Wpis oznaczony jako tylko dla obserwujących lub bezpośredni nie może zostać powtórzony",
|
||||
"repeated": "powtórzono",
|
||||
"repeated": "powtórzył(-a)",
|
||||
"show_new": "Pokaż nowe",
|
||||
"up_to_date": "Na bieżąco",
|
||||
"no_more_statuses": "Brak kolejnych statusów",
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"back": "Назад",
|
||||
"chat": "Локальный чат",
|
||||
"mentions": "Упоминания",
|
||||
"interactions": "Взаимодействия",
|
||||
"public_tl": "Публичная лента",
|
||||
"timeline": "Лента",
|
||||
"twkn": "Федеративная лента"
|
||||
|
@ -36,14 +37,24 @@
|
|||
"read": "Прочесть",
|
||||
"repeated_you": "повторил(а) ваш статус"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Повторы и фавориты",
|
||||
"follows": "Новые подписки",
|
||||
"load_older": "Загрузить старые взаимодействия"
|
||||
},
|
||||
"post_status": {
|
||||
"account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может зафоловить вас чтобы прочитать посты только для подписчиков",
|
||||
"account_not_locked_warning_link": "залочен",
|
||||
"attachments_sensitive": "Вложения содержат чувствительный контент",
|
||||
"content_warning": "Тема (не обязательно)",
|
||||
"default": "Что нового?",
|
||||
"direct_warning": "Этот пост будет видет только упомянутым пользователям",
|
||||
"direct_warning": "Этот пост будет виден только упомянутым пользователям",
|
||||
"posting": "Отправляется",
|
||||
"scope_notice": {
|
||||
"public": "Этот пост будет виден всем",
|
||||
"private": "Этот пост будет виден только вашим подписчикам",
|
||||
"unlisted": "Этот пост не будет виден в публичной и федеративной ленте"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "Личное - этот пост видят только те кто в нём упомянут",
|
||||
"private": "Для подписчиков - этот пост видят только подписчики",
|
||||
|
@ -152,6 +163,7 @@
|
|||
"reply_visibility_all": "Показывать все ответы",
|
||||
"reply_visibility_following": "Показывать только ответы мне и тех на кого я подписан",
|
||||
"reply_visibility_self": "Показывать только ответы мне",
|
||||
"autohide_floating_post_button": "Автоматически скрывать кнопку постинга (в мобильной версии)",
|
||||
"saving_err": "Не удалось сохранить настройки",
|
||||
"saving_ok": "Сохранено",
|
||||
"security_tab": "Безопасность",
|
||||
|
|
11
src/main.js
11
src/main.js
|
@ -13,6 +13,7 @@ import oauthModule from './modules/oauth.js'
|
|||
import mediaViewerModule from './modules/media_viewer.js'
|
||||
import oauthTokensModule from './modules/oauth_tokens.js'
|
||||
import pollModule from './modules/poll.js'
|
||||
import reportsModule from './modules/reports.js'
|
||||
|
||||
import VueTimeago from 'vue-timeago'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
@ -23,6 +24,7 @@ import pushNotifications from './lib/push_notifications_plugin.js'
|
|||
import messages from './i18n/messages.js'
|
||||
|
||||
import VueChatScroll from 'vue-chat-scroll'
|
||||
import VueClickOutside from 'v-click-outside'
|
||||
|
||||
import afterStoreSetup from './boot/after_store.js'
|
||||
|
||||
|
@ -40,6 +42,7 @@ Vue.use(VueTimeago, {
|
|||
})
|
||||
Vue.use(VueI18n)
|
||||
Vue.use(VueChatScroll)
|
||||
Vue.use(VueClickOutside)
|
||||
|
||||
const i18n = new VueI18n({
|
||||
// By default, use the browser locale, we will update it if neccessary
|
||||
|
@ -60,6 +63,11 @@ const persistedStateOptions = {
|
|||
const persistedState = await createPersistedState(persistedStateOptions)
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
i18n: {
|
||||
getters: {
|
||||
i18n: () => i18n
|
||||
}
|
||||
},
|
||||
interface: interfaceModule,
|
||||
instance: instanceModule,
|
||||
statuses: statusesModule,
|
||||
|
@ -70,7 +78,8 @@ const persistedStateOptions = {
|
|||
oauth: oauthModule,
|
||||
mediaViewer: mediaViewerModule,
|
||||
oauthTokens: oauthTokensModule,
|
||||
poll: pollModule
|
||||
poll: pollModule,
|
||||
reports: reportsModule
|
||||
},
|
||||
plugins: [persistedState, pushNotifications],
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { Socket } from 'phoenix'
|
||||
// import { Socket } from 'phoenix'
|
||||
|
||||
const api = {
|
||||
state: {
|
||||
|
@ -58,10 +58,10 @@ const api = {
|
|||
initializeSocket (store) {
|
||||
// Set up websocket connection
|
||||
if (!store.state.chatDisabled) {
|
||||
const token = store.state.wsToken
|
||||
const socket = new Socket('/socket', {params: {token}})
|
||||
socket.connect()
|
||||
store.dispatch('initializeChat', socket)
|
||||
// const token = store.state.wsToken
|
||||
// const socket = new Socket('/socket', {params: {token}})
|
||||
// socket.connect()
|
||||
// store.dispatch('initializeChat', socket)
|
||||
}
|
||||
},
|
||||
disableChat (store) {
|
||||
|
|
|
@ -17,6 +17,7 @@ const defaultState = {
|
|||
autoLoad: true,
|
||||
streaming: false,
|
||||
hoverPreview: true,
|
||||
autohideFloatingPostButton: false,
|
||||
pauseOnUnfocused: true,
|
||||
stopGifs: false,
|
||||
replyVisibility: 'all',
|
||||
|
@ -30,6 +31,7 @@ const defaultState = {
|
|||
muteWords: [],
|
||||
highlight: {},
|
||||
interfaceLanguage: browserLocale,
|
||||
hideScopeNotice: false,
|
||||
scopeCopy: undefined, // instance default
|
||||
subjectLineBehavior: undefined, // instance default
|
||||
alwaysShowSubjectInput: undefined, // instance default
|
||||
|
|
|
@ -54,7 +54,14 @@ const defaultState = {
|
|||
|
||||
// Version Information
|
||||
backendVersion: '',
|
||||
frontendVersion: ''
|
||||
frontendVersion: '',
|
||||
|
||||
pollLimits: {
|
||||
max_options: 4,
|
||||
max_option_chars: 255,
|
||||
min_expiration: 60,
|
||||
max_expiration: 300
|
||||
}
|
||||
}
|
||||
|
||||
const instance = {
|
||||
|
|
30
src/modules/reports.js
Normal file
30
src/modules/reports.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import filter from 'lodash/filter'
|
||||
|
||||
const reports = {
|
||||
state: {
|
||||
userId: null,
|
||||
statuses: [],
|
||||
modalActivated: false
|
||||
},
|
||||
mutations: {
|
||||
openUserReportingModal (state, { userId, statuses }) {
|
||||
state.userId = userId
|
||||
state.statuses = statuses
|
||||
state.modalActivated = true
|
||||
},
|
||||
closeUserReportingModal (state) {
|
||||
state.modalActivated = false
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
openUserReportingModal ({ rootState, commit }, userId) {
|
||||
const statuses = filter(rootState.statuses.allStatuses, status => status.user.id === userId)
|
||||
commit('openUserReportingModal', { userId, statuses })
|
||||
},
|
||||
closeUserReportingModal ({ commit }) {
|
||||
commit('closeUserReportingModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default reports
|
|
@ -1,4 +1,4 @@
|
|||
import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
|
||||
import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import apiService from '../services/api/api.service.js'
|
||||
// import parse from '../services/status_parser/status_parser.js'
|
||||
|
@ -33,6 +33,7 @@ const emptyNotifications = () => ({
|
|||
export const defaultState = () => ({
|
||||
allStatuses: [],
|
||||
allStatusesObject: {},
|
||||
conversationsObject: {},
|
||||
maxId: 0,
|
||||
notifications: emptyNotifications(),
|
||||
favorites: new Set(),
|
||||
|
@ -112,6 +113,39 @@ const sortTimeline = (timeline) => {
|
|||
return timeline
|
||||
}
|
||||
|
||||
// Add status to the global storages (arrays and objects maintaining statuses) except timelines
|
||||
const addStatusToGlobalStorage = (state, data) => {
|
||||
const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data)
|
||||
if (result.new) {
|
||||
// Add to conversation
|
||||
const status = result.item
|
||||
const conversationsObject = state.conversationsObject
|
||||
const conversationId = status.statusnet_conversation_id
|
||||
if (conversationsObject[conversationId]) {
|
||||
conversationsObject[conversationId].push(status)
|
||||
} else {
|
||||
set(conversationsObject, conversationId, [status])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Remove status from the global storages (arrays and objects maintaining statuses) except timelines
|
||||
const removeStatusFromGlobalStorage = (state, status) => {
|
||||
remove(state.allStatuses, { id: status.id })
|
||||
|
||||
// TODO: Need to remove from allStatusesObject?
|
||||
|
||||
// Remove possible notification
|
||||
remove(state.notifications.data, ({action: {id}}) => id === status.id)
|
||||
|
||||
// Remove from conversation
|
||||
const conversationId = status.statusnet_conversation_id
|
||||
if (state.conversationsObject[conversationId]) {
|
||||
remove(state.conversationsObject[conversationId], { id: status.id })
|
||||
}
|
||||
}
|
||||
|
||||
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
|
||||
// Sanity check
|
||||
if (!isArray(statuses)) {
|
||||
|
@ -119,7 +153,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
|
||||
const allStatuses = state.allStatuses
|
||||
const allStatusesObject = state.allStatusesObject
|
||||
const timelineObject = state.timelines[timeline]
|
||||
|
||||
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
|
||||
|
@ -142,7 +175,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
|
||||
const addStatus = (data, showImmediately, addToTimeline = true) => {
|
||||
const result = mergeOrAdd(allStatuses, allStatusesObject, data)
|
||||
const result = addStatusToGlobalStorage(state, data)
|
||||
const status = result.item
|
||||
|
||||
if (result.new) {
|
||||
|
@ -236,16 +269,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
},
|
||||
'deletion': (deletion) => {
|
||||
const uri = deletion.uri
|
||||
|
||||
// Remove possible notification
|
||||
const status = find(allStatuses, {uri})
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
|
||||
remove(state.notifications.data, ({action: {id}}) => id === status.id)
|
||||
removeStatusFromGlobalStorage(state, status)
|
||||
|
||||
remove(allStatuses, { uri })
|
||||
if (timeline) {
|
||||
remove(timelineObject.statuses, { uri })
|
||||
remove(timelineObject.visibleStatuses, { uri })
|
||||
|
@ -272,12 +302,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
}
|
||||
|
||||
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
|
||||
const allStatuses = state.allStatuses
|
||||
const allStatusesObject = state.allStatusesObject
|
||||
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
|
||||
each(notifications, (notification) => {
|
||||
notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
|
||||
notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
|
||||
if (notification.type !== 'follow') {
|
||||
notification.action = addStatusToGlobalStorage(state, notification.action).item
|
||||
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
|
||||
}
|
||||
|
||||
// Only add a new notification if we don't have one for the same action
|
||||
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
|
||||
|
@ -293,15 +323,32 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
|
||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
||||
const notifObj = {}
|
||||
const action = notification.action
|
||||
const title = action.user.name
|
||||
notifObj.icon = action.user.profile_image_url
|
||||
notifObj.body = action.text // there's a problem that it doesn't put a space before links tho
|
||||
const status = notification.status
|
||||
const title = notification.from_profile.name
|
||||
notifObj.icon = notification.from_profile.profile_image_url
|
||||
let i18nString
|
||||
switch (notification.type) {
|
||||
case 'like':
|
||||
i18nString = 'favorited_you'
|
||||
break
|
||||
case 'repeat':
|
||||
i18nString = 'repeated_you'
|
||||
break
|
||||
case 'follow':
|
||||
i18nString = 'followed_you'
|
||||
break
|
||||
}
|
||||
|
||||
if (i18nString) {
|
||||
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
|
||||
} else {
|
||||
notifObj.body = notification.status.text
|
||||
}
|
||||
|
||||
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
||||
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
|
||||
action.attachments[0].mimetype.startsWith('image/')) {
|
||||
notifObj.image = action.attachments[0].url
|
||||
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
|
||||
status.attachments[0].mimetype.startsWith('image/')) {
|
||||
notifObj.image = status.attachments[0].url
|
||||
}
|
||||
|
||||
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
|
||||
|
@ -355,12 +402,31 @@ export const mutations = {
|
|||
},
|
||||
setFavorited (state, { status, value }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
|
||||
if (newStatus.favorited !== value) {
|
||||
if (value) {
|
||||
newStatus.fave_num++
|
||||
} else {
|
||||
newStatus.fave_num--
|
||||
}
|
||||
}
|
||||
|
||||
newStatus.favorited = value
|
||||
},
|
||||
setFavoritedConfirm (state, { status }) {
|
||||
setFavoritedConfirm (state, { status, user }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.favorited = status.favorited
|
||||
newStatus.fave_num = status.fave_num
|
||||
const index = findIndex(newStatus.favoritedBy, { id: user.id })
|
||||
if (index !== -1 && !newStatus.favorited) {
|
||||
newStatus.favoritedBy.splice(index, 1)
|
||||
} else if (index === -1 && newStatus.favorited) {
|
||||
newStatus.favoritedBy.push(user)
|
||||
}
|
||||
},
|
||||
setPinned (state, status) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.pinned = status.pinned
|
||||
},
|
||||
setRetweeted (state, { status, value }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
|
@ -375,6 +441,17 @@ export const mutations = {
|
|||
|
||||
newStatus.repeated = value
|
||||
},
|
||||
setRetweetedConfirm (state, { status, user }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.repeated = status.repeated
|
||||
newStatus.repeat_num = status.repeat_num
|
||||
const index = findIndex(newStatus.rebloggedBy, { id: user.id })
|
||||
if (index !== -1 && !newStatus.repeated) {
|
||||
newStatus.rebloggedBy.splice(index, 1)
|
||||
} else if (index === -1 && newStatus.repeated) {
|
||||
newStatus.rebloggedBy.push(user)
|
||||
}
|
||||
},
|
||||
setDeleted (state, { status }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.deleted = true
|
||||
|
@ -412,6 +489,11 @@ export const mutations = {
|
|||
},
|
||||
queueFlush (state, { timeline, id }) {
|
||||
state.timelines[timeline].flushMarker = id
|
||||
},
|
||||
addFavsAndRepeats (state, { id, favoritedByUsers, rebloggedByUsers }) {
|
||||
const newStatus = state.allStatusesObject[id]
|
||||
newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
|
||||
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -421,8 +503,8 @@ const statuses = {
|
|||
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
|
||||
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
|
||||
},
|
||||
addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
|
||||
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
|
||||
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
|
||||
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
|
||||
},
|
||||
setError ({ rootState, commit }, { value }) {
|
||||
commit('setError', { value })
|
||||
|
@ -446,27 +528,38 @@ const statuses = {
|
|||
favorite ({ rootState, commit }, status) {
|
||||
// Optimistic favoriting...
|
||||
commit('setFavorited', { status, value: true })
|
||||
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||
.then(status => {
|
||||
commit('setFavoritedConfirm', { status })
|
||||
})
|
||||
rootState.api.backendInteractor.favorite(status.id)
|
||||
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
|
||||
},
|
||||
unfavorite ({ rootState, commit }, status) {
|
||||
// Optimistic favoriting...
|
||||
// Optimistic unfavoriting...
|
||||
commit('setFavorited', { status, value: false })
|
||||
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||
.then(status => {
|
||||
commit('setFavoritedConfirm', { status })
|
||||
})
|
||||
rootState.api.backendInteractor.unfavorite(status.id)
|
||||
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
|
||||
},
|
||||
fetchPinnedStatuses ({ rootState, dispatch }, userId) {
|
||||
rootState.api.backendInteractor.fetchPinnedStatuses(userId)
|
||||
.then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true }))
|
||||
},
|
||||
pinStatus ({ rootState, commit }, statusId) {
|
||||
return rootState.api.backendInteractor.pinOwnStatus(statusId)
|
||||
.then((status) => commit('setPinned', status))
|
||||
},
|
||||
unpinStatus ({ rootState, commit }, statusId) {
|
||||
rootState.api.backendInteractor.unpinOwnStatus(statusId)
|
||||
.then((status) => commit('setPinned', status))
|
||||
},
|
||||
retweet ({ rootState, commit }, status) {
|
||||
// Optimistic retweeting...
|
||||
commit('setRetweeted', { status, value: true })
|
||||
apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||
rootState.api.backendInteractor.retweet(status.id)
|
||||
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
|
||||
},
|
||||
unretweet ({ rootState, commit }, status) {
|
||||
// Optimistic unretweeting...
|
||||
commit('setRetweeted', { status, value: false })
|
||||
apiService.unretweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||
rootState.api.backendInteractor.unretweet(status.id)
|
||||
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
|
||||
},
|
||||
queueFlush ({ rootState, commit }, { timeline, id }) {
|
||||
commit('queueFlush', { timeline, id })
|
||||
|
@ -477,6 +570,14 @@ const statuses = {
|
|||
id: rootState.statuses.notifications.maxId,
|
||||
credentials: rootState.users.currentUser.credentials
|
||||
})
|
||||
},
|
||||
fetchFavsAndRepeats ({ rootState, commit }, id) {
|
||||
Promise.all([
|
||||
rootState.api.backendInteractor.fetchFavoritedByUsers(id),
|
||||
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
|
||||
]).then(([favoritedByUsers, rebloggedByUsers]) =>
|
||||
commit('addFavsAndRepeats', { id, favoritedByUsers, rebloggedByUsers })
|
||||
)
|
||||
}
|
||||
},
|
||||
mutations
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { compact, map, each, merge, find, last } from 'lodash'
|
||||
import userSearchApi from '../services/new_api/user_search.js'
|
||||
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||
import oauthApi from '../services/new_api/oauth'
|
||||
|
@ -32,6 +33,35 @@ const getNotificationPermission = () => {
|
|||
return Promise.resolve(Notification.permission)
|
||||
}
|
||||
|
||||
const blockUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.blockUser(id)
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
store.commit('addBlockId', id)
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: id })
|
||||
store.commit('removeStatus', { timeline: 'public', userId: id })
|
||||
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
|
||||
})
|
||||
}
|
||||
|
||||
const unblockUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.unblockUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
}
|
||||
|
||||
const muteUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.muteUser(id)
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
store.commit('addMuteId', id)
|
||||
})
|
||||
}
|
||||
|
||||
const unmuteUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.unmuteUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setMuted (state, { user: { id }, muted }) {
|
||||
const user = state.usersObject[id]
|
||||
|
@ -73,42 +103,27 @@ export const mutations = {
|
|||
endLogin (state) {
|
||||
state.loggingIn = false
|
||||
},
|
||||
// TODO Clean after ourselves?
|
||||
addFriends (state, { id, friends }) {
|
||||
saveFriendIds (state, { id, friendIds }) {
|
||||
const user = state.usersObject[id]
|
||||
each(friends, friend => {
|
||||
if (!find(user.friends, { id: friend.id })) {
|
||||
user.friends.push(friend)
|
||||
}
|
||||
})
|
||||
user.lastFriendId = last(friends).id
|
||||
user.friendIds = uniq(concat(user.friendIds, friendIds))
|
||||
},
|
||||
addFollowers (state, { id, followers }) {
|
||||
saveFollowerIds (state, { id, followerIds }) {
|
||||
const user = state.usersObject[id]
|
||||
each(followers, follower => {
|
||||
if (!find(user.followers, { id: follower.id })) {
|
||||
user.followers.push(follower)
|
||||
}
|
||||
})
|
||||
user.lastFollowerId = last(followers).id
|
||||
user.followerIds = uniq(concat(user.followerIds, followerIds))
|
||||
},
|
||||
// Because frontend doesn't have a reason to keep these stuff in memory
|
||||
// outside of viewing someones user profile.
|
||||
clearFriends (state, userId) {
|
||||
const user = state.usersObject[userId]
|
||||
if (!user) {
|
||||
return
|
||||
if (user) {
|
||||
set(user, 'friendIds', [])
|
||||
}
|
||||
user.friends = []
|
||||
user.lastFriendId = null
|
||||
},
|
||||
clearFollowers (state, userId) {
|
||||
const user = state.usersObject[userId]
|
||||
if (!user) {
|
||||
return
|
||||
if (user) {
|
||||
set(user, 'followerIds', [])
|
||||
}
|
||||
user.followers = []
|
||||
user.lastFollowerId = null
|
||||
},
|
||||
addNewUsers (state, users) {
|
||||
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
||||
|
@ -132,6 +147,11 @@ export const mutations = {
|
|||
saveBlockIds (state, blockIds) {
|
||||
state.currentUser.blockIds = blockIds
|
||||
},
|
||||
addBlockId (state, blockId) {
|
||||
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
|
||||
state.currentUser.blockIds.push(blockId)
|
||||
}
|
||||
},
|
||||
updateMutes (state, mutedUsers) {
|
||||
// Reset muted of all fetched users
|
||||
each(state.users, (user) => { user.muted = false })
|
||||
|
@ -140,12 +160,28 @@ export const mutations = {
|
|||
saveMuteIds (state, muteIds) {
|
||||
state.currentUser.muteIds = muteIds
|
||||
},
|
||||
addMuteId (state, muteId) {
|
||||
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
|
||||
state.currentUser.muteIds.push(muteId)
|
||||
}
|
||||
},
|
||||
setPinned (state, status) {
|
||||
const user = state.usersObject[status.user.id]
|
||||
const index = user.pinnedStatuseIds.indexOf(status.id)
|
||||
if (status.pinned && index === -1) {
|
||||
user.pinnedStatuseIds.push(status.id)
|
||||
} else if (!status.pinned && index !== -1) {
|
||||
user.pinnedStatuseIds.splice(index, 1)
|
||||
}
|
||||
},
|
||||
setUserForStatus (state, status) {
|
||||
status.user = state.usersObject[status.user.id]
|
||||
},
|
||||
setUserForNotification (state, notification) {
|
||||
if (notification.type !== 'follow') {
|
||||
notification.action.user = state.usersObject[notification.action.user.id]
|
||||
notification.from_profile = state.usersObject[notification.action.user.id]
|
||||
}
|
||||
notification.from_profile = state.usersObject[notification.from_profile.id]
|
||||
},
|
||||
setColor (state, { user: { id }, highlighted }) {
|
||||
const user = state.usersObject[id]
|
||||
|
@ -198,8 +234,10 @@ const users = {
|
|||
})
|
||||
},
|
||||
fetchUserRelationship (store, id) {
|
||||
return store.rootState.api.backendInteractor.fetchUserRelationship({ id })
|
||||
if (store.state.currentUser) {
|
||||
store.rootState.api.backendInteractor.fetchUserRelationship({ id })
|
||||
.then((relationships) => store.commit('updateUserRelationship', relationships))
|
||||
}
|
||||
},
|
||||
fetchBlocks (store) {
|
||||
return store.rootState.api.backendInteractor.fetchBlocks()
|
||||
|
@ -209,18 +247,17 @@ const users = {
|
|||
return blocks
|
||||
})
|
||||
},
|
||||
blockUser (store, userId) {
|
||||
return store.rootState.api.backendInteractor.blockUser(userId)
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
store.commit('removeStatus', { timeline: 'friends', userId })
|
||||
store.commit('removeStatus', { timeline: 'public', userId })
|
||||
store.commit('removeStatus', { timeline: 'publicAndExternal', userId })
|
||||
})
|
||||
blockUser (store, id) {
|
||||
return blockUser(store, id)
|
||||
},
|
||||
unblockUser (store, id) {
|
||||
return store.rootState.api.backendInteractor.unblockUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
return unblockUser(store, id)
|
||||
},
|
||||
blockUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => blockUser(store, id)))
|
||||
},
|
||||
unblockUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => unblockUser(store, id)))
|
||||
},
|
||||
fetchMutes (store) {
|
||||
return store.rootState.api.backendInteractor.fetchMutes()
|
||||
|
@ -231,32 +268,34 @@ const users = {
|
|||
})
|
||||
},
|
||||
muteUser (store, id) {
|
||||
return store.rootState.api.backendInteractor.muteUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
return muteUser(store, id)
|
||||
},
|
||||
unmuteUser (store, id) {
|
||||
return store.rootState.api.backendInteractor.unmuteUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
return unmuteUser(store, id)
|
||||
},
|
||||
addFriends ({ rootState, commit }, fetchBy) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const user = rootState.users.usersObject[fetchBy]
|
||||
const maxId = user.lastFriendId
|
||||
rootState.api.backendInteractor.fetchFriends({ id: user.id, maxId })
|
||||
muteUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => muteUser(store, id)))
|
||||
},
|
||||
unmuteUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => unmuteUser(store, id)))
|
||||
},
|
||||
fetchFriends ({ rootState, commit }, id) {
|
||||
const user = rootState.users.usersObject[id]
|
||||
const maxId = last(user.friendIds)
|
||||
return rootState.api.backendInteractor.fetchFriends({ id, maxId })
|
||||
.then((friends) => {
|
||||
commit('addFriends', { id: user.id, friends })
|
||||
resolve(friends)
|
||||
}).catch(() => {
|
||||
reject()
|
||||
})
|
||||
commit('addNewUsers', friends)
|
||||
commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
|
||||
return friends
|
||||
})
|
||||
},
|
||||
addFollowers ({ rootState, commit }, fetchBy) {
|
||||
const user = rootState.users.usersObject[fetchBy]
|
||||
const maxId = user.lastFollowerId
|
||||
return rootState.api.backendInteractor.fetchFollowers({ id: user.id, maxId })
|
||||
fetchFollowers ({ rootState, commit }, id) {
|
||||
const user = rootState.users.usersObject[id]
|
||||
const maxId = last(user.followerIds)
|
||||
return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
|
||||
.then((followers) => {
|
||||
commit('addFollowers', { id: user.id, followers })
|
||||
commit('addNewUsers', followers)
|
||||
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
|
||||
return followers
|
||||
})
|
||||
},
|
||||
|
@ -279,19 +318,26 @@ const users = {
|
|||
|
||||
unregisterPushNotifications(token)
|
||||
},
|
||||
addNewUsers ({ commit }, users) {
|
||||
commit('addNewUsers', users)
|
||||
},
|
||||
addNewStatuses (store, { statuses }) {
|
||||
const users = map(statuses, 'user')
|
||||
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
||||
store.commit('addNewUsers', users)
|
||||
store.commit('addNewUsers', retweetedUsers)
|
||||
|
||||
// Reconnect users to statuses
|
||||
each(statuses, (status) => {
|
||||
// Reconnect users to statuses
|
||||
store.commit('setUserForStatus', status)
|
||||
// Set pinned statuses to user
|
||||
store.commit('setPinned', status)
|
||||
})
|
||||
// Reconnect users to retweets
|
||||
each(compact(map(statuses, 'retweeted_status')), (status) => {
|
||||
// Reconnect users to retweets
|
||||
store.commit('setUserForStatus', status)
|
||||
// Set pinned retweets to user
|
||||
store.commit('setPinned', status)
|
||||
})
|
||||
},
|
||||
addNewNotifications (store, { notifications }) {
|
||||
|
@ -309,6 +355,14 @@ const users = {
|
|||
store.commit('setUserForNotification', notification)
|
||||
})
|
||||
},
|
||||
searchUsers (store, query) {
|
||||
// TODO: Move userSearch api into api.service
|
||||
return userSearchApi.search({query, store: { state: store.rootState }})
|
||||
.then((users) => {
|
||||
store.commit('addNewUsers', users)
|
||||
return users
|
||||
})
|
||||
},
|
||||
async signUp (store, userInfo) {
|
||||
store.commit('signUpPending')
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue