Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma-fe into fine_grained_moderation_privileges
This commit is contained in:
commit
5541d0ec29
2
.babelrc
2
.babelrc
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env"],
|
"presets": ["@babel/preset-env"],
|
||||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||||
"comments": false
|
"comments": true
|
||||||
}
|
}
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ test/e2e/reports
|
|||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
.idea/
|
.idea/
|
||||||
config/local.json
|
config/local.json
|
||||||
|
static/emoji.json
|
||||||
|
@ -10,3 +10,5 @@ Contributors of this project.
|
|||||||
- shpuld (shpuld@shitposter.club): CSS and styling
|
- shpuld (shpuld@shitposter.club): CSS and styling
|
||||||
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
|
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
|
||||||
- hj (hj@shigusegubu.club): Code
|
- hj (hj@shigusegubu.club): Code
|
||||||
|
- Sean King (seanking@freespeechextremist.com): Code
|
||||||
|
- Tusooa Zhu (tusooa@kazv.moe): Code
|
||||||
|
@ -18,6 +18,9 @@ console.log(
|
|||||||
var spinner = ora('building for production...')
|
var spinner = ora('building for production...')
|
||||||
spinner.start()
|
spinner.start()
|
||||||
|
|
||||||
|
var updateEmoji = require('./update-emoji').updateEmoji
|
||||||
|
updateEmoji()
|
||||||
|
|
||||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||||
rm('-rf', assetsPath)
|
rm('-rf', assetsPath)
|
||||||
mkdir('-p', assetsPath)
|
mkdir('-p', assetsPath)
|
||||||
|
@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
|
|||||||
? require('./webpack.prod.conf')
|
? require('./webpack.prod.conf')
|
||||||
: require('./webpack.dev.conf')
|
: require('./webpack.dev.conf')
|
||||||
|
|
||||||
|
var updateEmoji = require('./update-emoji').updateEmoji
|
||||||
|
updateEmoji()
|
||||||
|
|
||||||
// default port where dev server listens for incoming traffic
|
// default port where dev server listens for incoming traffic
|
||||||
var port = process.env.PORT || config.dev.port
|
var port = process.env.PORT || config.dev.port
|
||||||
// Define HTTP proxies to your custom API backend
|
// Define HTTP proxies to your custom API backend
|
||||||
@ -29,18 +32,20 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
var hotMiddleware = require('webpack-hot-middleware')(compiler)
|
var hotMiddleware = require('webpack-hot-middleware')(compiler)
|
||||||
// force page reload when html-webpack-plugin template changes
|
|
||||||
compiler.plugin('compilation', function (compilation) {
|
|
||||||
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
|
||||||
// FIXME: This supposed to reload whole page when index.html is changed,
|
|
||||||
// however now it reloads entire page on every breath, i suppose the order
|
|
||||||
// of plugins changed or something. It's a minor thing and douesn't hurt
|
|
||||||
// disabling it, constant reloads hurt much more
|
|
||||||
|
|
||||||
// hotMiddleware.publish({ action: 'reload' })
|
// FIXME: The statement below gives error about hooks being required in webpack 5.
|
||||||
// cb()
|
// force page reload when html-webpack-plugin template changes
|
||||||
})
|
// compiler.plugin('compilation', function (compilation) {
|
||||||
})
|
// compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||||
|
// // FIXME: This supposed to reload whole page when index.html is changed,
|
||||||
|
// // however now it reloads entire page on every breath, i suppose the order
|
||||||
|
// // of plugins changed or something. It's a minor thing and douesn't hurt
|
||||||
|
// // disabling it, constant reloads hurt much more
|
||||||
|
|
||||||
|
// // hotMiddleware.publish({ action: 'reload' })
|
||||||
|
// // cb()
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
// proxy api requests
|
// proxy api requests
|
||||||
Object.keys(proxyTable).forEach(function (context) {
|
Object.keys(proxyTable).forEach(function (context) {
|
||||||
@ -48,7 +53,7 @@ Object.keys(proxyTable).forEach(function (context) {
|
|||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
options = { target: options }
|
options = { target: options }
|
||||||
}
|
}
|
||||||
app.use(proxyMiddleware(context, options))
|
app.use(proxyMiddleware.createProxyMiddleware(context, options))
|
||||||
})
|
})
|
||||||
|
|
||||||
// handle fallback for HTML5 history API
|
// handle fallback for HTML5 history API
|
||||||
|
27
build/update-emoji.js
Normal file
27
build/update-emoji.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
module.exports = {
|
||||||
|
updateEmoji () {
|
||||||
|
const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
Object.keys(emojis)
|
||||||
|
.map(k => {
|
||||||
|
emojis[k].map(e => {
|
||||||
|
delete e.unicode_version
|
||||||
|
delete e.emoji_version
|
||||||
|
delete e.skin_tone_support_unicode_version
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = {}
|
||||||
|
Object.keys(emojis)
|
||||||
|
.map(k => {
|
||||||
|
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||||
|
res[groupId] = emojis[k]
|
||||||
|
})
|
||||||
|
|
||||||
|
console.info('Updating emojis...')
|
||||||
|
fs.writeFileSync('static/emoji.json', JSON.stringify(res))
|
||||||
|
console.info('Done.')
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ var path = require('path')
|
|||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var projectRoot = path.resolve(__dirname, '../')
|
var projectRoot = path.resolve(__dirname, '../')
|
||||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin')
|
||||||
var CopyPlugin = require('copy-webpack-plugin');
|
var CopyPlugin = require('copy-webpack-plugin');
|
||||||
var { VueLoaderPlugin } = require('vue-loader')
|
var { VueLoaderPlugin } = require('vue-loader')
|
||||||
var ESLintPlugin = require('eslint-webpack-plugin');
|
var ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
@ -24,7 +24,8 @@ module.exports = {
|
|||||||
output: {
|
output: {
|
||||||
path: config.build.assetsRoot,
|
path: config.build.assetsRoot,
|
||||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||||
filename: '[name].js'
|
filename: '[name].js',
|
||||||
|
chunkFilename: '[name].js'
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
@ -42,6 +43,10 @@ module.exports = {
|
|||||||
'assets': path.resolve(__dirname, '../src/assets'),
|
'assets': path.resolve(__dirname, '../src/assets'),
|
||||||
'components': path.resolve(__dirname, '../src/components'),
|
'components': path.resolve(__dirname, '../src/components'),
|
||||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
'querystring': require.resolve('querystring-es3'),
|
||||||
|
'url': require.resolve('url/')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
@ -78,22 +83,16 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||||
use: {
|
type: 'asset',
|
||||||
loader: 'url-loader',
|
generator: {
|
||||||
options: {
|
filename: utils.assetsPath('img/[name].[hash:7][ext]')
|
||||||
limit: 10000,
|
|
||||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||||
use: {
|
type: 'asset',
|
||||||
loader: 'url-loader',
|
generator: {
|
||||||
options: {
|
filename: utils.assetsPath('fonts/[name].[hash:7][ext]')
|
||||||
limit: 10000,
|
|
||||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -117,9 +116,8 @@ module.exports = {
|
|||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
from: "node_modules/@ruffle-rs/ruffle/*",
|
from: "node_modules/@ruffle-rs/ruffle/**/*",
|
||||||
to: "static/ruffle",
|
to: "static/ruffle/[name][ext]"
|
||||||
flatten: true
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
|
@ -16,7 +16,7 @@ module.exports = merge(baseWebpackConfig, {
|
|||||||
},
|
},
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
// eval-source-map is faster for development
|
// eval-source-map is faster for development
|
||||||
devtool: '#eval-source-map',
|
devtool: 'eval-source-map',
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': config.dev.env,
|
'process.env': config.dev.env,
|
||||||
|
@ -5,6 +5,7 @@ var webpack = require('webpack')
|
|||||||
var merge = require('webpack-merge')
|
var merge = require('webpack-merge')
|
||||||
var baseWebpackConfig = require('./webpack.base.conf')
|
var baseWebpackConfig = require('./webpack.base.conf')
|
||||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
|
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
var env = process.env.NODE_ENV === 'testing'
|
var env = process.env.NODE_ENV === 'testing'
|
||||||
? require('../config/test.env')
|
? require('../config/test.env')
|
||||||
@ -19,12 +20,16 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||||||
module: {
|
module: {
|
||||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||||
},
|
},
|
||||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
devtool: config.build.productionSourceMap ? 'source-map' : false,
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: true,
|
minimize: true,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
chunks: 'all'
|
chunks: 'all'
|
||||||
}
|
},
|
||||||
|
minimizer: [
|
||||||
|
`...`,
|
||||||
|
new CssMinimizerPlugin()
|
||||||
|
]
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: config.build.assetsRoot,
|
path: config.build.assetsRoot,
|
||||||
@ -60,9 +65,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||||||
ignoreCustomComments: [/server-generated-meta/]
|
ignoreCustomComments: [/server-generated-meta/]
|
||||||
// more options:
|
// more options:
|
||||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||||
},
|
}
|
||||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
|
||||||
chunksSortMode: 'dependency'
|
|
||||||
}),
|
}),
|
||||||
// split vendor js into its own file
|
// split vendor js into its own file
|
||||||
// extract webpack runtime and module manifest to its own file in order to
|
// extract webpack runtime and module manifest to its own file in order to
|
||||||
|
113
package.json
113
package.json
@ -18,13 +18,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.18.9",
|
"@babel/runtime": "7.18.9",
|
||||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
"@fortawesome/fontawesome-svg-core": "6.2.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.1.2",
|
"@fortawesome/free-regular-svg-icons": "6.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.1.2",
|
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||||
"@ruffle-rs/ruffle": "^0.1.0-nightly.2022.7.12",
|
"@kazvmoe-infra/unicode-emoji-json": "^0.4.0",
|
||||||
"@vuelidate/core": "2.0.0-alpha.43",
|
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
|
||||||
|
"@vuelidate/core": "2.0.0-alpha.44",
|
||||||
"@vuelidate/validators": "2.0.0-alpha.31",
|
"@vuelidate/validators": "2.0.0-alpha.31",
|
||||||
"body-scroll-lock": "3.1.5",
|
"body-scroll-lock": "3.1.5",
|
||||||
"chromatism": "3.0.0",
|
"chromatism": "3.0.0",
|
||||||
@ -32,95 +33,95 @@
|
|||||||
"cropperjs": "1.5.12",
|
"cropperjs": "1.5.12",
|
||||||
"diff": "3.5.0",
|
"diff": "3.5.0",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"parse-link-header": "1.0.1",
|
"lozad": "^1.16.0",
|
||||||
|
"parse-link-header": "2.0.0",
|
||||||
"phoenix": "1.6.2",
|
"phoenix": "1.6.2",
|
||||||
"punycode.js": "2.1.0",
|
"punycode.js": "2.1.0",
|
||||||
"qrcode": "1",
|
"qrcode": "1.5.0",
|
||||||
"utf8": "^3.0.0",
|
"querystring-es3": "0.2.1",
|
||||||
"vue": "3.2.37",
|
"url": "0.11.0",
|
||||||
"vue-i18n": "9.2.0",
|
"utf8": "3.0.0",
|
||||||
"vue-router": "4.1.3",
|
"vue": "3.2.38",
|
||||||
"vue-template-compiler": "2.7.8",
|
"vue-i18n": "9.2.2",
|
||||||
|
"vue-router": "4.1.5",
|
||||||
|
"vue-template-compiler": "2.7.10",
|
||||||
"vuex": "4.0.2"
|
"vuex": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.9",
|
"@babel/core": "7.18.13",
|
||||||
"@babel/plugin-transform-runtime": "7.18.9",
|
|
||||||
"@babel/preset-env": "7.18.9",
|
|
||||||
"@babel/register": "7.18.9",
|
|
||||||
"@babel/eslint-parser": "7.18.9",
|
"@babel/eslint-parser": "7.18.9",
|
||||||
"@intlify/vue-i18n-loader": "^5.0.0",
|
"@babel/plugin-transform-runtime": "7.18.10",
|
||||||
|
"@babel/preset-env": "7.18.10",
|
||||||
|
"@babel/register": "7.18.9",
|
||||||
|
"@intlify/vue-i18n-loader": "5.0.0",
|
||||||
"@ungap/event-target": "0.2.3",
|
"@ungap/event-target": "0.2.3",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||||
"@vue/babel-plugin-jsx": "1.1.1",
|
"@vue/babel-plugin-jsx": "1.1.1",
|
||||||
"@vue/compiler-sfc": "3.2.37",
|
"@vue/compiler-sfc": "3.2.38",
|
||||||
"@vue/test-utils": "2.0.2",
|
"@vue/test-utils": "2.0.2",
|
||||||
"autoprefixer": "6.7.7",
|
"autoprefixer": "10.4.8",
|
||||||
"babel-loader": "8.2.5",
|
"babel-loader": "8.2.5",
|
||||||
"babel-plugin-lodash": "3.3.4",
|
"babel-plugin-lodash": "3.3.4",
|
||||||
"chai": "3.5.0",
|
"chai": "4.3.6",
|
||||||
"chalk": "1.1.3",
|
"chalk": "1.1.3",
|
||||||
"chromedriver": "103.0.0",
|
"chromedriver": "104.0.0",
|
||||||
"connect-history-api-fallback": "1.6.0",
|
"connect-history-api-fallback": "2.0.0",
|
||||||
"copy-webpack-plugin": "6.4.1",
|
"copy-webpack-plugin": "11.0.0",
|
||||||
"cross-spawn": "4.0.2",
|
"cross-spawn": "7.0.3",
|
||||||
"css-loader": "0.28.11",
|
"css-loader": "6.7.1",
|
||||||
|
"css-minimizer-webpack-plugin": "4.0.0",
|
||||||
"custom-event-polyfill": "1.0.7",
|
"custom-event-polyfill": "1.0.7",
|
||||||
"eslint": "8.20.0",
|
"eslint": "8.23.0",
|
||||||
"eslint-config-standard": "17.0.0",
|
"eslint-config-standard": "17.0.0",
|
||||||
"eslint-formatter-friendly": "7.0.0",
|
"eslint-formatter-friendly": "7.0.0",
|
||||||
"eslint-webpack-plugin": "2.7.0",
|
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-n": "15.2.4",
|
"eslint-plugin-n": "15.2.5",
|
||||||
"eslint-plugin-promise": "6.0.0",
|
"eslint-plugin-promise": "6.0.1",
|
||||||
"eslint-plugin-vue": "9.3.0",
|
"eslint-plugin-vue": "9.4.0",
|
||||||
|
"eslint-webpack-plugin": "3.2.0",
|
||||||
"eventsource-polyfill": "0.9.6",
|
"eventsource-polyfill": "0.9.6",
|
||||||
"express": "4.18.1",
|
"express": "4.18.1",
|
||||||
"file-loader": "3.0.1",
|
|
||||||
"function-bind": "1.1.1",
|
"function-bind": "1.1.1",
|
||||||
"html-webpack-plugin": "3.2.0",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"http-proxy-middleware": "0.21.0",
|
"http-proxy-middleware": "2.0.6",
|
||||||
"inject-loader": "2.0.1",
|
|
||||||
"iso-639-1": "2.1.15",
|
"iso-639-1": "2.1.15",
|
||||||
"isparta-loader": "2.0.0",
|
|
||||||
"json-loader": "0.5.7",
|
"json-loader": "0.5.7",
|
||||||
"karma": "6.4.0",
|
"karma": "6.4.0",
|
||||||
"karma-coverage": "1.1.2",
|
"karma-coverage": "2.2.0",
|
||||||
"karma-firefox-launcher": "1.3.0",
|
"karma-firefox-launcher": "2.1.2",
|
||||||
"karma-mocha": "2.0.1",
|
"karma-mocha": "2.0.1",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"karma-sinon-chai": "2.0.2",
|
"karma-sinon-chai": "2.0.2",
|
||||||
"karma-sourcemap-loader": "0.3.8",
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
"karma-spec-reporter": "0.0.34",
|
"karma-spec-reporter": "0.0.34",
|
||||||
"karma-webpack": "4.0.2",
|
"karma-webpack": "5.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lolex": "1.6.0",
|
"lolex": "1.6.0",
|
||||||
"mini-css-extract-plugin": "0.12.0",
|
"mini-css-extract-plugin": "2.6.1",
|
||||||
"mocha": "3.5.3",
|
"mocha": "10.0.0",
|
||||||
"nightwatch": "0.9.21",
|
"nightwatch": "2.3.3",
|
||||||
"opn": "4.0.2",
|
"opn": "5.5.0",
|
||||||
"ora": "0.4.1",
|
"ora": "0.4.1",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss": "8.4.16",
|
||||||
"raw-loader": "0.5.1",
|
"postcss-loader": "7.0.1",
|
||||||
"sass": "1.54.0",
|
"sass": "1.54.8",
|
||||||
"sass-loader": "7.3.1",
|
"sass-loader": "13.0.2",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "5.7.1",
|
"semver": "7.3.7",
|
||||||
"serviceworker-webpack-plugin": "1.0.1",
|
"serviceworker-webpack5-plugin": "2.0.0",
|
||||||
"shelljs": "0.8.5",
|
"shelljs": "0.8.5",
|
||||||
"sinon": "2.4.1",
|
"sinon": "14.0.0",
|
||||||
"sinon-chai": "2.14.0",
|
"sinon-chai": "3.7.0",
|
||||||
"stylelint": "13.13.1",
|
"stylelint": "13.13.1",
|
||||||
"stylelint-config-standard": "20.0.0",
|
"stylelint-config-standard": "20.0.0",
|
||||||
"stylelint-rscss": "0.4.0",
|
"stylelint-rscss": "0.4.0",
|
||||||
"url-loader": "1.1.2",
|
"vue-loader": "17.0.0",
|
||||||
"vue-loader": "^16.0.0",
|
|
||||||
"vue-style-loader": "4.1.3",
|
"vue-style-loader": "4.1.3",
|
||||||
"webpack": "4.46.0",
|
"webpack": "5.74.0",
|
||||||
"webpack-dev-middleware": "3.7.3",
|
"webpack-dev-middleware": "3.7.3",
|
||||||
"webpack-hot-middleware": "2.25.1",
|
"webpack-hot-middleware": "2.25.2",
|
||||||
"webpack-merge": "0.20.0"
|
"webpack-merge": "0.20.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
17
src/App.js
17
src/App.js
@ -10,7 +10,9 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
|
|||||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||||
|
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||||
|
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
@ -32,8 +34,11 @@ export default {
|
|||||||
MobileNav,
|
MobileNav,
|
||||||
DesktopNav,
|
DesktopNav,
|
||||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||||
|
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
|
||||||
UserReportingModal,
|
UserReportingModal,
|
||||||
PostStatusModal,
|
PostStatusModal,
|
||||||
|
EditStatusModal,
|
||||||
|
StatusHistoryModal,
|
||||||
GlobalNoticeList
|
GlobalNoticeList
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -59,6 +64,13 @@ export default {
|
|||||||
'-' + this.layoutType
|
'-' + this.layoutType
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
navClasses () {
|
||||||
|
const { navbarColumnStretch } = this.$store.getters.mergedConfig
|
||||||
|
return [
|
||||||
|
'-' + this.layoutType,
|
||||||
|
...(navbarColumnStretch ? ['-column-stretch'] : [])
|
||||||
|
]
|
||||||
|
},
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
userBackground () { return this.currentUser.background_image },
|
userBackground () { return this.currentUser.background_image },
|
||||||
instanceBackground () {
|
instanceBackground () {
|
||||||
@ -84,11 +96,16 @@ export default {
|
|||||||
isChats () {
|
isChats () {
|
||||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||||
},
|
},
|
||||||
|
isListEdit () {
|
||||||
|
return this.$route.name === 'lists-edit'
|
||||||
|
},
|
||||||
newPostButtonShown () {
|
newPostButtonShown () {
|
||||||
if (this.isChats) return false
|
if (this.isChats) return false
|
||||||
|
if (this.isListEdit) return false
|
||||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||||
},
|
},
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||||
|
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||||
shoutboxPosition () {
|
shoutboxPosition () {
|
||||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||||
},
|
},
|
||||||
|
67
src/App.scss
67
src/App.scss
@ -5,12 +5,12 @@
|
|||||||
--navbar-height: 3.5rem;
|
--navbar-height: 3.5rem;
|
||||||
--post-line-height: 1.4;
|
--post-line-height: 1.4;
|
||||||
// Z-Index stuff
|
// Z-Index stuff
|
||||||
--ZI_media_modal: 90000;
|
--ZI_media_modal: 9000;
|
||||||
--ZI_modals_popovers: 85000;
|
--ZI_modals_popovers: 8500;
|
||||||
--ZI_modals: 80000;
|
--ZI_modals: 8000;
|
||||||
--ZI_navbar_popovers: 75000;
|
--ZI_navbar_popovers: 7500;
|
||||||
--ZI_navbar: 70000;
|
--ZI_navbar: 7000;
|
||||||
--ZI_popovers: 60000;
|
--ZI_popovers: 6000;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@ -117,12 +117,28 @@ h4 {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconLetter {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
i[class*=icon-],
|
i[class*=icon-],
|
||||||
.svg-inline--fa {
|
.svg-inline--fa,
|
||||||
|
.iconLetter {
|
||||||
color: $fallback--icon;
|
color: $fallback--icon;
|
||||||
color: var(--icon, $fallback--icon);
|
color: var(--icon, $fallback--icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-unstyled:hover,
|
||||||
|
a:hover {
|
||||||
|
> i[class*=icon-],
|
||||||
|
> .svg-inline--fa,
|
||||||
|
> .iconLetter {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
z-index: var(--ZI_navbar);
|
z-index: var(--ZI_navbar);
|
||||||
color: var(--topBarText);
|
color: var(--topBarText);
|
||||||
@ -141,6 +157,11 @@ nav {
|
|||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#modal {
|
||||||
|
position: absolute;
|
||||||
|
z-index: var(--ZI_modals);
|
||||||
|
}
|
||||||
|
|
||||||
.column.-scrollable {
|
.column.-scrollable {
|
||||||
top: var(--navbar-height);
|
top: var(--navbar-height);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@ -182,13 +203,18 @@ nav {
|
|||||||
|
|
||||||
.app-layout {
|
.app-layout {
|
||||||
--miniColumn: 25rem;
|
--miniColumn: 25rem;
|
||||||
--maxiColumn: minmax(var(--miniColumn), 45rem);
|
--maxiColumn: 45rem;
|
||||||
--columnGap: 1em;
|
--columnGap: 1em;
|
||||||
--status-margin: 0.75em;
|
--status-margin: 0.75em;
|
||||||
|
--effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn)));
|
||||||
|
--effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn)));
|
||||||
|
--effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn)));
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--miniColumn) var(--maxiColumn);
|
grid-template-columns:
|
||||||
|
var(--effectiveSidebarColumnWidth)
|
||||||
|
var(--effectiveContentColumnWidth);
|
||||||
grid-template-areas: "sidebar content";
|
grid-template-areas: "sidebar content";
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -282,15 +308,24 @@ nav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.-reverse:not(.-wide):not(.-mobile) {
|
&.-reverse:not(.-wide):not(.-mobile) {
|
||||||
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
grid-template-columns:
|
||||||
|
var(--effectiveContentColumnWidth)
|
||||||
|
var(--effectiveSidebarColumnWidth);
|
||||||
grid-template-areas: "content sidebar";
|
grid-template-areas: "content sidebar";
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-wide {
|
&.-wide {
|
||||||
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
|
grid-template-columns:
|
||||||
|
var(--effectiveSidebarColumnWidth)
|
||||||
|
var(--effectiveContentColumnWidth)
|
||||||
|
var(--effectiveNotifsColumnWidth);
|
||||||
grid-template-areas: "sidebar content notifs";
|
grid-template-areas: "sidebar content notifs";
|
||||||
|
|
||||||
&.-reverse {
|
&.-reverse {
|
||||||
|
grid-template-columns:
|
||||||
|
var(--effectiveNotifsColumnWidth)
|
||||||
|
var(--effectiveContentColumnWidth)
|
||||||
|
var(--effectiveSidebarColumnWidth);
|
||||||
grid-template-areas: "notifs content sidebar";
|
grid-template-areas: "notifs content sidebar";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -746,17 +781,23 @@ option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fa-scale-110 {
|
.fa-scale-110 {
|
||||||
&.svg-inline--fa {
|
&.svg-inline--fa,
|
||||||
|
&.iconLetter {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-old-padding {
|
.fa-old-padding {
|
||||||
&.svg-inline--fa {
|
&.iconLetter,
|
||||||
|
&.svg-inline--fa, &-layer {
|
||||||
padding: 0 0.3em;
|
padding: 0 0.3em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.veryfaint {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
.login-hint {
|
.login-hint {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
10
src/App.vue
10
src/App.vue
@ -8,7 +8,10 @@
|
|||||||
class="app-bg-wrapper"
|
class="app-bg-wrapper"
|
||||||
/>
|
/>
|
||||||
<MobileNav v-if="layoutType === 'mobile'" />
|
<MobileNav v-if="layoutType === 'mobile'" />
|
||||||
<DesktopNav v-else />
|
<DesktopNav
|
||||||
|
v-else
|
||||||
|
:class="navClasses"
|
||||||
|
/>
|
||||||
<Notifications v-if="currentUser" />
|
<Notifications v-if="currentUser" />
|
||||||
<div
|
<div
|
||||||
id="content"
|
id="content"
|
||||||
@ -33,7 +36,7 @@
|
|||||||
<div
|
<div
|
||||||
id="main-scroller"
|
id="main-scroller"
|
||||||
class="column main"
|
class="column main"
|
||||||
:class="{ '-full-height': isChats }"
|
:class="{ '-full-height': isChats || isListEdit }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!currentUser"
|
v-if="!currentUser"
|
||||||
@ -64,7 +67,10 @@
|
|||||||
<MobilePostStatusButton />
|
<MobilePostStatusButton />
|
||||||
<UserReportingModal />
|
<UserReportingModal />
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
|
<EditStatusModal v-if="editingAvailable" />
|
||||||
|
<StatusHistoryModal v-if="editingAvailable" />
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
|
<UpdateNotification />
|
||||||
<div id="modal" />
|
<div id="modal" />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
<div id="popovers" />
|
<div id="popovers" />
|
||||||
|
17
src/_mixins.scss
Normal file
17
src/_mixins.scss
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@mixin unfocused-style {
|
||||||
|
@content;
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible):not(:hover) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin focused-style {
|
||||||
|
&:hover, &:focus {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
BIN
src/assets/pleromatan_apology.png
Normal file
BIN
src/assets/pleromatan_apology.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 396 KiB |
BIN
src/assets/pleromatan_apology_fox.png
Normal file
BIN
src/assets/pleromatan_apology_fox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 521 KiB |
BIN
src/assets/pleromatan_apology_fox_mask.png
Normal file
BIN
src/assets/pleromatan_apology_fox_mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/pleromatan_apology_mask.png
Normal file
BIN
src/assets/pleromatan_apology_mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
@ -12,7 +12,7 @@ import { windowWidth, windowHeight } from '../services/window_utils/window_utils
|
|||||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
|
||||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||||
|
|
||||||
let staticInitialResults = null
|
let staticInitialResults = null
|
||||||
@ -251,6 +251,7 @@ const getNodeInfo = async ({ store }) => {
|
|||||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||||
|
|
||||||
@ -360,6 +361,8 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||||||
console.error('Failed to load any theme!')
|
console.error('Failed to load any theme!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyConfig(store.state.config)
|
||||||
|
|
||||||
// Now we can try getting the server settings and logging in
|
// Now we can try getting the server settings and logging in
|
||||||
// Most of these are preloaded into the index.html so blocking is minimized
|
// Most of these are preloaded into the index.html so blocking is minimized
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -20,6 +20,10 @@ import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
|||||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||||
import About from 'components/about/about.vue'
|
import About from 'components/about/about.vue'
|
||||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||||
|
import Lists from 'components/lists/lists.vue'
|
||||||
|
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||||
|
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||||
|
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||||
|
|
||||||
export default (store) => {
|
export default (store) => {
|
||||||
const validateAuthenticatedRoute = (to, from, next) => {
|
const validateAuthenticatedRoute = (to, from, next) => {
|
||||||
@ -58,7 +62,7 @@ export default (store) => {
|
|||||||
component: RemoteUserResolver,
|
component: RemoteUserResolver,
|
||||||
beforeEnter: validateAuthenticatedRoute
|
beforeEnter: validateAuthenticatedRoute
|
||||||
},
|
},
|
||||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
|
||||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'registration', path: '/registration', component: Registration },
|
{ name: 'registration', path: '/registration', component: Registration },
|
||||||
@ -72,7 +76,13 @@ export default (store) => {
|
|||||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'about', path: '/about', component: About },
|
{ name: 'about', path: '/about', component: About },
|
||||||
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
|
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
||||||
|
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||||
|
{ name: 'lists', path: '/lists', component: Lists },
|
||||||
|
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
||||||
|
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
|
||||||
|
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
|
||||||
|
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }
|
||||||
]
|
]
|
||||||
|
|
||||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
|
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faEllipsisV
|
faEllipsisV
|
||||||
@ -19,7 +20,8 @@ const AccountActions = {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Popover
|
Popover,
|
||||||
|
UserListMenu
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showRepeats () {
|
showRepeats () {
|
||||||
@ -34,6 +36,9 @@ const AccountActions = {
|
|||||||
unblockUser () {
|
unblockUser () {
|
||||||
this.$store.dispatch('unblockUser', this.user.id)
|
this.$store.dispatch('unblockUser', this.user.id)
|
||||||
},
|
},
|
||||||
|
removeUserFromFollowers () {
|
||||||
|
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||||
|
},
|
||||||
reportUser () {
|
reportUser () {
|
||||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||||
},
|
},
|
||||||
|
@ -28,6 +28,14 @@
|
|||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<UserListMenu :user="user" />
|
||||||
|
<button
|
||||||
|
v-if="relationship.followed_by"
|
||||||
|
class="btn button-default btn-block dropdown-item"
|
||||||
|
@click="removeUserFromFollowers"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.remove_follower') }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="relationship.blocking"
|
v-if="relationship.blocking"
|
||||||
class="btn button-default btn-block dropdown-item"
|
class="btn button-default btn-block dropdown-item"
|
||||||
|
@ -129,6 +129,9 @@ const Attachment = {
|
|||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
'attachment.description' (newVal) {
|
||||||
|
this.localDescription = newVal
|
||||||
|
},
|
||||||
localDescription (newVal) {
|
localDescription (newVal) {
|
||||||
this.onEdit(newVal)
|
this.onEdit(newVal)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import UserPopover from '../user_popover/user_popover.vue'
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import UserLink from '../user_link/user_link.vue'
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
@ -10,7 +11,8 @@ const BasicUserCard = {
|
|||||||
components: {
|
components: {
|
||||||
UserPopover,
|
UserPopover,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
RichContent
|
RichContent,
|
||||||
|
UserLink
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
userProfileLink (user) {
|
userProfileLink (user) {
|
||||||
|
@ -30,12 +30,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<user-link
|
||||||
class="basic-user-card-screen-name"
|
class="basic-user-card-screen-name"
|
||||||
:to="userProfileLink(user)"
|
:user="user"
|
||||||
>
|
/>
|
||||||
@{{ user.screen_name_ui }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +57,7 @@ const Chat = {
|
|||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
window.removeEventListener('scroll', this.handleScroll)
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||||
this.$store.dispatch('clearCurrentChat')
|
this.$store.dispatch('clearCurrentChat')
|
||||||
},
|
},
|
||||||
@ -135,7 +136,7 @@ const Chat = {
|
|||||||
},
|
},
|
||||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||||
handleResize (opts = {}) {
|
handleResize (opts = {}) {
|
||||||
const { expand = false, delayed = false } = opts
|
const { delayed = false } = opts
|
||||||
|
|
||||||
if (delayed) {
|
if (delayed) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -146,10 +147,10 @@ const Chat = {
|
|||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const { offsetHeight = undefined } = getScrollPosition()
|
const { offsetHeight = undefined } = getScrollPosition()
|
||||||
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
const diff = offsetHeight - this.lastScrollPosition.offsetHeight
|
||||||
if (diff !== 0 || (!this.bottomedOut() && expand)) {
|
if (diff !== 0 && !this.bottomedOut()) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
window.scrollTo({ top: window.scrollY + diff })
|
window.scrollBy({ top: -Math.trunc(diff) })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.lastScrollPosition = getScrollPosition()
|
this.lastScrollPosition = getScrollPosition()
|
||||||
@ -187,6 +188,7 @@ const Chat = {
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
},
|
},
|
||||||
handleScroll: _.throttle(function () {
|
handleScroll: _.throttle(function () {
|
||||||
|
this.lastScrollPosition = getScrollPosition()
|
||||||
if (!this.currentChat) { return }
|
if (!this.currentChat) { return }
|
||||||
|
|
||||||
if (this.reachedTop()) {
|
if (this.reachedTop()) {
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
import ThreadTree from '../thread_tree/thread_tree.vue'
|
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||||
|
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||||
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
|
||||||
|
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
@ -77,6 +81,9 @@ const conversation = {
|
|||||||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||||
return maxDepth >= 1 ? maxDepth : 1
|
return maxDepth >= 1 ? maxDepth : 1
|
||||||
},
|
},
|
||||||
|
streamingEnabled () {
|
||||||
|
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||||
|
},
|
||||||
displayStyle () {
|
displayStyle () {
|
||||||
return this.$store.getters.mergedConfig.conversationDisplay
|
return this.$store.getters.mergedConfig.conversationDisplay
|
||||||
},
|
},
|
||||||
@ -339,11 +346,17 @@ const conversation = {
|
|||||||
},
|
},
|
||||||
maybeHighlight () {
|
maybeHighlight () {
|
||||||
return this.isExpanded ? this.highlight : null
|
return this.isExpanded ? this.highlight : null
|
||||||
}
|
},
|
||||||
|
...mapGetters(['mergedConfig']),
|
||||||
|
...mapState({
|
||||||
|
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
||||||
|
})
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status,
|
Status,
|
||||||
ThreadTree
|
ThreadTree,
|
||||||
|
QuickFilterSettings,
|
||||||
|
QuickViewSettings
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
statusId (newVal, oldVal) {
|
statusId (newVal, oldVal) {
|
||||||
@ -395,6 +408,11 @@ const conversation = {
|
|||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
|
|
||||||
|
if (!this.streamingEnabled) {
|
||||||
|
this.$store.dispatch('fetchStatus', id)
|
||||||
|
}
|
||||||
|
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,14 @@
|
|||||||
>
|
>
|
||||||
{{ $t('timeline.collapse') }}
|
{{ $t('timeline.collapse') }}
|
||||||
</button>
|
</button>
|
||||||
|
<QuickFilterSettings
|
||||||
|
v-if="!collapsable"
|
||||||
|
:conversation="true"
|
||||||
|
/>
|
||||||
|
<QuickViewSettings
|
||||||
|
v-if="!collapsable"
|
||||||
|
:conversation="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="conversation-body panel-body">
|
<div class="conversation-body panel-body">
|
||||||
<div
|
<div
|
||||||
|
@ -23,6 +23,26 @@
|
|||||||
max-width: 980px;
|
max-width: 980px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.-column-stretch .inner-nav {
|
||||||
|
--miniColumn: 25rem;
|
||||||
|
--maxiColumn: 45rem;
|
||||||
|
--columnGap: 1em;
|
||||||
|
max-width: calc(
|
||||||
|
var(--sidebarColumnWidth, var(--miniColumn)) +
|
||||||
|
var(--contentColumnWidth, var(--maxiColumn)) +
|
||||||
|
var(--columnGap)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-column-stretch.-wide .inner-nav {
|
||||||
|
max-width: calc(
|
||||||
|
var(--sidebarColumnWidth, var(--miniColumn)) +
|
||||||
|
var(--contentColumnWidth, var(--maxiColumn)) +
|
||||||
|
var(--notifsColumnWidth, var(--miniColumn)) +
|
||||||
|
var(--columnGap)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
&.-logoLeft .inner-nav {
|
&.-logoLeft .inner-nav {
|
||||||
grid-template-columns: auto 2fr 2fr;
|
grid-template-columns: auto 2fr 2fr;
|
||||||
grid-template-areas: "logo sitename actions";
|
grid-template-areas: "logo sitename actions";
|
||||||
@ -117,4 +137,8 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
:title="$t('nav.administration')"
|
:title="$t('nav.administration')"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
<span class="spacer" />
|
||||||
<button
|
<button
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
class="button-unstyled nav-icon"
|
class="button-unstyled nav-icon"
|
||||||
|
75
src/components/edit_status_modal/edit_status_modal.js
Normal file
75
src/components/edit_status_modal/edit_status_modal.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
|
import Modal from '../modal/modal.vue'
|
||||||
|
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||||
|
import get from 'lodash/get'
|
||||||
|
|
||||||
|
const EditStatusModal = {
|
||||||
|
components: {
|
||||||
|
PostStatusForm,
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
resettingForm: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
modalActivated () {
|
||||||
|
return this.$store.state.editStatus.modalActivated
|
||||||
|
},
|
||||||
|
isFormVisible () {
|
||||||
|
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||||
|
},
|
||||||
|
params () {
|
||||||
|
return this.$store.state.editStatus.params || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
params (newVal, oldVal) {
|
||||||
|
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||||
|
this.resettingForm = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.resettingForm = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isFormVisible (val) {
|
||||||
|
if (val) {
|
||||||
|
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||||
|
const params = {
|
||||||
|
store: this.$store,
|
||||||
|
statusId: this.$store.state.editStatus.params.statusId,
|
||||||
|
status,
|
||||||
|
spoilerText,
|
||||||
|
sensitive,
|
||||||
|
poll,
|
||||||
|
media,
|
||||||
|
contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusPosterService.editStatus(params)
|
||||||
|
.then((data) => {
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error editing status', err)
|
||||||
|
return {
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeModal () {
|
||||||
|
this.$store.dispatch('closeEditStatusModal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditStatusModal
|
48
src/components/edit_status_modal/edit_status_modal.vue
Normal file
48
src/components/edit_status_modal/edit_status_modal.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-if="isFormVisible"
|
||||||
|
class="edit-form-modal-view"
|
||||||
|
@backdropClicked="closeModal"
|
||||||
|
>
|
||||||
|
<div class="edit-form-modal-panel panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
{{ $t('post_status.edit_status') }}
|
||||||
|
</div>
|
||||||
|
<PostStatusForm
|
||||||
|
class="panel-body"
|
||||||
|
v-bind="params"
|
||||||
|
:post-handler="doEditStatus"
|
||||||
|
:disable-polls="true"
|
||||||
|
:disable-visibility-selector="true"
|
||||||
|
@posted="closeModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./edit_status_modal.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.modal-view.edit-form-modal-view {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.edit-form-modal-panel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 25%;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
margin-top: 8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-bottom-left {
|
||||||
|
max-width: 6.5em;
|
||||||
|
|
||||||
|
.emoji-icon {
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,8 +1,9 @@
|
|||||||
import Completion from '../../services/completion/completion.js'
|
import Completion from '../../services/completion/completion.js'
|
||||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
|
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faSmileBeam
|
faSmileBeam
|
||||||
@ -120,7 +121,8 @@ const EmojiInput = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
EmojiPicker
|
EmojiPicker,
|
||||||
|
UnicodeDomainIndicator
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
padEmoji () {
|
padEmoji () {
|
||||||
@ -141,6 +143,51 @@ const EmojiInput = {
|
|||||||
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||||
return word
|
return word
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
languages () {
|
||||||
|
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||||
|
},
|
||||||
|
maybeLocalizedEmojiNamesAndKeywords () {
|
||||||
|
return emoji => {
|
||||||
|
const names = [emoji.displayText]
|
||||||
|
const keywords = []
|
||||||
|
|
||||||
|
if (emoji.displayTextI18n) {
|
||||||
|
names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.annotations) {
|
||||||
|
this.languages.forEach(lang => {
|
||||||
|
names.push(emoji.annotations[lang]?.name)
|
||||||
|
|
||||||
|
keywords.push(...(emoji.annotations[lang]?.keywords || []))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
names: names.filter(k => k),
|
||||||
|
keywords: keywords.filter(k => k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeLocalizedEmojiName () {
|
||||||
|
return emoji => {
|
||||||
|
if (!emoji.annotations) {
|
||||||
|
return emoji.displayText
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.displayTextI18n) {
|
||||||
|
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const lang of this.languages) {
|
||||||
|
if (emoji.annotations[lang]?.name) {
|
||||||
|
return emoji.annotations[lang].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emoji.displayText
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
@ -179,7 +226,7 @@ const EmojiInput = {
|
|||||||
const firstchar = newWord.charAt(0)
|
const firstchar = newWord.charAt(0)
|
||||||
this.suggestions = []
|
this.suggestions = []
|
||||||
if (newWord === firstchar) return
|
if (newWord === firstchar) return
|
||||||
const matchedSuggestions = await this.suggest(newWord)
|
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
|
||||||
// Async: cancel if textAtCaret has changed during wait
|
// Async: cancel if textAtCaret has changed during wait
|
||||||
if (this.textAtCaret !== newWord) return
|
if (this.textAtCaret !== newWord) return
|
||||||
if (matchedSuggestions.length <= 0) return
|
if (matchedSuggestions.length <= 0) return
|
||||||
@ -205,7 +252,6 @@ const EmojiInput = {
|
|||||||
},
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.showPicker = true
|
this.showPicker = true
|
||||||
this.$refs.picker.startEmojiLoad()
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.focusPickerInput()
|
this.focusPickerInput()
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
v-if="enableEmojiPicker"
|
v-if="enableEmojiPicker"
|
||||||
ref="picker"
|
ref="picker"
|
||||||
:class="{ hide: !showPicker }"
|
:class="{ hide: !showPicker }"
|
||||||
|
:showing="showPicker"
|
||||||
:enable-sticker-picker="enableStickerPicker"
|
:enable-sticker-picker="enableStickerPicker"
|
||||||
class="emoji-picker-panel"
|
class="emoji-picker-panel"
|
||||||
@emoji="insert"
|
@emoji="insert"
|
||||||
@ -50,7 +51,21 @@
|
|||||||
<span v-else>{{ suggestion.replacement }}</span>
|
<span v-else>{{ suggestion.replacement }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="displayText">{{ suggestion.displayText }}</span>
|
<span
|
||||||
|
v-if="suggestion.user"
|
||||||
|
class="displayText"
|
||||||
|
>
|
||||||
|
{{ suggestion.displayText }}<UnicodeDomainIndicator
|
||||||
|
:user="suggestion.user"
|
||||||
|
:at="false"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!suggestion.user"
|
||||||
|
class="displayText"
|
||||||
|
>
|
||||||
|
{{ maybeLocalizedEmojiName(suggestion) }}
|
||||||
|
</span>
|
||||||
<span class="detailText">{{ suggestion.detailText }}</span>
|
<span class="detailText">{{ suggestion.detailText }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* suggest - generates a suggestor function to be used by emoji-input
|
* suggest - generates a suggestor function to be used by emoji-input
|
||||||
* data: object providing source information for specific types of suggestions:
|
* data: object providing source information for specific types of suggestions:
|
||||||
* data.emoji - optional, an array of all emoji available i.e.
|
* data.emoji - optional, an array of all emoji available i.e.
|
||||||
* (state.instance.emoji + state.instance.customEmoji)
|
* (getters.standardEmojiList + state.instance.customEmoji)
|
||||||
* data.users - optional, an array of all known users
|
* data.users - optional, an array of all known users
|
||||||
* updateUsersList - optional, a function to search and append to users
|
* updateUsersList - optional, a function to search and append to users
|
||||||
*
|
*
|
||||||
@ -13,10 +13,10 @@
|
|||||||
export default data => {
|
export default data => {
|
||||||
const emojiCurry = suggestEmoji(data.emoji)
|
const emojiCurry = suggestEmoji(data.emoji)
|
||||||
const usersCurry = data.store && suggestUsers(data.store)
|
const usersCurry = data.store && suggestUsers(data.store)
|
||||||
return input => {
|
return (input, nameKeywordLocalizer) => {
|
||||||
const firstChar = input[0]
|
const firstChar = input[0]
|
||||||
if (firstChar === ':' && data.emoji) {
|
if (firstChar === ':' && data.emoji) {
|
||||||
return emojiCurry(input)
|
return emojiCurry(input, nameKeywordLocalizer)
|
||||||
}
|
}
|
||||||
if (firstChar === '@' && usersCurry) {
|
if (firstChar === '@' && usersCurry) {
|
||||||
return usersCurry(input)
|
return usersCurry(input)
|
||||||
@ -25,34 +25,34 @@ export default data => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const suggestEmoji = emojis => input => {
|
export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => {
|
||||||
const noPrefix = input.toLowerCase().substr(1)
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
return emojis
|
return emojis
|
||||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
.map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||||
.sort((a, b) => {
|
.filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
|
||||||
let aScore = 0
|
.map(k => {
|
||||||
let bScore = 0
|
let score = 0
|
||||||
|
|
||||||
// An exact match always wins
|
// An exact match always wins
|
||||||
aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
|
score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
|
||||||
bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
|
|
||||||
|
|
||||||
// Prioritize custom emoji a lot
|
// Prioritize custom emoji a lot
|
||||||
aScore += a.imageUrl ? 100 : 0
|
score += k.imageUrl ? 100 : 0
|
||||||
bScore += b.imageUrl ? 100 : 0
|
|
||||||
|
|
||||||
// Prioritize prefix matches somewhat
|
// Prioritize prefix matches somewhat
|
||||||
aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
|
||||||
bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
|
||||||
|
|
||||||
// Sort by length
|
// Sort by length
|
||||||
aScore -= a.displayText.length
|
score -= k.displayText.length
|
||||||
bScore -= b.displayText.length
|
|
||||||
|
|
||||||
|
k.score = score
|
||||||
|
return k
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
// Break ties alphabetically
|
// Break ties alphabetically
|
||||||
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
|
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
|
||||||
|
|
||||||
return bScore - aScore + alphabetically
|
return b.score - a.score + alphabetically
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||||||
|
|
||||||
return diff + nameAlphabetically + screenNameAlphabetically
|
return diff + nameAlphabetically + screenNameAlphabetically
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
}).map((user) => ({
|
||||||
displayText: screen_name_ui,
|
user,
|
||||||
detailText: name,
|
displayText: user.screen_name_ui,
|
||||||
imageUrl: profile_image_url_original,
|
detailText: user.name,
|
||||||
replacement: '@' + screen_name + ' '
|
imageUrl: user.profile_image_url_original,
|
||||||
|
replacement: '@' + user.screen_name + ' '
|
||||||
}))
|
}))
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
@ -1,33 +1,76 @@
|
|||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
import StillImage from '../still-image/still-image.vue'
|
||||||
|
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||||
|
import lozad from 'lozad'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
faStickyNote,
|
faStickyNote,
|
||||||
faSmileBeam
|
faSmileBeam,
|
||||||
|
faSmile,
|
||||||
|
faUser,
|
||||||
|
faPaw,
|
||||||
|
faIceCream,
|
||||||
|
faBus,
|
||||||
|
faBasketballBall,
|
||||||
|
faLightbulb,
|
||||||
|
faCode,
|
||||||
|
faFlag
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { trim } from 'lodash'
|
import { debounce, trim } from 'lodash'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
faStickyNote,
|
faStickyNote,
|
||||||
faSmileBeam
|
faSmileBeam,
|
||||||
|
faSmile,
|
||||||
|
faUser,
|
||||||
|
faPaw,
|
||||||
|
faIceCream,
|
||||||
|
faBus,
|
||||||
|
faBasketballBall,
|
||||||
|
faLightbulb,
|
||||||
|
faCode,
|
||||||
|
faFlag
|
||||||
)
|
)
|
||||||
|
|
||||||
// At widest, approximately 20 emoji are visible in a row,
|
const UNICODE_EMOJI_GROUP_ICON = {
|
||||||
// loading 3 rows, could be overkill for narrow picker
|
'smileys-and-emotion': 'smile',
|
||||||
const LOAD_EMOJI_BY = 60
|
'people-and-body': 'user',
|
||||||
|
'animals-and-nature': 'paw',
|
||||||
|
'food-and-drink': 'ice-cream',
|
||||||
|
'travel-and-places': 'bus',
|
||||||
|
activities: 'basketball-ball',
|
||||||
|
objects: 'lightbulb',
|
||||||
|
symbols: 'code',
|
||||||
|
flags: 'flag'
|
||||||
|
}
|
||||||
|
|
||||||
// When to start loading new batch emoji, in pixels
|
const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
|
||||||
const LOAD_EMOJI_MARGIN = 64
|
const res = [emoji.displayText, nameLocalizer(emoji)]
|
||||||
|
if (emoji.annotations) {
|
||||||
|
languages.forEach(lang => {
|
||||||
|
const keywords = emoji.annotations[lang]?.keywords || []
|
||||||
|
const name = emoji.annotations[lang]?.name
|
||||||
|
res.push(...(keywords.concat([name]).filter(k => k)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
const filterByKeyword = (list, keyword = '') => {
|
const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
|
||||||
if (keyword === '') return list
|
if (keyword === '') return list
|
||||||
|
|
||||||
const keywordLowercase = keyword.toLowerCase()
|
const keywordLowercase = keyword.toLowerCase()
|
||||||
const orderedEmojiList = []
|
const orderedEmojiList = []
|
||||||
for (const emoji of list) {
|
for (const emoji of list) {
|
||||||
const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
|
const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
|
||||||
|
.map(k => k.toLowerCase().indexOf(keywordLowercase))
|
||||||
|
.filter(k => k > -1)
|
||||||
|
|
||||||
|
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
|
||||||
|
|
||||||
if (indexOfKeyword > -1) {
|
if (indexOfKeyword > -1) {
|
||||||
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
||||||
orderedEmojiList[indexOfKeyword] = []
|
orderedEmojiList[indexOfKeyword] = []
|
||||||
@ -44,6 +87,10 @@ const EmojiPicker = {
|
|||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
showing: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@ -53,16 +100,26 @@ const EmojiPicker = {
|
|||||||
showingStickers: false,
|
showingStickers: false,
|
||||||
groupsScrolledClass: 'scrolled-top',
|
groupsScrolledClass: 'scrolled-top',
|
||||||
keepOpen: false,
|
keepOpen: false,
|
||||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
|
||||||
customEmojiTimeout: null,
|
customEmojiTimeout: null,
|
||||||
customEmojiLoadAllConfirmed: false
|
// Lazy-load only after the first time `showing` becomes true.
|
||||||
|
contentLoaded: false,
|
||||||
|
groupRefs: {},
|
||||||
|
emojiRefs: {},
|
||||||
|
filteredEmojiGroups: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
StillImage
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setGroupRef (name) {
|
||||||
|
return el => { this.groupRefs[name] = el }
|
||||||
|
},
|
||||||
|
setEmojiRef (name) {
|
||||||
|
return el => { this.emojiRefs[name] = el }
|
||||||
|
},
|
||||||
onStickerUploaded (e) {
|
onStickerUploaded (e) {
|
||||||
this.$emit('sticker-uploaded', e)
|
this.$emit('sticker-uploaded', e)
|
||||||
},
|
},
|
||||||
@ -77,10 +134,38 @@ const EmojiPicker = {
|
|||||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||||
this.updateScrolledClass(target)
|
this.updateScrolledClass(target)
|
||||||
this.scrolledGroup(target)
|
this.scrolledGroup(target)
|
||||||
this.triggerLoadMore(target)
|
},
|
||||||
|
scrolledGroup (target) {
|
||||||
|
const top = target.scrollTop + 5
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.allEmojiGroups.forEach(group => {
|
||||||
|
const ref = this.groupRefs['group-' + group.id]
|
||||||
|
if (ref && ref.offsetTop <= top) {
|
||||||
|
this.activeGroup = group.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.scrollHeader()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scrollHeader () {
|
||||||
|
// Scroll the active tab's header into view
|
||||||
|
const headerRef = this.groupRefs['group-header-' + this.activeGroup]
|
||||||
|
const left = headerRef.offsetLeft
|
||||||
|
const right = left + headerRef.offsetWidth
|
||||||
|
const headerCont = this.$refs.header
|
||||||
|
const currentScroll = headerCont.scrollLeft
|
||||||
|
const currentScrollRight = currentScroll + headerCont.clientWidth
|
||||||
|
const setScroll = s => { headerCont.scrollLeft = s }
|
||||||
|
|
||||||
|
const margin = 7 // .emoji-tabs-item: padding
|
||||||
|
if (left - margin < currentScroll) {
|
||||||
|
setScroll(left - margin)
|
||||||
|
} else if (right + margin > currentScrollRight) {
|
||||||
|
setScroll(right + margin - headerCont.clientWidth)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
highlight (key) {
|
highlight (key) {
|
||||||
const ref = this.$refs['group-' + key]
|
const ref = this.groupRefs['group-' + key]
|
||||||
const top = ref.offsetTop
|
const top = ref.offsetTop
|
||||||
this.setShowStickers(false)
|
this.setShowStickers(false)
|
||||||
this.activeGroup = key
|
this.activeGroup = key
|
||||||
@ -97,73 +182,90 @@ const EmojiPicker = {
|
|||||||
this.groupsScrolledClass = 'scrolled-middle'
|
this.groupsScrolledClass = 'scrolled-middle'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
triggerLoadMore (target) {
|
|
||||||
const ref = this.$refs['group-end-custom']
|
|
||||||
if (!ref) return
|
|
||||||
const bottom = ref.offsetTop + ref.offsetHeight
|
|
||||||
|
|
||||||
const scrollerBottom = target.scrollTop + target.clientHeight
|
|
||||||
const scrollerTop = target.scrollTop
|
|
||||||
const scrollerMax = target.scrollHeight
|
|
||||||
|
|
||||||
// Loads more emoji when they come into view
|
|
||||||
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
|
|
||||||
// Always load when at the very top in case there's no scroll space yet
|
|
||||||
const atTop = scrollerTop < 5
|
|
||||||
// Don't load when looking at unicode category or at the very bottom
|
|
||||||
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
|
|
||||||
if (!bottomAboveViewport && (approachingBottom || atTop)) {
|
|
||||||
this.loadEmoji()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrolledGroup (target) {
|
|
||||||
const top = target.scrollTop + 5
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.emojisView.forEach(group => {
|
|
||||||
const ref = this.$refs['group-' + group.id]
|
|
||||||
if (ref.offsetTop <= top) {
|
|
||||||
this.activeGroup = group.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadEmoji () {
|
|
||||||
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
|
|
||||||
|
|
||||||
if (allLoaded) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.customEmojiBufferSlice += LOAD_EMOJI_BY
|
|
||||||
},
|
|
||||||
startEmojiLoad (forceUpdate = false) {
|
|
||||||
if (!forceUpdate) {
|
|
||||||
this.keyword = ''
|
|
||||||
}
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs['emoji-groups'].scrollTop = 0
|
|
||||||
})
|
|
||||||
const bufferSize = this.customEmojiBuffer.length
|
|
||||||
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
|
|
||||||
if (bufferPrefilledAll && !forceUpdate) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.customEmojiBufferSlice = LOAD_EMOJI_BY
|
|
||||||
},
|
|
||||||
toggleStickers () {
|
toggleStickers () {
|
||||||
this.showingStickers = !this.showingStickers
|
this.showingStickers = !this.showingStickers
|
||||||
},
|
},
|
||||||
setShowStickers (value) {
|
setShowStickers (value) {
|
||||||
this.showingStickers = value
|
this.showingStickers = value
|
||||||
|
},
|
||||||
|
filterByKeyword (list, keyword) {
|
||||||
|
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
|
||||||
|
},
|
||||||
|
initializeLazyLoad () {
|
||||||
|
this.destroyLazyLoad()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$lozad = lozad('.still-image.emoji-picker-emoji', {
|
||||||
|
load: el => {
|
||||||
|
const name = el.getAttribute('data-emoji-name')
|
||||||
|
const vn = this.emojiRefs[name]
|
||||||
|
if (!vn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vn.loadLazy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.$lozad.observe()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
waitForDomAndInitializeLazyLoad () {
|
||||||
|
this.$nextTick(() => this.initializeLazyLoad())
|
||||||
|
},
|
||||||
|
destroyLazyLoad () {
|
||||||
|
if (this.$lozad) {
|
||||||
|
if (this.$lozad.observer) {
|
||||||
|
this.$lozad.observer.disconnect()
|
||||||
|
}
|
||||||
|
if (this.$lozad.mutationObserver) {
|
||||||
|
this.$lozad.mutationObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShowing () {
|
||||||
|
const oldContentLoaded = this.contentLoaded
|
||||||
|
this.contentLoaded = true
|
||||||
|
this.waitForDomAndInitializeLazyLoad()
|
||||||
|
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||||
|
if (!oldContentLoaded) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.defaultGroup) {
|
||||||
|
this.highlight(this.defaultGroup)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFilteredEmojiGroups () {
|
||||||
|
return this.allEmojiGroups
|
||||||
|
.map(group => ({
|
||||||
|
...group,
|
||||||
|
emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
|
||||||
|
}))
|
||||||
|
.filter(group => group.emojis.length > 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
keyword () {
|
keyword () {
|
||||||
this.customEmojiLoadAllConfirmed = false
|
|
||||||
this.onScroll()
|
this.onScroll()
|
||||||
this.startEmojiLoad(true)
|
this.debouncedHandleKeywordChange()
|
||||||
|
},
|
||||||
|
allCustomGroups () {
|
||||||
|
this.waitForDomAndInitializeLazyLoad()
|
||||||
|
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||||
|
},
|
||||||
|
showing (val) {
|
||||||
|
if (val) {
|
||||||
|
this.onShowing()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.showing) {
|
||||||
|
this.onShowing()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.destroyLazyLoad()
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
activeGroupView () {
|
activeGroupView () {
|
||||||
return this.showingStickers ? '' : this.activeGroup
|
return this.showingStickers ? '' : this.activeGroup
|
||||||
@ -174,39 +276,55 @@ const EmojiPicker = {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
filteredEmoji () {
|
allCustomGroups () {
|
||||||
return filterByKeyword(
|
return this.$store.getters.groupedCustomEmojis
|
||||||
this.$store.state.instance.customEmoji || [],
|
|
||||||
trim(this.keyword)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
customEmojiBuffer () {
|
defaultGroup () {
|
||||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
return Object.keys(this.allCustomGroups)[0]
|
||||||
},
|
},
|
||||||
emojis () {
|
unicodeEmojiGroups () {
|
||||||
const standardEmojis = this.$store.state.instance.emoji || []
|
return this.$store.getters.standardEmojiGroupList.map(group => ({
|
||||||
const customEmojis = this.customEmojiBuffer
|
id: `standard-${group.id}`,
|
||||||
|
text: this.$t(`emoji.unicode_groups.${group.id}`),
|
||||||
return [
|
icon: UNICODE_EMOJI_GROUP_ICON[group.id],
|
||||||
{
|
emojis: group.emojis
|
||||||
id: 'custom',
|
}))
|
||||||
text: this.$t('emoji.custom'),
|
|
||||||
icon: 'smile-beam',
|
|
||||||
emojis: customEmojis
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'standard',
|
|
||||||
text: this.$t('emoji.unicode'),
|
|
||||||
icon: 'box-open',
|
|
||||||
emojis: filterByKeyword(standardEmojis, trim(this.keyword))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
emojisView () {
|
allEmojiGroups () {
|
||||||
return this.emojis.filter(value => value.emojis.length > 0)
|
return Object.entries(this.allCustomGroups)
|
||||||
|
.map(([_, v]) => v)
|
||||||
|
.concat(this.unicodeEmojiGroups)
|
||||||
},
|
},
|
||||||
stickerPickerEnabled () {
|
stickerPickerEnabled () {
|
||||||
return (this.$store.state.instance.stickers || []).length !== 0
|
return (this.$store.state.instance.stickers || []).length !== 0
|
||||||
|
},
|
||||||
|
debouncedHandleKeywordChange () {
|
||||||
|
return debounce(() => {
|
||||||
|
this.waitForDomAndInitializeLazyLoad()
|
||||||
|
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
languages () {
|
||||||
|
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||||
|
},
|
||||||
|
maybeLocalizedEmojiName () {
|
||||||
|
return emoji => {
|
||||||
|
if (!emoji.annotations) {
|
||||||
|
return emoji.displayText
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.displayTextI18n) {
|
||||||
|
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const lang of this.languages) {
|
||||||
|
if (emoji.annotations[lang]?.name) {
|
||||||
|
return emoji.annotations[lang].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emoji.displayText
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
$emoji-picker-header-height: 36px;
|
||||||
|
$emoji-picker-header-picture-width: 32px;
|
||||||
|
$emoji-picker-header-picture-height: 32px;
|
||||||
|
$emoji-picker-emoji-size: 32px;
|
||||||
|
|
||||||
.emoji-picker {
|
.emoji-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -19,6 +24,23 @@
|
|||||||
--lightText: var(--popoverLightText, $fallback--lightText);
|
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||||
--icon: var(--popoverIcon, $fallback--icon);
|
--icon: var(--popoverIcon, $fallback--icon);
|
||||||
|
|
||||||
|
&-header-image {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: $emoji-picker-header-picture-width;
|
||||||
|
max-width: $emoji-picker-header-picture-width;
|
||||||
|
height: $emoji-picker-header-picture-height;
|
||||||
|
max-height: $emoji-picker-header-picture-height;
|
||||||
|
.still-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.keep-open,
|
.keep-open,
|
||||||
.too-many-emoji {
|
.too-many-emoji {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
@ -37,7 +59,6 @@
|
|||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
|
||||||
padding: 10px 7px 5px;
|
padding: 10px 7px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +71,10 @@
|
|||||||
|
|
||||||
.emoji-tabs {
|
.emoji-tabs {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-groups {
|
.emoji-groups {
|
||||||
@ -57,6 +82,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.additional-tabs {
|
.additional-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
border-left: 1px solid;
|
border-left: 1px solid;
|
||||||
border-left-color: $fallback--icon;
|
border-left-color: $fallback--icon;
|
||||||
border-left-color: var(--icon, $fallback--icon);
|
border-left-color: var(--icon, $fallback--icon);
|
||||||
@ -66,15 +93,20 @@
|
|||||||
|
|
||||||
.additional-tabs,
|
.additional-tabs,
|
||||||
.emoji-tabs {
|
.emoji-tabs {
|
||||||
display: block;
|
|
||||||
min-width: 0;
|
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
flex-shrink: 1;
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
padding: 0 7px;
|
padding: 0 7px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.85em;
|
font-size: 1.85em;
|
||||||
|
width: $emoji-picker-header-picture-width;
|
||||||
|
max-width: $emoji-picker-header-picture-width;
|
||||||
|
height: $emoji-picker-header-picture-height;
|
||||||
|
max-height: $emoji-picker-header-picture-height;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -164,22 +196,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
width: 32px;
|
width: $emoji-picker-emoji-size;
|
||||||
height: 32px;
|
height: $emoji-picker-emoji-size;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 32px;
|
line-height: $emoji-picker-emoji-size;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
.emoji-picker-emoji.-custom {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
.emoji-picker-emoji.-unicode {
|
||||||
|
font-size: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="emoji-picker panel panel-default panel-body">
|
<div
|
||||||
|
class="emoji-picker panel panel-default panel-body"
|
||||||
|
>
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<span class="emoji-tabs">
|
<span
|
||||||
|
ref="header"
|
||||||
|
class="emoji-tabs"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-for="group in emojis"
|
v-for="group in filteredEmojiGroups"
|
||||||
|
:ref="setGroupRef('group-header-' + group.id)"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
class="emoji-tabs-item"
|
class="emoji-tabs-item"
|
||||||
:class="{
|
:class="{
|
||||||
active: activeGroupView === group.id,
|
active: activeGroupView === group.id
|
||||||
disabled: group.emojis.length === 0
|
|
||||||
}"
|
}"
|
||||||
:title="group.text"
|
:title="group.text"
|
||||||
@click.prevent="highlight(group.id)"
|
@click.prevent="highlight(group.id)"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="group.image"
|
||||||
|
class="emoji-picker-header-image"
|
||||||
|
>
|
||||||
|
<still-image
|
||||||
|
:alt="group.text"
|
||||||
|
:src="group.image"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
v-else
|
||||||
:icon="group.icon"
|
:icon="group.icon"
|
||||||
fixed-width
|
fixed-width
|
||||||
/>
|
/>
|
||||||
@ -36,7 +51,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div
|
||||||
|
v-if="contentLoaded"
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="emoji-content"
|
class="emoji-content"
|
||||||
:class="{hidden: showingStickers}"
|
:class="{hidden: showingStickers}"
|
||||||
@ -57,12 +75,12 @@
|
|||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="group in emojisView"
|
v-for="group in filteredEmojiGroups"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
class="emoji-group"
|
class="emoji-group"
|
||||||
>
|
>
|
||||||
<h6
|
<h6
|
||||||
:ref="'group-' + group.id"
|
:ref="setGroupRef('group-' + group.id)"
|
||||||
class="emoji-group-title"
|
class="emoji-group-title"
|
||||||
>
|
>
|
||||||
{{ group.text }}
|
{{ group.text }}
|
||||||
@ -70,17 +88,23 @@
|
|||||||
<span
|
<span
|
||||||
v-for="emoji in group.emojis"
|
v-for="emoji in group.emojis"
|
||||||
:key="group.id + emoji.displayText"
|
:key="group.id + emoji.displayText"
|
||||||
:title="emoji.displayText"
|
:title="maybeLocalizedEmojiName(emoji)"
|
||||||
class="emoji-item"
|
class="emoji-item"
|
||||||
@click.stop.prevent="onEmoji(emoji)"
|
@click.stop.prevent="onEmoji(emoji)"
|
||||||
>
|
>
|
||||||
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
|
<span
|
||||||
<img
|
v-if="!emoji.imageUrl"
|
||||||
|
class="emoji-picker-emoji -unicode"
|
||||||
|
>{{ emoji.replacement }}</span>
|
||||||
|
<still-image
|
||||||
v-else
|
v-else
|
||||||
:src="emoji.imageUrl"
|
:ref="setEmojiRef(group.id + emoji.displayText)"
|
||||||
>
|
class="emoji-picker-emoji -custom"
|
||||||
|
:data-src="emoji.imageUrl"
|
||||||
|
:data-emoji-name="group.id + emoji.displayText"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span :ref="'group-end-' + group.id" />
|
<span :ref="setGroupRef('group-end-' + group.id)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="keep-open">
|
<div class="keep-open">
|
||||||
|
@ -6,7 +6,10 @@ import {
|
|||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faThumbtack,
|
faThumbtack,
|
||||||
faShareAlt,
|
faShareAlt,
|
||||||
faExternalLinkAlt
|
faExternalLinkAlt,
|
||||||
|
faHistory,
|
||||||
|
faPlus,
|
||||||
|
faTimes
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {
|
import {
|
||||||
faBookmark as faBookmarkReg,
|
faBookmark as faBookmarkReg,
|
||||||
@ -21,13 +24,27 @@ library.add(
|
|||||||
faThumbtack,
|
faThumbtack,
|
||||||
faShareAlt,
|
faShareAlt,
|
||||||
faExternalLinkAlt,
|
faExternalLinkAlt,
|
||||||
faFlag
|
faFlag,
|
||||||
|
faHistory,
|
||||||
|
faPlus,
|
||||||
|
faTimes
|
||||||
)
|
)
|
||||||
|
|
||||||
const ExtraButtons = {
|
const ExtraButtons = {
|
||||||
props: ['status'],
|
props: ['status'],
|
||||||
components: { Popover },
|
components: { Popover },
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
expanded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onShow () {
|
||||||
|
this.expanded = true
|
||||||
|
},
|
||||||
|
onClose () {
|
||||||
|
this.expanded = false
|
||||||
|
},
|
||||||
deleteStatus () {
|
deleteStatus () {
|
||||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
@ -71,6 +88,25 @@ const ExtraButtons = {
|
|||||||
},
|
},
|
||||||
reportStatus () {
|
reportStatus () {
|
||||||
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
|
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
|
||||||
|
},
|
||||||
|
editStatus () {
|
||||||
|
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||||
|
.then(data => this.$store.dispatch('openEditStatusModal', {
|
||||||
|
statusId: this.status.id,
|
||||||
|
subject: data.spoiler_text,
|
||||||
|
statusText: data.text,
|
||||||
|
statusIsSensitive: this.status.nsfw,
|
||||||
|
statusPoll: this.status.poll,
|
||||||
|
statusFiles: [...this.status.attachments],
|
||||||
|
visibility: this.status.visibility,
|
||||||
|
statusContentType: data.content_type
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
showStatusHistory () {
|
||||||
|
const originalStatus = { ...this.status }
|
||||||
|
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
|
||||||
|
stripFieldsList.forEach(p => delete originalStatus[p])
|
||||||
|
this.$store.dispatch('openStatusHistoryModal', originalStatus)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -93,7 +129,11 @@ const ExtraButtons = {
|
|||||||
},
|
},
|
||||||
statusLink () {
|
statusLink () {
|
||||||
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||||
}
|
},
|
||||||
|
isEdited () {
|
||||||
|
return this.status.edited_at !== null
|
||||||
|
},
|
||||||
|
editingAvailable () { return this.$store.state.instance.editingAvailable }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
:offset="{ y: 5 }"
|
:offset="{ y: 5 }"
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
remove-padding
|
remove-padding
|
||||||
|
@show="onShow"
|
||||||
|
@close="onClose"
|
||||||
>
|
>
|
||||||
<template #content="{close}">
|
<template #content="{close}">
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
@ -75,6 +77,28 @@
|
|||||||
/><span>{{ $t("status.unbookmark") }}</span>
|
/><span>{{ $t("status.unbookmark") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
<button
|
||||||
|
v-if="ownStatus && editingAvailable"
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="editStatus"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="pen"
|
||||||
|
/><span>{{ $t("status.edit") }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isEdited && editingAvailable"
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="showStatusHistory"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="history"
|
||||||
|
/><span>{{ $t("status.status_history") }}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDelete"
|
v-if="canDelete"
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@ -122,10 +146,24 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<span class="button-unstyled popover-trigger">
|
<span class="button-unstyled popover-trigger">
|
||||||
<FAIcon
|
<FALayers class="fa-old-padding-layer">
|
||||||
class="fa-scale-110 fa-old-padding"
|
<FAIcon
|
||||||
icon="ellipsis-h"
|
class="fa-scale-110 "
|
||||||
/>
|
icon="ellipsis-h"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-show="!expanded"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-8 right-16"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-show="expanded"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-8 right-16"
|
||||||
|
icon="times"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -135,6 +173,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
@import '../../_mixins.scss';
|
||||||
|
|
||||||
.ExtraButtons {
|
.ExtraButtons {
|
||||||
/* override of popover internal stuff */
|
/* override of popover internal stuff */
|
||||||
@ -151,6 +190,21 @@
|
|||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-trigger-button {
|
||||||
|
@include unfocused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include focused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faStar } from '@fortawesome/free-solid-svg-icons'
|
import {
|
||||||
|
faStar,
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
faCheck
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {
|
import {
|
||||||
faStar as faStarRegular
|
faStar as faStarRegular
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faStar,
|
faStar,
|
||||||
faStarRegular
|
faStarRegular,
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
faCheck
|
||||||
)
|
)
|
||||||
|
|
||||||
const FavoriteButton = {
|
const FavoriteButton = {
|
||||||
|
@ -7,11 +7,31 @@
|
|||||||
:title="$t('tool_tip.favorite')"
|
:title="$t('tool_tip.favorite')"
|
||||||
@click.prevent="favorite()"
|
@click.prevent="favorite()"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||||
class="fa-scale-110 fa-old-padding"
|
<FAIcon
|
||||||
:icon="[status.favorited ? 'fas' : 'far', 'star']"
|
class="fa-scale-110"
|
||||||
:spin="animated"
|
:icon="[status.favorited ? 'fas' : 'far', 'star']"
|
||||||
/>
|
:spin="animated"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="status.favorited"
|
||||||
|
class="active-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="check"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="!status.favorited"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-else
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="minus"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</button>
|
</button>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
@ -33,6 +53,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
@import '../../_mixins.scss';
|
||||||
|
|
||||||
.FavoriteButton {
|
.FavoriteButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -57,6 +78,26 @@
|
|||||||
color: $fallback--cOrange;
|
color: $fallback--cOrange;
|
||||||
color: var(--cOrange, $fallback--cOrange);
|
color: var(--cOrange, $fallback--cOrange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include unfocused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include focused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||||
import FollowButton from '../follow_button/follow_button.vue'
|
import FollowButton from '../follow_button/follow_button.vue'
|
||||||
|
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||||
|
|
||||||
const FollowCard = {
|
const FollowCard = {
|
||||||
props: [
|
props: [
|
||||||
@ -10,7 +11,8 @@ const FollowCard = {
|
|||||||
components: {
|
components: {
|
||||||
BasicUserCard,
|
BasicUserCard,
|
||||||
RemoteFollow,
|
RemoteFollow,
|
||||||
FollowButton
|
FollowButton,
|
||||||
|
RemoveFollowerButton
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMe () {
|
isMe () {
|
||||||
|
@ -22,6 +22,11 @@
|
|||||||
class="follow-card-follow-button"
|
class="follow-card-follow-button"
|
||||||
:user="user"
|
:user="user"
|
||||||
/>
|
/>
|
||||||
|
<RemoveFollowerButton
|
||||||
|
v-if="noFollowsYou && relationship.followed_by"
|
||||||
|
:relationship="relationship"
|
||||||
|
class="follow-card-button"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</basic-user-card>
|
</basic-user-card>
|
||||||
@ -40,6 +45,12 @@
|
|||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
padding: 0 1.5em;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
&-follow-button {
|
&-follow-button {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -29,10 +29,10 @@
|
|||||||
|
|
||||||
.global-notice-list {
|
.global-notice-list {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50px;
|
top: calc(var(--navbar-height) + 0.5em);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: var(--ZI_popovers);
|
z-index: var(--ZI_navbar_popovers);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -5,6 +5,8 @@ const tabModeDict = {
|
|||||||
mentions: ['mention'],
|
mentions: ['mention'],
|
||||||
'likes+repeats': ['repeat', 'like'],
|
'likes+repeats': ['repeat', 'like'],
|
||||||
follows: ['follow'],
|
follows: ['follow'],
|
||||||
|
reactions: ['pleroma:emoji_reaction'],
|
||||||
|
reports: ['pleroma:report'],
|
||||||
moves: ['move']
|
moves: ['move']
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,7 +14,8 @@ const Interactions = {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||||
filterMode: tabModeDict.mentions
|
filterMode: tabModeDict.mentions,
|
||||||
|
canSeeReports: ['moderator', 'admin'].includes(this.$store.state.users.currentUser.role)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -21,6 +21,15 @@
|
|||||||
key="follows"
|
key="follows"
|
||||||
:label="$t('interactions.follows')"
|
:label="$t('interactions.follows')"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
key="reactions"
|
||||||
|
:label="$t('interactions.emoji_reactions')"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="canSeeReports"
|
||||||
|
key="reports"
|
||||||
|
:label="$t('interactions.reports')"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="!allowFollowingMove"
|
v-if="!allowFollowingMove"
|
||||||
key="moves"
|
key="moves"
|
||||||
|
27
src/components/lists/lists.js
Normal file
27
src/components/lists/lists.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import ListsCard from '../lists_card/lists_card.vue'
|
||||||
|
|
||||||
|
const Lists = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isNew: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ListsCard
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
lists () {
|
||||||
|
return this.$store.state.lists.allLists
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelNewList () {
|
||||||
|
this.isNew = false
|
||||||
|
},
|
||||||
|
newList () {
|
||||||
|
this.isNew = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Lists
|
33
src/components/lists/lists.vue
Normal file
33
src/components/lists/lists.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="Lists panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="title">
|
||||||
|
{{ $t('lists.lists') }}
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'lists-new' }"
|
||||||
|
class="button-default btn new-list-button"
|
||||||
|
>
|
||||||
|
{{ $t("lists.new") }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<ListsCard
|
||||||
|
v-for="list in lists.slice().reverse()"
|
||||||
|
:key="list"
|
||||||
|
:list="list"
|
||||||
|
class="list-item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./lists.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.Lists {
|
||||||
|
.new-list-button {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
16
src/components/lists_card/lists_card.js
Normal file
16
src/components/lists_card/lists_card.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faEllipsisH
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faEllipsisH
|
||||||
|
)
|
||||||
|
|
||||||
|
const ListsCard = {
|
||||||
|
props: [
|
||||||
|
'list'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListsCard
|
51
src/components/lists_card/lists_card.vue
Normal file
51
src/components/lists_card/lists_card.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list-card">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'lists-timeline', params: { id: list.id } }"
|
||||||
|
class="list-name"
|
||||||
|
>
|
||||||
|
{{ list.title }}
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'lists-edit', params: { id: list.id } }"
|
||||||
|
class="button-list-edit"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="ellipsis-h"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./lists_card.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-name,
|
||||||
|
.button-list-edit {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em;
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--link, $fallback--link);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--selectedMenuText, $fallback--link);
|
||||||
|
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-name {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
</style>
|
145
src/components/lists_edit/lists_edit.js
Normal file
145
src/components/lists_edit/lists_edit.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
|
import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
|
||||||
|
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||||
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faSearch,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faSearch,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
|
const ListsNew = {
|
||||||
|
components: {
|
||||||
|
BasicUserCard,
|
||||||
|
UserAvatar,
|
||||||
|
ListsUserSearch,
|
||||||
|
TabSwitcher,
|
||||||
|
PanelLoading
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
title: '',
|
||||||
|
titleDraft: '',
|
||||||
|
membersUserIds: [],
|
||||||
|
removedUserIds: new Set([]), // users we added for members, to undo
|
||||||
|
searchUserIds: [],
|
||||||
|
addedUserIds: new Set([]), // users we added from search, to undo
|
||||||
|
searchLoading: false,
|
||||||
|
reallyDelete: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (!this.id) return
|
||||||
|
this.$store.dispatch('fetchList', { listId: this.id })
|
||||||
|
.then(() => {
|
||||||
|
this.title = this.findListTitle(this.id)
|
||||||
|
this.titleDraft = this.title
|
||||||
|
})
|
||||||
|
this.$store.dispatch('fetchListAccounts', { listId: this.id })
|
||||||
|
.then(() => {
|
||||||
|
this.membersUserIds = this.findListAccounts(this.id)
|
||||||
|
this.membersUserIds.forEach(userId => {
|
||||||
|
this.$store.dispatch('fetchUserIfMissing', userId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
id () {
|
||||||
|
return this.$route.params.id
|
||||||
|
},
|
||||||
|
membersUsers () {
|
||||||
|
return [...this.membersUserIds, ...this.addedUserIds]
|
||||||
|
.map(userId => this.findUser(userId)).filter(user => user)
|
||||||
|
},
|
||||||
|
searchUsers () {
|
||||||
|
return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user)
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser
|
||||||
|
}),
|
||||||
|
...mapGetters(['findUser', 'findListTitle', 'findListAccounts'])
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onInput () {
|
||||||
|
this.search(this.query)
|
||||||
|
},
|
||||||
|
toggleRemoveMember (user) {
|
||||||
|
if (this.removedUserIds.has(user.id)) {
|
||||||
|
this.id && this.addUser(user)
|
||||||
|
this.removedUserIds.delete(user.id)
|
||||||
|
} else {
|
||||||
|
this.id && this.removeUser(user.id)
|
||||||
|
this.removedUserIds.add(user.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleAddFromSearch (user) {
|
||||||
|
if (this.addedUserIds.has(user.id)) {
|
||||||
|
this.id && this.removeUser(user.id)
|
||||||
|
this.addedUserIds.delete(user.id)
|
||||||
|
} else {
|
||||||
|
this.id && this.addUser(user)
|
||||||
|
this.addedUserIds.add(user.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isRemoved (user) {
|
||||||
|
return this.removedUserIds.has(user.id)
|
||||||
|
},
|
||||||
|
isAdded (user) {
|
||||||
|
return this.addedUserIds.has(user.id)
|
||||||
|
},
|
||||||
|
addUser (user) {
|
||||||
|
this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id })
|
||||||
|
},
|
||||||
|
removeUser (userId) {
|
||||||
|
this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id })
|
||||||
|
},
|
||||||
|
onSearchLoading (results) {
|
||||||
|
this.searchLoading = true
|
||||||
|
},
|
||||||
|
onSearchLoadingDone (results) {
|
||||||
|
this.searchLoading = false
|
||||||
|
},
|
||||||
|
onSearchResults (results) {
|
||||||
|
this.searchLoading = false
|
||||||
|
this.searchUserIds = results
|
||||||
|
},
|
||||||
|
updateListTitle () {
|
||||||
|
this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft })
|
||||||
|
.then(() => {
|
||||||
|
this.title = this.findListTitle(this.id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createList () {
|
||||||
|
this.$store.dispatch('createList', { title: this.titleDraft })
|
||||||
|
.then((list) => {
|
||||||
|
return this
|
||||||
|
.$store
|
||||||
|
.dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] })
|
||||||
|
.then(() => list.id)
|
||||||
|
})
|
||||||
|
.then((listId) => {
|
||||||
|
this.$router.push({ name: 'lists-timeline', params: { id: listId } })
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.$store.dispatch('pushGlobalNotice', {
|
||||||
|
messageKey: 'lists.error',
|
||||||
|
messageArgs: [e.message],
|
||||||
|
level: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteList () {
|
||||||
|
this.$store.dispatch('deleteList', { listId: this.id })
|
||||||
|
this.$router.push({ name: 'lists' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListsNew
|
228
src/components/lists_edit/lists_edit.vue
Normal file
228
src/components/lists_edit/lists_edit.vue
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel-default panel ListEdit">
|
||||||
|
<div
|
||||||
|
ref="header"
|
||||||
|
class="panel-heading list-edit-heading"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button-unstyled go-back-button"
|
||||||
|
@click="$router.back"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
size="lg"
|
||||||
|
icon="chevron-left"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="title">
|
||||||
|
<i18n-t
|
||||||
|
v-if="id"
|
||||||
|
keypath="lists.editing_list"
|
||||||
|
>
|
||||||
|
<template #listTitle>
|
||||||
|
{{ title }}
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<i18n-t
|
||||||
|
v-else
|
||||||
|
keypath="lists.creating_list"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="input-wrap">
|
||||||
|
<label for="list-edit-title">{{ $t('lists.title') }}</label>
|
||||||
|
{{ ' ' }}
|
||||||
|
<input
|
||||||
|
id="list-edit-title"
|
||||||
|
ref="title"
|
||||||
|
v-model="titleDraft"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="id"
|
||||||
|
class="btn button-default follow-button"
|
||||||
|
@click="updateListTitle"
|
||||||
|
>
|
||||||
|
{{ $t('lists.update_title') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<tab-switcher
|
||||||
|
class="list-member-management"
|
||||||
|
:scrollable-tabs="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="id || addedUserIds.size > 0"
|
||||||
|
:label="$t('lists.manage_members')"
|
||||||
|
class="members-list"
|
||||||
|
>
|
||||||
|
<div class="users-list">
|
||||||
|
<div
|
||||||
|
v-for="user in membersUsers"
|
||||||
|
:key="user.id"
|
||||||
|
class="member"
|
||||||
|
>
|
||||||
|
<BasicUserCard
|
||||||
|
:user="user"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn button-default follow-button"
|
||||||
|
@click="toggleRemoveMember(user)"
|
||||||
|
>
|
||||||
|
{{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
|
||||||
|
</button>
|
||||||
|
</BasicUserCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="search-list"
|
||||||
|
:label="$t('lists.add_members')"
|
||||||
|
>
|
||||||
|
<ListsUserSearch
|
||||||
|
@results="onSearchResults"
|
||||||
|
@loading="onSearchLoading"
|
||||||
|
@loadingDone="onSearchLoadingDone"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="searchLoading"
|
||||||
|
class="loading"
|
||||||
|
>
|
||||||
|
<PanelLoading />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="users-list"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="user in searchUsers"
|
||||||
|
:key="user.id"
|
||||||
|
class="member"
|
||||||
|
>
|
||||||
|
<BasicUserCard
|
||||||
|
:user="user"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="membersUserIds.includes(user.id)"
|
||||||
|
>
|
||||||
|
{{ $t('lists.is_in_list') }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="!membersUserIds.includes(user.id)"
|
||||||
|
class="btn button-default follow-button"
|
||||||
|
@click="toggleAddFromSearch(user)"
|
||||||
|
>
|
||||||
|
{{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn button-default follow-button"
|
||||||
|
@click="toggleRemoveMember(user)"
|
||||||
|
>
|
||||||
|
{{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }}
|
||||||
|
</button>
|
||||||
|
</BasicUserCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<span class="spacer" />
|
||||||
|
<button
|
||||||
|
v-if="!id"
|
||||||
|
class="btn button-default footer-button"
|
||||||
|
@click="createList"
|
||||||
|
>
|
||||||
|
{{ $t('lists.create') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="!reallyDelete"
|
||||||
|
class="btn button-default footer-button"
|
||||||
|
@click="reallyDelete = true"
|
||||||
|
>
|
||||||
|
{{ $t('lists.delete') }}
|
||||||
|
</button>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('lists.really_delete') }}
|
||||||
|
<button
|
||||||
|
class="btn button-default footer-button"
|
||||||
|
@click="deleteList"
|
||||||
|
>
|
||||||
|
{{ $t('general.yes') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default footer-button"
|
||||||
|
@click="reallyDelete = false"
|
||||||
|
>
|
||||||
|
{{ $t('general.no') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./lists_edit.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.ListEdit {
|
||||||
|
--panel-body-padding: 0.5em;
|
||||||
|
|
||||||
|
height: calc(100vh - var(--navbar-height));
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.list-edit-heading {
|
||||||
|
grid-template-columns: auto minmax(50%, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-member-management {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-list {
|
||||||
|
padding-bottom: 0.7rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .search-list,
|
||||||
|
& .members-list {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-button {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
height: 100%;
|
||||||
|
align-self: start;
|
||||||
|
width: var(--__panel-heading-height-inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
grid-template-columns: minmax(10%, 1fr);
|
||||||
|
|
||||||
|
.footer-button {
|
||||||
|
min-width: 9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
22
src/components/lists_menu/lists_menu_content.js
Normal file
22
src/components/lists_menu/lists_menu_content.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { mapState } from 'vuex'
|
||||||
|
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||||
|
import { getListEntries } from 'src/components/navigation/filter.js'
|
||||||
|
|
||||||
|
export const ListsMenuContent = {
|
||||||
|
props: [
|
||||||
|
'showPin'
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
NavigationEntry
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
lists: getListEntries,
|
||||||
|
currentUser: state => state.users.currentUser,
|
||||||
|
privateMode: state => state.instance.private,
|
||||||
|
federating: state => state.instance.federating
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListsMenuContent
|
12
src/components/lists_menu/lists_menu_content.vue
Normal file
12
src/components/lists_menu/lists_menu_content.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<NavigationEntry
|
||||||
|
v-for="item in lists"
|
||||||
|
:key="item.name"
|
||||||
|
:show-pin="showPin"
|
||||||
|
:item="item"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./lists_menu_content.js"></script>
|
36
src/components/lists_timeline/lists_timeline.js
Normal file
36
src/components/lists_timeline/lists_timeline.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Timeline from '../timeline/timeline.vue'
|
||||||
|
const ListsTimeline = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
listId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Timeline
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
timeline () { return this.$store.state.statuses.timelines.list }
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: function (route) {
|
||||||
|
if (route.name === 'lists-timeline' && route.params.id !== this.listId) {
|
||||||
|
this.listId = route.params.id
|
||||||
|
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'list' })
|
||||||
|
this.$store.dispatch('fetchList', { listId: this.listId })
|
||||||
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.listId = this.$route.params.id
|
||||||
|
this.$store.dispatch('fetchList', { listId: this.listId })
|
||||||
|
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||||
|
},
|
||||||
|
unmounted () {
|
||||||
|
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'list' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListsTimeline
|
10
src/components/lists_timeline/lists_timeline.vue
Normal file
10
src/components/lists_timeline/lists_timeline.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<Timeline
|
||||||
|
title="list.name"
|
||||||
|
:timeline="timeline"
|
||||||
|
:list-id="listId"
|
||||||
|
timeline-name="list"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./lists_timeline.js"></script>
|
51
src/components/lists_user_search/lists_user_search.js
Normal file
51
src/components/lists_user_search/lists_user_search.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faSearch,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faSearch,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
|
const ListsUserSearch = {
|
||||||
|
components: {
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
emits: ['loading', 'loadingDone', 'results'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
query: '',
|
||||||
|
followingOnly: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onInput: debounce(function () {
|
||||||
|
this.search(this.query)
|
||||||
|
}, 2000),
|
||||||
|
search (query) {
|
||||||
|
if (!query) {
|
||||||
|
this.loading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.$emit('loading')
|
||||||
|
this.userIds = []
|
||||||
|
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
|
||||||
|
.then(data => {
|
||||||
|
this.$emit('results', data.accounts.map(a => a.id))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
this.$emit('loadingDone')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListsUserSearch
|
47
src/components/lists_user_search/lists_user_search.vue
Normal file
47
src/components/lists_user_search/lists_user_search.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ListsUserSearch">
|
||||||
|
<div class="input-wrap">
|
||||||
|
<div class="input-search">
|
||||||
|
<FAIcon
|
||||||
|
class="search-icon fa-scale-110 fa-old-padding"
|
||||||
|
icon="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="search"
|
||||||
|
v-model="query"
|
||||||
|
:placeholder="$t('lists.search')"
|
||||||
|
@input="onInput"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="input-wrap">
|
||||||
|
<Checkbox
|
||||||
|
v-model="followingOnly"
|
||||||
|
@change="onInput"
|
||||||
|
>
|
||||||
|
{{ $t('lists.following_only') }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./lists_user_search.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.ListsUserSearch {
|
||||||
|
.input-wrap {
|
||||||
|
display: flex;
|
||||||
|
margin: 0.7em 0.5em 0.7em 0.5em;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
@ -16,6 +17,7 @@ const MentionLink = {
|
|||||||
name: 'MentionLink',
|
name: 'MentionLink',
|
||||||
components: {
|
components: {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
|
UnicodeDomainIndicator,
|
||||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -47,6 +47,9 @@
|
|||||||
class="serverName"
|
class="serverName"
|
||||||
:class="{ '-faded': shouldFadeDomain }"
|
:class="{ '-faded': shouldFadeDomain }"
|
||||||
v-html="'@' + serverName"
|
v-html="'@' + serverName"
|
||||||
|
/><UnicodeDomainIndicator
|
||||||
|
v-if="shouldShowFullUserName"
|
||||||
|
:user="user"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
|||||||
import Notifications from '../notifications/notifications.vue'
|
import Notifications from '../notifications/notifications.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
|
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
@ -19,7 +20,8 @@ library.add(
|
|||||||
const MobileNav = {
|
const MobileNav = {
|
||||||
components: {
|
components: {
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
Notifications
|
Notifications,
|
||||||
|
NavigationPins
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
notificationsCloseGesture: undefined,
|
notificationsCloseGesture: undefined,
|
||||||
@ -47,7 +49,10 @@ const MobileNav = {
|
|||||||
isChat () {
|
isChat () {
|
||||||
return this.$route.name === 'chat'
|
return this.$route.name === 'chat'
|
||||||
},
|
},
|
||||||
...mapGetters(['unreadChatCount'])
|
...mapGetters(['unreadChatCount']),
|
||||||
|
chatsPinned () {
|
||||||
|
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleMobileSidebar () {
|
toggleMobileSidebar () {
|
||||||
|
@ -17,20 +17,12 @@
|
|||||||
icon="bars"
|
icon="bars"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="unreadChatCount"
|
v-if="unreadChatCount && !chatsPinned"
|
||||||
class="alert-dot"
|
class="alert-dot"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<router-link
|
<NavigationPins class="pins" />
|
||||||
v-if="!hideSitename"
|
</div> <div class="item right">
|
||||||
class="site-name"
|
|
||||||
:to="{ name: 'root' }"
|
|
||||||
active-class="home"
|
|
||||||
>
|
|
||||||
{{ sitename }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="item right">
|
|
||||||
<button
|
<button
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
class="button-unstyled mobile-nav-button"
|
class="button-unstyled mobile-nav-button"
|
||||||
@ -94,6 +86,7 @@
|
|||||||
grid-template-columns: 2fr auto;
|
grid-template-columns: 2fr auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--topBarLink, $fallback--link);
|
color: var(--topBarLink, $fallback--link);
|
||||||
}
|
}
|
||||||
@ -178,13 +171,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pins {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.pinned-item {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-notifications {
|
.mobile-notifications {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
@ -194,14 +194,17 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.panel:after {
|
|
||||||
|
.panel::after {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel .panel-heading {
|
.panel .panel-heading {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -10,7 +10,8 @@ library.add(
|
|||||||
|
|
||||||
const HIDDEN_FOR_PAGES = new Set([
|
const HIDDEN_FOR_PAGES = new Set([
|
||||||
'chats',
|
'chats',
|
||||||
'chat'
|
'chat',
|
||||||
|
'lists-edit'
|
||||||
])
|
])
|
||||||
|
|
||||||
const MobilePostStatusButton = {
|
const MobilePostStatusButton = {
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue'
|
import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js'
|
||||||
|
import { filterNavigation } from 'src/components/navigation/filter.js'
|
||||||
|
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||||
|
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
@ -12,7 +17,8 @@ import {
|
|||||||
faComments,
|
faComments,
|
||||||
faBell,
|
faBell,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faStream
|
faStream,
|
||||||
|
faList
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
@ -25,26 +31,52 @@ library.add(
|
|||||||
faComments,
|
faComments,
|
||||||
faBell,
|
faBell,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faStream
|
faStream,
|
||||||
|
faList
|
||||||
)
|
)
|
||||||
|
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
|
props: ['forceExpand', 'forceEditMode'],
|
||||||
created () {
|
created () {
|
||||||
if (this.currentUser && this.currentUser.locked) {
|
|
||||||
this.$store.dispatch('startFetchingFollowRequests')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
TimelineMenuContent
|
ListsMenuContent,
|
||||||
|
NavigationEntry,
|
||||||
|
NavigationPins,
|
||||||
|
Checkbox
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showTimelines: false
|
editMode: false,
|
||||||
|
showTimelines: false,
|
||||||
|
showLists: false,
|
||||||
|
timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })),
|
||||||
|
rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k }))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleTimelines () {
|
toggleTimelines () {
|
||||||
this.showTimelines = !this.showTimelines
|
this.showTimelines = !this.showTimelines
|
||||||
|
},
|
||||||
|
toggleLists () {
|
||||||
|
this.showLists = !this.showLists
|
||||||
|
},
|
||||||
|
toggleEditMode () {
|
||||||
|
this.editMode = !this.editMode
|
||||||
|
},
|
||||||
|
toggleCollapse () {
|
||||||
|
this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed })
|
||||||
|
this.$store.dispatch('pushServerSideStorage')
|
||||||
|
},
|
||||||
|
isPinned (item) {
|
||||||
|
return this.pinnedItems.has(item)
|
||||||
|
},
|
||||||
|
togglePin (item) {
|
||||||
|
if (this.isPinned(item)) {
|
||||||
|
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||||
|
} else {
|
||||||
|
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||||
|
}
|
||||||
|
this.$store.dispatch('pushServerSideStorage')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -53,8 +85,36 @@ const NavPanel = {
|
|||||||
followRequestCount: state => state.api.followRequests.length,
|
followRequestCount: state => state.api.followRequests.length,
|
||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
federating: state => state.instance.federating,
|
federating: state => state.instance.federating,
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||||
|
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
|
||||||
|
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav
|
||||||
}),
|
}),
|
||||||
|
timelinesItems () {
|
||||||
|
return filterNavigation(
|
||||||
|
Object
|
||||||
|
.entries({ ...TIMELINES })
|
||||||
|
.map(([k, v]) => ({ ...v, name: k })),
|
||||||
|
{
|
||||||
|
hasChats: this.pleromaChatMessagesAvailable,
|
||||||
|
isFederating: this.federating,
|
||||||
|
isPrivate: this.privateMode,
|
||||||
|
currentUser: this.currentUser
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
rootItems () {
|
||||||
|
return filterNavigation(
|
||||||
|
Object
|
||||||
|
.entries({ ...ROOT_ITEMS })
|
||||||
|
.map(([k, v]) => ({ ...v, name: k })),
|
||||||
|
{
|
||||||
|
hasChats: this.pleromaChatMessagesAvailable,
|
||||||
|
isFederating: this.federating,
|
||||||
|
isPrivate: this.privateMode,
|
||||||
|
currentUser: this.currentUser
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
...mapGetters(['unreadChatCount'])
|
...mapGetters(['unreadChatCount'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,90 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="NavPanel">
|
<div class="NavPanel">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<ul>
|
<div
|
||||||
<li v-if="currentUser || !privateMode">
|
v-if="!forceExpand"
|
||||||
<button
|
class="panel-heading nav-panel-heading"
|
||||||
class="button-unstyled menu-item"
|
>
|
||||||
@click="toggleTimelines"
|
<NavigationPins :limit="6" />
|
||||||
>
|
<div class="spacer" />
|
||||||
<FAIcon
|
<button
|
||||||
fixed-width
|
class="button-unstyled"
|
||||||
class="fa-scale-110"
|
@click="toggleCollapse"
|
||||||
icon="stream"
|
>
|
||||||
/>{{ $t("nav.timelines") }}
|
<FAIcon
|
||||||
<FAIcon
|
class="timelines-chevron"
|
||||||
class="timelines-chevron"
|
fixed-width
|
||||||
fixed-width
|
:icon="collapsed ? 'chevron-down' : 'chevron-up'"
|
||||||
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-if="!collapsed || forceExpand"
|
||||||
|
class="panel-body"
|
||||||
|
>
|
||||||
|
<NavigationEntry
|
||||||
|
v-if="currentUser || !privateMode"
|
||||||
|
:show-pin="false"
|
||||||
|
:item="{ icon: 'stream', label: 'nav.timelines' }"
|
||||||
|
:aria-expanded="showTimelines ? 'true' : 'false'"
|
||||||
|
@click="toggleTimelines"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="timelines-chevron"
|
||||||
|
fixed-width
|
||||||
|
:icon="showTimelines ? 'chevron-up' : 'chevron-down'"
|
||||||
|
/>
|
||||||
|
</NavigationEntry>
|
||||||
|
<div
|
||||||
|
v-show="showTimelines"
|
||||||
|
class="timelines-background"
|
||||||
|
>
|
||||||
|
<div class="timelines">
|
||||||
|
<NavigationEntry
|
||||||
|
v-for="item in timelinesItems"
|
||||||
|
:key="item.name"
|
||||||
|
:show-pin="editMode || forceEditMode"
|
||||||
|
:item="item"
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-show="showTimelines"
|
|
||||||
class="timelines-background"
|
|
||||||
>
|
|
||||||
<TimelineMenuContent class="timelines" />
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<li v-if="currentUser">
|
<NavigationEntry
|
||||||
|
v-if="currentUser"
|
||||||
|
:show-pin="false"
|
||||||
|
:item="{ icon: 'list', label: 'nav.lists' }"
|
||||||
|
:aria-expanded="showLists ? 'true' : 'false'"
|
||||||
|
@click="toggleLists"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
class="menu-item"
|
:title="$t('lists.manage_lists')"
|
||||||
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
|
class="extra-button"
|
||||||
|
:to="{ name: 'lists' }"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
class="extra-button"
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110"
|
icon="wrench"
|
||||||
icon="bell"
|
/>
|
||||||
/>{{ $t("nav.interactions") }}
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
<FAIcon
|
||||||
<li v-if="currentUser && pleromaChatMessagesAvailable">
|
class="timelines-chevron"
|
||||||
<router-link
|
fixed-width
|
||||||
class="menu-item"
|
:icon="showLists ? 'chevron-up' : 'chevron-down'"
|
||||||
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
|
/>
|
||||||
>
|
</NavigationEntry>
|
||||||
<div
|
<div
|
||||||
v-if="unreadChatCount"
|
v-show="showLists"
|
||||||
class="badge badge-notification"
|
class="timelines-background"
|
||||||
>
|
>
|
||||||
{{ unreadChatCount }}
|
<ListsMenuContent
|
||||||
</div>
|
:show-pin="editMode || forceEditMode"
|
||||||
<FAIcon
|
class="timelines"
|
||||||
fixed-width
|
/>
|
||||||
class="fa-scale-110"
|
</div>
|
||||||
icon="comments"
|
<NavigationEntry
|
||||||
/>{{ $t("nav.chats") }}
|
v-for="item in rootItems"
|
||||||
</router-link>
|
:key="item.name"
|
||||||
</li>
|
:show-pin="editMode || forceEditMode"
|
||||||
<li v-if="currentUser && currentUser.locked">
|
:item="item"
|
||||||
<router-link
|
/>
|
||||||
class="menu-item"
|
<NavigationEntry
|
||||||
:to="{ name: 'friend-requests' }"
|
v-if="!forceEditMode && currentUser"
|
||||||
>
|
:show-pin="false"
|
||||||
<FAIcon
|
:item="{ label: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }"
|
||||||
fixed-width
|
@click="toggleEditMode"
|
||||||
class="fa-scale-110"
|
/>
|
||||||
icon="user-plus"
|
|
||||||
/>{{ $t("nav.friend_requests") }}
|
|
||||||
<span
|
|
||||||
v-if="followRequestCount > 0"
|
|
||||||
class="badge badge-notification"
|
|
||||||
>
|
|
||||||
{{ followRequestCount }}
|
|
||||||
</span>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link
|
|
||||||
class="menu-item"
|
|
||||||
:to="{ name: 'about' }"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
class="fa-scale-110"
|
|
||||||
icon="info-circle"
|
|
||||||
/>{{ $t("nav.about") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -112,7 +121,6 @@
|
|||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
border-color: var(--border, $fallback--border);
|
border-color: var(--border, $fallback--border);
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> li {
|
> li {
|
||||||
@ -135,46 +143,9 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 3.5em;
|
|
||||||
line-height: 3.5em;
|
|
||||||
padding: 0 1em;
|
|
||||||
width: 100%;
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--link, $fallback--link);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $fallback--lightBg;
|
|
||||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--selectedMenuText, $fallback--link);
|
|
||||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
|
||||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
|
||||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
|
||||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.router-link-active {
|
|
||||||
font-weight: bolder;
|
|
||||||
background-color: $fallback--lightBg;
|
|
||||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--selectedMenuText, $fallback--text);
|
|
||||||
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
|
||||||
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
|
||||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
|
||||||
--icon: var(--selectedMenuIcon, $fallback--icon);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timelines-chevron {
|
.timelines-chevron {
|
||||||
margin-left: 0.8em;
|
margin-left: 0.8em;
|
||||||
|
margin-right: 0.8em;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +153,7 @@
|
|||||||
padding: 0 0 0 0.6em;
|
padding: 0 0 0 0.6em;
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--lightBg;
|
||||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||||
border-top: 1px solid;
|
border-bottom: 1px solid;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
border-color: var(--border, $fallback--border);
|
border-color: var(--border, $fallback--border);
|
||||||
}
|
}
|
||||||
@ -192,14 +163,9 @@
|
|||||||
background-color: var(--bg, $fallback--bg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-scale-110 {
|
.nav-panel-heading {
|
||||||
margin-right: 0.8em;
|
// breaks without a unit
|
||||||
}
|
--panel-heading-height-padding: 0em;
|
||||||
|
|
||||||
.badge {
|
|
||||||
position: absolute;
|
|
||||||
right: 0.6rem;
|
|
||||||
top: 1.25em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
18
src/components/navigation/filter.js
Normal file
18
src/components/navigation/filter.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export const filterNavigation = (list = [], { hasChats, isFederating, isPrivate, currentUser }) => {
|
||||||
|
return list.filter(({ criteria, anon, anonRoute }) => {
|
||||||
|
const set = new Set(criteria || [])
|
||||||
|
if (!isFederating && set.has('federating')) return false
|
||||||
|
if (isPrivate && set.has('!private')) return false
|
||||||
|
if (!currentUser && !(anon || anonRoute)) return false
|
||||||
|
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
|
||||||
|
if (!hasChats && set.has('chats')) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getListEntries = state => state.lists.allLists.map(list => ({
|
||||||
|
name: 'list-' + list.id,
|
||||||
|
routeObject: { name: 'lists-timeline', params: { id: list.id } },
|
||||||
|
labelRaw: list.title,
|
||||||
|
iconLetter: list.title[0]
|
||||||
|
}))
|
75
src/components/navigation/navigation.js
Normal file
75
src/components/navigation/navigation.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
export const USERNAME_ROUTES = new Set([
|
||||||
|
'bookmarks',
|
||||||
|
'dms',
|
||||||
|
'interactions',
|
||||||
|
'notifications',
|
||||||
|
'chat',
|
||||||
|
'chats',
|
||||||
|
'user-profile'
|
||||||
|
])
|
||||||
|
|
||||||
|
export const TIMELINES = {
|
||||||
|
home: {
|
||||||
|
route: 'friends',
|
||||||
|
icon: 'home',
|
||||||
|
label: 'nav.home_timeline',
|
||||||
|
criteria: ['!private']
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
route: 'public-timeline',
|
||||||
|
anon: true,
|
||||||
|
icon: 'users',
|
||||||
|
label: 'nav.public_tl',
|
||||||
|
criteria: ['!private']
|
||||||
|
},
|
||||||
|
twkn: {
|
||||||
|
route: 'public-external-timeline',
|
||||||
|
anon: true,
|
||||||
|
icon: 'globe',
|
||||||
|
label: 'nav.twkn',
|
||||||
|
criteria: ['!private', 'federating']
|
||||||
|
},
|
||||||
|
bookmarks: {
|
||||||
|
route: 'bookmarks',
|
||||||
|
icon: 'bookmark',
|
||||||
|
label: 'nav.bookmarks'
|
||||||
|
},
|
||||||
|
favorites: {
|
||||||
|
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },
|
||||||
|
icon: 'star',
|
||||||
|
label: 'user_card.favorites'
|
||||||
|
},
|
||||||
|
dms: {
|
||||||
|
route: 'dms',
|
||||||
|
icon: 'envelope',
|
||||||
|
label: 'nav.dms'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROOT_ITEMS = {
|
||||||
|
interactions: {
|
||||||
|
route: 'interactions',
|
||||||
|
icon: 'bell',
|
||||||
|
label: 'nav.interactions'
|
||||||
|
},
|
||||||
|
chats: {
|
||||||
|
route: 'chats',
|
||||||
|
icon: 'comments',
|
||||||
|
label: 'nav.chats',
|
||||||
|
badgeGetter: 'unreadChatCount',
|
||||||
|
criteria: ['chats']
|
||||||
|
},
|
||||||
|
friendRequests: {
|
||||||
|
route: 'friend-requests',
|
||||||
|
icon: 'user-plus',
|
||||||
|
label: 'nav.friend_requests',
|
||||||
|
criteria: ['lockedUser'],
|
||||||
|
badgeGetter: 'followRequestCount'
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
route: 'about',
|
||||||
|
anon: true,
|
||||||
|
icon: 'info-circle',
|
||||||
|
label: 'nav.about'
|
||||||
|
}
|
||||||
|
}
|
51
src/components/navigation/navigation_entry.js
Normal file
51
src/components/navigation/navigation_entry.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { mapState } from 'vuex'
|
||||||
|
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
||||||
|
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faThumbtack)
|
||||||
|
|
||||||
|
const NavigationEntry = {
|
||||||
|
props: ['item', 'showPin'],
|
||||||
|
components: {
|
||||||
|
OptionalRouterLink
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isPinned (value) {
|
||||||
|
return this.pinnedItems.has(value)
|
||||||
|
},
|
||||||
|
togglePin (value) {
|
||||||
|
if (this.isPinned(value)) {
|
||||||
|
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||||
|
} else {
|
||||||
|
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||||
|
}
|
||||||
|
this.$store.dispatch('pushServerSideStorage')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
routeTo () {
|
||||||
|
if (!this.item.route && !this.item.routeObject) return null
|
||||||
|
let route
|
||||||
|
if (this.item.routeObject) {
|
||||||
|
route = this.item.routeObject
|
||||||
|
} else {
|
||||||
|
route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute }
|
||||||
|
}
|
||||||
|
if (USERNAME_ROUTES.has(route.name)) {
|
||||||
|
route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name }
|
||||||
|
}
|
||||||
|
return route
|
||||||
|
},
|
||||||
|
getters () {
|
||||||
|
return this.$store.getters
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
currentUser: state => state.users.currentUser,
|
||||||
|
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavigationEntry
|
133
src/components/navigation/navigation_entry.vue
Normal file
133
src/components/navigation/navigation_entry.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<OptionalRouterLink
|
||||||
|
v-slot="{ isActive, href, navigate } = {}"
|
||||||
|
ass="ass"
|
||||||
|
:to="routeTo"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="NavigationEntry menu-item"
|
||||||
|
:class="{ '-active': isActive }"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="routeTo ? 'a' : 'button'"
|
||||||
|
class="main-link button-unstyled"
|
||||||
|
:href="href"
|
||||||
|
@click="navigate"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<FAIcon
|
||||||
|
v-if="item.icon"
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 menu-icon"
|
||||||
|
:icon="item.icon"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="item.iconLetter"
|
||||||
|
class="icon iconLetter fa-scale-110 menu-icon"
|
||||||
|
>{{ item.iconLetter }}
|
||||||
|
</span>
|
||||||
|
<span class="label">
|
||||||
|
{{ item.labelRaw || $t(item.label) }}
|
||||||
|
</span>
|
||||||
|
</component>
|
||||||
|
<slot />
|
||||||
|
<div
|
||||||
|
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
||||||
|
class="badge badge-notification"
|
||||||
|
>
|
||||||
|
{{ getters[item.badgeGetter] }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showPin && currentUser"
|
||||||
|
type="button"
|
||||||
|
class="button-unstyled extra-button"
|
||||||
|
:title="$t(isPinned ? 'general.unpin' : 'general.pin' )"
|
||||||
|
:aria-pressed="!!isPinned"
|
||||||
|
@click.stop.prevent="togglePin(item.name)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
v-if="showPin && currentUser"
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110"
|
||||||
|
:class="{ 'veryfaint': !isPinned(item.name) }"
|
||||||
|
:transform="!isPinned(item.name) ? 'rotate-45' : ''"
|
||||||
|
icon="thumbtack"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</OptionalRouterLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./navigation_entry.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.NavigationEntry {
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: baseline;
|
||||||
|
height: 3.5em;
|
||||||
|
line-height: 3.5em;
|
||||||
|
padding: 0 1em;
|
||||||
|
width: 100%;
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--link, $fallback--link);
|
||||||
|
|
||||||
|
.timelines-chevron {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-link {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
margin-right: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra-button {
|
||||||
|
width: 3em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: -0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--selectedMenuText, $fallback--link);
|
||||||
|
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
--icon: var(--text, $fallback--icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-active {
|
||||||
|
font-weight: bolder;
|
||||||
|
background-color: $fallback--lightBg;
|
||||||
|
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--selectedMenuText, $fallback--text);
|
||||||
|
--faint: var(--selectedMenuFaintText, $fallback--faint);
|
||||||
|
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
|
||||||
|
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
--icon: var(--text, $fallback--icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
88
src/components/navigation/navigation_pins.js
Normal file
88
src/components/navigation/navigation_pins.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { mapState } from 'vuex'
|
||||||
|
import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
||||||
|
import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faUsers,
|
||||||
|
faGlobe,
|
||||||
|
faBookmark,
|
||||||
|
faEnvelope,
|
||||||
|
faComments,
|
||||||
|
faBell,
|
||||||
|
faInfoCircle,
|
||||||
|
faStream,
|
||||||
|
faList
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faUsers,
|
||||||
|
faGlobe,
|
||||||
|
faBookmark,
|
||||||
|
faEnvelope,
|
||||||
|
faComments,
|
||||||
|
faBell,
|
||||||
|
faInfoCircle,
|
||||||
|
faStream,
|
||||||
|
faList
|
||||||
|
)
|
||||||
|
|
||||||
|
const NavPanel = {
|
||||||
|
props: ['limit'],
|
||||||
|
methods: {
|
||||||
|
getRouteTo (item) {
|
||||||
|
if (item.routeObject) {
|
||||||
|
return item.routeObject
|
||||||
|
}
|
||||||
|
const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute }
|
||||||
|
if (USERNAME_ROUTES.has(route.name)) {
|
||||||
|
route.params = { username: this.currentUser.screen_name }
|
||||||
|
}
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getters () {
|
||||||
|
return this.$store.getters
|
||||||
|
},
|
||||||
|
...mapState({
|
||||||
|
lists: getListEntries,
|
||||||
|
currentUser: state => state.users.currentUser,
|
||||||
|
followRequestCount: state => state.api.followRequests.length,
|
||||||
|
privateMode: state => state.instance.private,
|
||||||
|
federating: state => state.instance.federating,
|
||||||
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||||
|
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||||
|
}),
|
||||||
|
pinnedList () {
|
||||||
|
if (!this.currentUser) {
|
||||||
|
return [
|
||||||
|
{ ...TIMELINES.public, name: 'public' },
|
||||||
|
{ ...TIMELINES.twkn, name: 'twkn' },
|
||||||
|
{ ...ROOT_ITEMS.about, name: 'about' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return filterNavigation(
|
||||||
|
[
|
||||||
|
...Object
|
||||||
|
.entries({ ...TIMELINES })
|
||||||
|
.filter(([k]) => this.pinnedItems.has(k))
|
||||||
|
.map(([k, v]) => ({ ...v, name: k })),
|
||||||
|
...this.lists.filter((k) => this.pinnedItems.has(k.name)),
|
||||||
|
...Object
|
||||||
|
.entries({ ...ROOT_ITEMS })
|
||||||
|
.filter(([k]) => this.pinnedItems.has(k))
|
||||||
|
.map(([k, v]) => ({ ...v, name: k }))
|
||||||
|
],
|
||||||
|
{
|
||||||
|
hasChats: this.pleromaChatMessagesAvailable,
|
||||||
|
isFederating: this.federating,
|
||||||
|
isPrivate: this.privateMode,
|
||||||
|
currentUser: this.currentUser
|
||||||
|
}
|
||||||
|
).slice(0, this.limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavPanel
|
76
src/components/navigation/navigation_pins.vue
Normal file
76
src/components/navigation/navigation_pins.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<span class="NavigationPins">
|
||||||
|
<router-link
|
||||||
|
v-for="item in pinnedList"
|
||||||
|
:key="item.name"
|
||||||
|
class="pinned-item"
|
||||||
|
:to="getRouteTo(item)"
|
||||||
|
:title="item.labelRaw || $t(item.label)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
v-if="item.icon"
|
||||||
|
fixed-width
|
||||||
|
:icon="item.icon"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="item.iconLetter"
|
||||||
|
class="iconLetter fa-scale-110 fa-old-padding"
|
||||||
|
>{{ item.iconLetter }}</span>
|
||||||
|
<div
|
||||||
|
v-if="item.badgeGetter && getters[item.badgeGetter]"
|
||||||
|
class="alert-dot"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./navigation_pins.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
.NavigationPins {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.alert-dot {
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 0.5em;
|
||||||
|
width: 0.5em;
|
||||||
|
position: absolute;
|
||||||
|
right: calc(50% - 0.25em);
|
||||||
|
top: calc(50% - 0.25em);
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-top: -6px;
|
||||||
|
background-color: $fallback--cRed;
|
||||||
|
background-color: var(--badgeNotification, $fallback--cRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-item {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 0 3em;
|
||||||
|
min-width: 2em;
|
||||||
|
text-align: center;
|
||||||
|
overflow: visible;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& .svg-inline--fa,
|
||||||
|
& .iconLetter {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.router-link-active {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--selectedMenuText, $fallback--text);
|
||||||
|
border-bottom: 4px solid;
|
||||||
|
|
||||||
|
& .svg-inline--fa,
|
||||||
|
& .iconLetter {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -4,6 +4,8 @@ import Status from '../status/status.vue'
|
|||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
|
import Report from '../report/report.vue'
|
||||||
|
import UserLink from '../user_link/user_link.vue'
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import UserPopover from '../user_popover/user_popover.vue'
|
import UserPopover from '../user_popover/user_popover.vue'
|
||||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||||
@ -47,8 +49,10 @@ const Notification = {
|
|||||||
UserCard,
|
UserCard,
|
||||||
Timeago,
|
Timeago,
|
||||||
Status,
|
Status,
|
||||||
|
Report,
|
||||||
RichContent,
|
RichContent,
|
||||||
UserPopover
|
UserPopover,
|
||||||
|
UserLink
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
@ -11,9 +11,10 @@
|
|||||||
class="Notification container -muted"
|
class="Notification container -muted"
|
||||||
>
|
>
|
||||||
<small>
|
<small>
|
||||||
<router-link :to="userProfileLink">
|
<user-link
|
||||||
{{ notification.from_profile.screen_name_ui }}
|
:user="notification.from_profile"
|
||||||
</router-link>
|
:at="false"
|
||||||
|
/>
|
||||||
</small>
|
</small>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled unmute"
|
class="button-unstyled unmute"
|
||||||
@ -121,6 +122,9 @@
|
|||||||
</i18n-t>
|
</i18n-t>
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="notification.type === 'pleroma:report'">
|
||||||
|
<small>{{ $t('notifications.submitted_report') }}</small>
|
||||||
|
</span>
|
||||||
<span v-if="notification.type === 'poll'">
|
<span v-if="notification.type === 'poll'">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="type-icon"
|
class="type-icon"
|
||||||
@ -171,12 +175,10 @@
|
|||||||
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
|
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
|
||||||
class="follow-text"
|
class="follow-text"
|
||||||
>
|
>
|
||||||
<router-link
|
<user-link
|
||||||
:to="userProfileLink"
|
|
||||||
class="follow-name"
|
class="follow-name"
|
||||||
>
|
:user="notification.from_profile"
|
||||||
@{{ notification.from_profile.screen_name_ui }}
|
/>
|
||||||
</router-link>
|
|
||||||
<div
|
<div
|
||||||
v-if="notification.type === 'follow_request'"
|
v-if="notification.type === 'follow_request'"
|
||||||
style="white-space: nowrap;"
|
style="white-space: nowrap;"
|
||||||
@ -207,10 +209,14 @@
|
|||||||
v-else-if="notification.type === 'move'"
|
v-else-if="notification.type === 'move'"
|
||||||
class="move-text"
|
class="move-text"
|
||||||
>
|
>
|
||||||
<router-link :to="targetUserProfileLink">
|
<user-link
|
||||||
@{{ notification.target.screen_name_ui }}
|
:user="notification.target"
|
||||||
</router-link>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Report
|
||||||
|
v-else-if="notification.type === 'pleroma:report'"
|
||||||
|
:report-id="notification.report.id"
|
||||||
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
class="faint"
|
class="faint"
|
||||||
|
@ -59,8 +59,10 @@
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
--link: var(--faintLink);
|
.faint {
|
||||||
--text: var(--faint);
|
--link: var(--faintLink);
|
||||||
|
--text: var(--faint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.follow-request-accept {
|
.follow-request-accept {
|
||||||
|
23
src/components/optional_router_link/optional_router_link.vue
Normal file
23
src/components/optional_router_link/optional_router_link.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<!-- eslint-disable vue/no-multiple-template-root -->
|
||||||
|
<router-link
|
||||||
|
v-if="to"
|
||||||
|
v-slot="props"
|
||||||
|
:to="to"
|
||||||
|
custom
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
v-bind="props"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
v-bind="{}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ['to']
|
||||||
|
}
|
||||||
|
</script>
|
@ -4,7 +4,7 @@ const Popover = {
|
|||||||
// Action to trigger popover: either 'hover' or 'click'
|
// Action to trigger popover: either 'hover' or 'click'
|
||||||
trigger: String,
|
trigger: String,
|
||||||
|
|
||||||
// Either 'top' or 'bottom'
|
// 'top', 'bottom', 'left', 'right'
|
||||||
placement: String,
|
placement: String,
|
||||||
|
|
||||||
// Takes object with properties 'x' and 'y', values of these can be
|
// Takes object with properties 'x' and 'y', values of these can be
|
||||||
@ -84,6 +84,8 @@ const Popover = {
|
|||||||
const anchorStyle = getComputedStyle(anchorEl)
|
const anchorStyle = getComputedStyle(anchorEl)
|
||||||
const topPadding = parseFloat(anchorStyle.paddingTop)
|
const topPadding = parseFloat(anchorStyle.paddingTop)
|
||||||
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
|
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
|
||||||
|
const rightPadding = parseFloat(anchorStyle.paddingRight)
|
||||||
|
const leftPadding = parseFloat(anchorStyle.paddingLeft)
|
||||||
|
|
||||||
// Screen position of the origin point for popover = center of the anchor
|
// Screen position of the origin point for popover = center of the anchor
|
||||||
const origin = {
|
const origin = {
|
||||||
@ -170,7 +172,7 @@ const Popover = {
|
|||||||
if (overlayCenter) {
|
if (overlayCenter) {
|
||||||
translateX = origin.x + horizOffset
|
translateX = origin.x + horizOffset
|
||||||
translateY = origin.y + vertOffset
|
translateY = origin.y + vertOffset
|
||||||
} else {
|
} else if (this.placement !== 'right' && this.placement !== 'left') {
|
||||||
// Default to whatever user wished with placement prop
|
// Default to whatever user wished with placement prop
|
||||||
let usingTop = this.placement !== 'bottom'
|
let usingTop = this.placement !== 'bottom'
|
||||||
|
|
||||||
@ -189,6 +191,25 @@ const Popover = {
|
|||||||
|
|
||||||
const xOffset = (this.offset && this.offset.x) || 0
|
const xOffset = (this.offset && this.offset.x) || 0
|
||||||
translateX = origin.x + horizOffset + xOffset
|
translateX = origin.x + horizOffset + xOffset
|
||||||
|
} else {
|
||||||
|
// Default to whatever user wished with placement prop
|
||||||
|
let usingRight = this.placement !== 'left'
|
||||||
|
|
||||||
|
// Handle special cases, first force to displaying on top if there's not space on bottom,
|
||||||
|
// regardless of what placement value was. Then check if there's not space on top, and
|
||||||
|
// force to bottom, again regardless of what placement value was.
|
||||||
|
const rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0)
|
||||||
|
const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0)
|
||||||
|
if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true
|
||||||
|
if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false
|
||||||
|
|
||||||
|
const xOffset = (this.offset && this.offset.x) || 0
|
||||||
|
translateX = usingRight
|
||||||
|
? rightBoundary - xOffset - content.offsetWidth
|
||||||
|
: leftBoundary + xOffset
|
||||||
|
|
||||||
|
const yOffset = (this.offset && this.offset.y) || 0
|
||||||
|
translateY = origin.y + vertOffset + yOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
this.styles = {
|
this.styles = {
|
||||||
|
@ -126,6 +126,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.-has-submenu {
|
||||||
|
.chevron-icon {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:active, &:hover {
|
&:active, &:hover {
|
||||||
background-color: $fallback--lightBg;
|
background-color: $fallback--lightBg;
|
||||||
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
background-color: var(--selectedMenuPopover, $fallback--lightBg);
|
||||||
|
@ -55,6 +55,14 @@ const pxStringToNumber = (str) => {
|
|||||||
|
|
||||||
const PostStatusForm = {
|
const PostStatusForm = {
|
||||||
props: [
|
props: [
|
||||||
|
'statusId',
|
||||||
|
'statusText',
|
||||||
|
'statusIsSensitive',
|
||||||
|
'statusPoll',
|
||||||
|
'statusFiles',
|
||||||
|
'statusMediaDescriptions',
|
||||||
|
'statusScope',
|
||||||
|
'statusContentType',
|
||||||
'replyTo',
|
'replyTo',
|
||||||
'repliedUser',
|
'repliedUser',
|
||||||
'attentions',
|
'attentions',
|
||||||
@ -62,6 +70,7 @@ const PostStatusForm = {
|
|||||||
'subject',
|
'subject',
|
||||||
'disableSubject',
|
'disableSubject',
|
||||||
'disableScopeSelector',
|
'disableScopeSelector',
|
||||||
|
'disableVisibilitySelector',
|
||||||
'disableNotice',
|
'disableNotice',
|
||||||
'disableLockWarning',
|
'disableLockWarning',
|
||||||
'disablePolls',
|
'disablePolls',
|
||||||
@ -125,22 +134,38 @@ const PostStatusForm = {
|
|||||||
|
|
||||||
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
|
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
|
||||||
|
|
||||||
|
let statusParams = {
|
||||||
|
spoilerText: this.subject || '',
|
||||||
|
status: statusText,
|
||||||
|
nsfw: !!sensitiveByDefault,
|
||||||
|
files: [],
|
||||||
|
poll: {},
|
||||||
|
mediaDescriptions: {},
|
||||||
|
visibility: scope,
|
||||||
|
contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusId) {
|
||||||
|
const statusContentType = this.statusContentType || contentType
|
||||||
|
statusParams = {
|
||||||
|
spoilerText: this.subject || '',
|
||||||
|
status: this.statusText || '',
|
||||||
|
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
||||||
|
files: this.statusFiles || [],
|
||||||
|
poll: this.statusPoll || {},
|
||||||
|
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||||
|
visibility: this.statusScope || scope,
|
||||||
|
contentType: statusContentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dropFiles: [],
|
dropFiles: [],
|
||||||
uploadingFiles: false,
|
uploadingFiles: false,
|
||||||
error: null,
|
error: null,
|
||||||
posting: false,
|
posting: false,
|
||||||
highlighted: 0,
|
highlighted: 0,
|
||||||
newStatus: {
|
newStatus: statusParams,
|
||||||
spoilerText: this.subject || '',
|
|
||||||
status: statusText,
|
|
||||||
nsfw: !!sensitiveByDefault,
|
|
||||||
files: [],
|
|
||||||
poll: {},
|
|
||||||
mediaDescriptions: {},
|
|
||||||
visibility: scope,
|
|
||||||
contentType
|
|
||||||
},
|
|
||||||
caret: 0,
|
caret: 0,
|
||||||
pollFormVisible: false,
|
pollFormVisible: false,
|
||||||
showDropIcon: 'hide',
|
showDropIcon: 'hide',
|
||||||
@ -164,7 +189,7 @@ const PostStatusForm = {
|
|||||||
emojiUserSuggestor () {
|
emojiUserSuggestor () {
|
||||||
return suggestor({
|
return suggestor({
|
||||||
emoji: [
|
emoji: [
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.getters.standardEmojiList,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
],
|
],
|
||||||
store: this.$store
|
store: this.$store
|
||||||
@ -173,13 +198,13 @@ const PostStatusForm = {
|
|||||||
emojiSuggestor () {
|
emojiSuggestor () {
|
||||||
return suggestor({
|
return suggestor({
|
||||||
emoji: [
|
emoji: [
|
||||||
...this.$store.state.instance.emoji,
|
...this.$store.getters.standardEmojiList,
|
||||||
...this.$store.state.instance.customEmoji
|
...this.$store.state.instance.customEmoji
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
emoji () {
|
emoji () {
|
||||||
return this.$store.state.instance.emoji || []
|
return this.$store.getters.standardEmojiList || []
|
||||||
},
|
},
|
||||||
customEmoji () {
|
customEmoji () {
|
||||||
return this.$store.state.instance.customEmoji || []
|
return this.$store.state.instance.customEmoji || []
|
||||||
@ -236,6 +261,9 @@ const PostStatusForm = {
|
|||||||
uploadFileLimitReached () {
|
uploadFileLimitReached () {
|
||||||
return this.newStatus.files.length >= this.fileLimit
|
return this.newStatus.files.length >= this.fileLimit
|
||||||
},
|
},
|
||||||
|
isEdit () {
|
||||||
|
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
mobileLayout: state => state.interface.mobileLayout
|
mobileLayout: state => state.interface.mobileLayout
|
||||||
|
@ -66,6 +66,13 @@
|
|||||||
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
||||||
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="isEdit"
|
||||||
|
class="visibility-notice edit-warning"
|
||||||
|
>
|
||||||
|
<p>{{ $t('post_status.edit_remote_warning') }}</p>
|
||||||
|
<p>{{ $t('post_status.edit_unsupported_warning') }}</p>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!disablePreview"
|
v-if="!disablePreview"
|
||||||
class="preview-heading faint"
|
class="preview-heading faint"
|
||||||
@ -170,6 +177,7 @@
|
|||||||
class="visibility-tray"
|
class="visibility-tray"
|
||||||
>
|
>
|
||||||
<scope-selector
|
<scope-selector
|
||||||
|
v-if="!disableVisibilitySelector"
|
||||||
:show-all="showAllScopes"
|
:show-all="showAllScopes"
|
||||||
:user-default="userDefaultScope"
|
:user-default="userDefaultScope"
|
||||||
:original-scope="copyMessageScope"
|
:original-scope="copyMessageScope"
|
||||||
@ -410,6 +418,16 @@
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-notice.edit-warning {
|
||||||
|
> :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.media-upload-icon, .poll-icon, .emoji-icon {
|
.media-upload-icon, .poll-icon, .emoji-icon {
|
||||||
font-size: 1.85em;
|
font-size: 1.85em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
@ -9,7 +9,10 @@ library.add(
|
|||||||
faWrench
|
faWrench
|
||||||
)
|
)
|
||||||
|
|
||||||
const TimelineQuickSettings = {
|
const QuickFilterSettings = {
|
||||||
|
props: {
|
||||||
|
conversation: Boolean
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
Popover
|
Popover
|
||||||
},
|
},
|
||||||
@ -64,4 +67,4 @@ const TimelineQuickSettings = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineQuickSettings
|
export default QuickFilterSettings
|
@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
class="TimelineQuickSettings"
|
class="QuickFilterSettings"
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<div v-if="loggedIn">
|
<div v-if="loggedIn">
|
||||||
<button
|
<button
|
||||||
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="replyVisibilityAll = true"
|
@click="replyVisibilityAll = true"
|
||||||
>
|
>
|
||||||
@ -17,6 +18,7 @@
|
|||||||
/>{{ $t('settings.reply_visibility_all') }}
|
/>{{ $t('settings.reply_visibility_all') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="replyVisibilityFollowing = true"
|
@click="replyVisibilityFollowing = true"
|
||||||
>
|
>
|
||||||
@ -26,6 +28,7 @@
|
|||||||
/>{{ $t('settings.reply_visibility_following_short') }}
|
/>{{ $t('settings.reply_visibility_following_short') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="replyVisibilitySelf = true"
|
@click="replyVisibilitySelf = true"
|
||||||
>
|
>
|
||||||
@ -35,6 +38,7 @@
|
|||||||
/>{{ $t('settings.reply_visibility_self_short') }}
|
/>{{ $t('settings.reply_visibility_self_short') }}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
|
v-if="!conversation"
|
||||||
role="separator"
|
role="separator"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
@ -70,13 +74,7 @@
|
|||||||
class="button-default dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click="openTab('filtering')"
|
@click="openTab('filtering')"
|
||||||
>
|
>
|
||||||
<FAIcon icon="font" />{{ $t('settings.word_filter') }}
|
<FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }}
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
|
||||||
@click="openTab('general')"
|
|
||||||
>
|
|
||||||
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -88,11 +86,11 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./timeline_quick_settings.js"></script>
|
<script src="./quick_filter_settings.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
.TimelineQuickSettings {
|
.QuickFilterSettings {
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
line-height: 100%;
|
line-height: 100%;
|
69
src/components/quick_view_settings/quick_view_settings.js
Normal file
69
src/components/quick_view_settings/quick_view_settings.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import Popover from '../popover/popover.vue'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faList, faFolderTree, faBars, faWrench } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faList,
|
||||||
|
faFolderTree,
|
||||||
|
faBars,
|
||||||
|
faWrench
|
||||||
|
)
|
||||||
|
|
||||||
|
const QuickViewSettings = {
|
||||||
|
props: {
|
||||||
|
conversation: Boolean
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Popover
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setConversationDisplay (visibility) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'conversationDisplay', value: visibility })
|
||||||
|
},
|
||||||
|
openTab (tab) {
|
||||||
|
this.$store.dispatch('openSettingsModalTab', tab)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['mergedConfig']),
|
||||||
|
loggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
conversationDisplay: {
|
||||||
|
get () { return this.mergedConfig.conversationDisplay },
|
||||||
|
set (newVal) { this.setConversationDisplay(newVal) }
|
||||||
|
},
|
||||||
|
autoUpdate: {
|
||||||
|
get () { return this.mergedConfig.streaming },
|
||||||
|
set () {
|
||||||
|
const value = !this.autoUpdate
|
||||||
|
this.$store.dispatch('setOption', { name: 'streaming', value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
collapseWithSubjects: {
|
||||||
|
get () { return this.mergedConfig.collapseMessageWithSubject },
|
||||||
|
set () {
|
||||||
|
const value = !this.collapseWithSubjects
|
||||||
|
this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showUserAvatars: {
|
||||||
|
get () { return this.mergedConfig.mentionLinkShowAvatar },
|
||||||
|
set () {
|
||||||
|
const value = !this.showUserAvatars
|
||||||
|
console.log(value)
|
||||||
|
this.$store.dispatch('setOption', { name: 'mentionLinkShowAvatar', value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
muteBotStatuses: {
|
||||||
|
get () { return this.mergedConfig.muteBotStatuses },
|
||||||
|
set () {
|
||||||
|
const value = !this.muteBotStatuses
|
||||||
|
this.$store.dispatch('setOption', { name: 'muteBotStatuses', value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuickViewSettings
|
94
src/components/quick_view_settings/quick_view_settings.vue
Normal file
94
src/components/quick_view_settings/quick_view_settings.vue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
class="QuickViewSettings"
|
||||||
|
:bound-to="{ x: 'container' }"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="conversationDisplay = 'tree'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
|
||||||
|
/><FAIcon icon="folder-tree" /> {{ $t('settings.conversation_display_tree_quick') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="conversationDisplay = 'linear'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
|
||||||
|
/><FAIcon icon="list" /> {{ $t('settings.conversation_display_linear_quick') }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
role="separator"
|
||||||
|
class="dropdown-divider"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="showUserAvatars = !showUserAvatars"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': showUserAvatars }"
|
||||||
|
/>{{ $t('settings.mention_link_show_avatar_quick') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!conversation"
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="autoUpdate = !autoUpdate"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': autoUpdate }"
|
||||||
|
/>{{ $t('settings.auto_update') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!conversation"
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="collapseWithSubjects = !collapseWithSubjects"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': collapseWithSubjects }"
|
||||||
|
/>{{ $t('settings.collapse_subject') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click="openTab('general')"
|
||||||
|
>
|
||||||
|
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #trigger>
|
||||||
|
<button class="button-unstyled">
|
||||||
|
<FAIcon icon="bars" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./quick_view_settings.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
.QuickViewSettings {
|
||||||
|
|
||||||
|
> button {
|
||||||
|
line-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: var(--__panel-heading-height-inner);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -1,15 +1,21 @@
|
|||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { trim } from 'lodash'
|
import { trim } from 'lodash'
|
||||||
|
|
||||||
library.add(faSmileBeam)
|
library.add(
|
||||||
|
faPlus,
|
||||||
|
faTimes,
|
||||||
|
faSmileBeam
|
||||||
|
)
|
||||||
|
|
||||||
const ReactButton = {
|
const ReactButton = {
|
||||||
props: ['status'],
|
props: ['status'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
filterWord: ''
|
filterWord: '',
|
||||||
|
expanded: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@ -25,6 +31,13 @@ const ReactButton = {
|
|||||||
}
|
}
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
|
onShow () {
|
||||||
|
this.expanded = true
|
||||||
|
this.focusInput()
|
||||||
|
},
|
||||||
|
onClose () {
|
||||||
|
this.expanded = false
|
||||||
|
},
|
||||||
focusInput () {
|
focusInput () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const input = this.$el.querySelector('input')
|
const input = this.$el.querySelector('input')
|
||||||
@ -46,7 +59,7 @@ const ReactButton = {
|
|||||||
if (this.filterWord !== '') {
|
if (this.filterWord !== '') {
|
||||||
const filterWordLowercase = trim(this.filterWord.toLowerCase())
|
const filterWordLowercase = trim(this.filterWord.toLowerCase())
|
||||||
const orderedEmojiList = []
|
const orderedEmojiList = []
|
||||||
for (const emoji of this.$store.state.instance.emoji) {
|
for (const emoji of this.$store.getters.standardEmojiList) {
|
||||||
if (emoji.replacement === this.filterWord) return [emoji]
|
if (emoji.replacement === this.filterWord) return [emoji]
|
||||||
|
|
||||||
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
|
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
|
||||||
@ -59,7 +72,7 @@ const ReactButton = {
|
|||||||
}
|
}
|
||||||
return orderedEmojiList.flat()
|
return orderedEmojiList.flat()
|
||||||
}
|
}
|
||||||
return this.$store.state.instance.emoji || []
|
return this.$store.getters.standardEmojiList || []
|
||||||
},
|
},
|
||||||
mergedConfig () {
|
mergedConfig () {
|
||||||
return this.$store.getters.mergedConfig
|
return this.$store.getters.mergedConfig
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
remove-padding
|
remove-padding
|
||||||
popover-class="ReactButton popover-default"
|
popover-class="ReactButton popover-default"
|
||||||
@show="focusInput"
|
@show="onShow"
|
||||||
|
@close="onClose"
|
||||||
>
|
>
|
||||||
<template #content="{close}">
|
<template #content="{close}">
|
||||||
<div class="reaction-picker-filter">
|
<div class="reaction-picker-filter">
|
||||||
@ -46,10 +47,24 @@
|
|||||||
class="button-unstyled popover-trigger"
|
class="button-unstyled popover-trigger"
|
||||||
:title="$t('tool_tip.add_reaction')"
|
:title="$t('tool_tip.add_reaction')"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FALayers>
|
||||||
class="fa-scale-110 fa-old-padding"
|
<FAIcon
|
||||||
:icon="['far', 'smile-beam']"
|
class="fa-scale-110 fa-old-padding"
|
||||||
/>
|
:icon="['far', 'smile-beam']"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-show="!expanded"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-17"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-show="expanded"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-17"
|
||||||
|
icon="times"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -59,6 +74,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
@import '../../_mixins.scss';
|
||||||
|
|
||||||
.ReactButton {
|
.ReactButton {
|
||||||
.reaction-picker-filter {
|
.reaction-picker-filter {
|
||||||
@ -125,6 +141,21 @@
|
|||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-trigger-button {
|
||||||
|
@include unfocused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include focused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
export default {
|
||||||
|
props: ['relationship'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
inProgress: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
label () {
|
||||||
|
if (this.inProgress) {
|
||||||
|
return this.$t('user_card.follow_progress')
|
||||||
|
} else {
|
||||||
|
return this.$t('user_card.remove_follower')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.inProgress = true
|
||||||
|
this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => {
|
||||||
|
this.inProgress = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="btn button-default follow-button"
|
||||||
|
:class="{ toggled: inProgress }"
|
||||||
|
:disabled="inProgress"
|
||||||
|
:title="$t('user_card.remove_follower')"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./remove_follower_button.js"></script>
|
@ -1,7 +1,15 @@
|
|||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faReply } from '@fortawesome/free-solid-svg-icons'
|
import {
|
||||||
|
faReply,
|
||||||
|
faPlus,
|
||||||
|
faTimes
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(faReply)
|
library.add(
|
||||||
|
faReply,
|
||||||
|
faPlus,
|
||||||
|
faTimes
|
||||||
|
)
|
||||||
|
|
||||||
const ReplyButton = {
|
const ReplyButton = {
|
||||||
name: 'ReplyButton',
|
name: 'ReplyButton',
|
||||||
|
@ -7,10 +7,24 @@
|
|||||||
:title="$t('tool_tip.reply')"
|
:title="$t('tool_tip.reply')"
|
||||||
@click.prevent="$emit('toggle')"
|
@click.prevent="$emit('toggle')"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FALayers class="fa-old-padding-layer">
|
||||||
class="fa-scale-110 fa-old-padding"
|
<FAIcon
|
||||||
icon="reply"
|
class="fa-scale-110"
|
||||||
/>
|
icon="reply"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="!replying"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-8 right-11"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-else
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-8 right-11"
|
||||||
|
icon="times"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</button>
|
</button>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
@ -32,6 +46,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
@import '../../_mixins.scss';
|
||||||
|
|
||||||
.ReplyButton {
|
.ReplyButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -52,6 +67,18 @@
|
|||||||
color: $fallback--cBlue;
|
color: $fallback--cBlue;
|
||||||
color: var(--cBlue, $fallback--cBlue);
|
color: var(--cBlue, $fallback--cBlue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include unfocused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include focused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
34
src/components/report/report.js
Normal file
34
src/components/report/report.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Select from '../select/select.vue'
|
||||||
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
|
import Timeago from '../timeago/timeago.vue'
|
||||||
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
|
const Report = {
|
||||||
|
props: [
|
||||||
|
'reportId'
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
Select,
|
||||||
|
StatusContent,
|
||||||
|
Timeago
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
report () {
|
||||||
|
return this.$store.state.reports.reports[this.reportId] || {}
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
get: function () { return this.report.state },
|
||||||
|
set: function (val) { this.setReportState(val) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
generateUserProfileLink (user) {
|
||||||
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
},
|
||||||
|
setReportState (state) {
|
||||||
|
return this.$store.dispatch('setReportState', { id: this.report.id, state })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Report
|
43
src/components/report/report.scss
Normal file
43
src/components/report/report.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.Report {
|
||||||
|
.report-content {
|
||||||
|
margin: 0.5em 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-state {
|
||||||
|
margin: 0.5em 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reported-status {
|
||||||
|
border: 1px solid $fallback--faint;
|
||||||
|
border-color: var(--faint, $fallback--faint);
|
||||||
|
border-radius: $fallback--inputRadius;
|
||||||
|
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
.status-content {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reported-status-heading {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reported-status-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
74
src/components/report/report.vue
Normal file
74
src/components/report/report.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="Report">
|
||||||
|
<div class="reported-user">
|
||||||
|
<span>{{ $t('report.reported_user') }}</span>
|
||||||
|
<router-link :to="generateUserProfileLink(report.acct)">
|
||||||
|
@{{ report.acct.screen_name }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="reporter">
|
||||||
|
<span>{{ $t('report.reporter') }}</span>
|
||||||
|
<router-link :to="generateUserProfileLink(report.actor)">
|
||||||
|
@{{ report.actor.screen_name }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="report-state">
|
||||||
|
<span>{{ $t('report.state') }}</span>
|
||||||
|
<Select
|
||||||
|
:id="report-state"
|
||||||
|
v-model="state"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="state in ['open', 'closed', 'resolved']"
|
||||||
|
:key="state"
|
||||||
|
:value="state"
|
||||||
|
>
|
||||||
|
{{ $t('report.state_' + state) }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<RichContent
|
||||||
|
class="report-content"
|
||||||
|
:html="report.content"
|
||||||
|
:emoji="[]"
|
||||||
|
/>
|
||||||
|
<div v-if="report.statuses.length">
|
||||||
|
<small>{{ $t('report.reported_statuses') }}</small>
|
||||||
|
<router-link
|
||||||
|
v-for="status in report.statuses"
|
||||||
|
:key="status.id"
|
||||||
|
:to="{ name: 'conversation', params: { id: status.id } }"
|
||||||
|
class="reported-status"
|
||||||
|
>
|
||||||
|
<div class="reported-status-heading">
|
||||||
|
<span class="reported-status-name">{{ status.user.name }}</span>
|
||||||
|
<Timeago
|
||||||
|
:time="status.created_at"
|
||||||
|
:auto-update="240"
|
||||||
|
class="faint"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<status-content :status="status" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-if="report.notes.length">
|
||||||
|
<small>{{ $t('report.notes') }}</small>
|
||||||
|
<div
|
||||||
|
v-for="note in report.notes"
|
||||||
|
:key="note.id"
|
||||||
|
class="note"
|
||||||
|
>
|
||||||
|
<span>{{ note.content }}</span>
|
||||||
|
<Timeago
|
||||||
|
:time="note.created_at"
|
||||||
|
:auto-update="240"
|
||||||
|
class="faint"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./report.js"></script>
|
||||||
|
<style src="./report.scss" lang="scss"></style>
|
@ -1,7 +1,17 @@
|
|||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faRetweet } from '@fortawesome/free-solid-svg-icons'
|
import {
|
||||||
|
faRetweet,
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
faCheck
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(faRetweet)
|
library.add(
|
||||||
|
faRetweet,
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
faCheck
|
||||||
|
)
|
||||||
|
|
||||||
const RetweetButton = {
|
const RetweetButton = {
|
||||||
props: ['status', 'loggedIn', 'visibility'],
|
props: ['status', 'loggedIn', 'visibility'],
|
||||||
|
@ -7,11 +7,31 @@
|
|||||||
:title="$t('tool_tip.repeat')"
|
:title="$t('tool_tip.repeat')"
|
||||||
@click.prevent="retweet()"
|
@click.prevent="retweet()"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FALayers class="fa-old-padding-layer">
|
||||||
class="fa-scale-110 fa-old-padding"
|
<FAIcon
|
||||||
icon="retweet"
|
class="fa-scale-110"
|
||||||
:spin="animated"
|
icon="retweet"
|
||||||
/>
|
:spin="animated"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="status.repeated"
|
||||||
|
class="active-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="check"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="!status.repeated"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-else
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="minus"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</button>
|
</button>
|
||||||
<span v-else-if="loggedIn">
|
<span v-else-if="loggedIn">
|
||||||
<FAIcon
|
<FAIcon
|
||||||
@ -40,6 +60,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
@import '../../_mixins.scss';
|
||||||
|
|
||||||
.RetweetButton {
|
.RetweetButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -64,6 +85,26 @@
|
|||||||
color: $fallback--cGreen;
|
color: $fallback--cGreen;
|
||||||
color: var(--cGreen, $fallback--cGreen);
|
color: var(--cGreen, $fallback--cGreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include unfocused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include focused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -47,6 +47,8 @@
|
|||||||
class="cancel-icon fa-scale-110 fa-old-padding"
|
class="cancel-icon fa-scale-110 fa-old-padding"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<span class="spacer" />
|
||||||
|
<span class="spacer" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -42,6 +42,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
update (e) {
|
update (e) {
|
||||||
set(this.$parent, this.path, e)
|
set(this.$parent, this.path, e)
|
||||||
|
},
|
||||||
|
reset () {
|
||||||
|
set(this.$parent, this.path, this.defaultState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,12 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox>
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
|
<ServerSideIndicator :server-side="isServerSide" />
|
||||||
|
</Checkbox>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -43,6 +43,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
update (e) {
|
update (e) {
|
||||||
set(this.$parent, this.path, e)
|
set(this.$parent, this.path, e)
|
||||||
|
},
|
||||||
|
reset () {
|
||||||
|
set(this.$parent, this.path, this.defaultState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,10 @@
|
|||||||
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
|
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
|
||||||
</option>
|
</option>
|
||||||
</Select>
|
</Select>
|
||||||
<ModifiedIndicator :changed="isChanged" />
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
<ServerSideIndicator :server-side="isServerSide" />
|
<ServerSideIndicator :server-side="isServerSide" />
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user