Merge a ton of changes
Do any of them work? probably not. Signed-off-by: Sam Therapy <sam@samtherapy.net>
This commit is contained in:
commit
e71bf66223
300 changed files with 12068 additions and 10887 deletions
2
.babelrc
2
.babelrc
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||
"comments": false
|
||||
"comments": true
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ steps:
|
|||
from_secret: SSH_KEY
|
||||
commands:
|
||||
- apt update && apt install -y wget dos2unix openssh-client rsync
|
||||
- wget https://f.ruina.exposed/add-froth-key.sh && dos2unix ./add-froth-key.sh && chmod +x add-froth-key.sh && bash ./add-froth-key.sh
|
||||
- wget https://f.ruina.exposed/pleroma-fe-build-froth.sh && dos2unix ./pleroma-fe-build-froth.sh && chmod +x pleroma-fe-build-froth.sh && bash ./pleroma-fe-build-froth.sh
|
||||
- ./ci/add-key.sh
|
||||
- ./ci/deploy.sh
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
parser: '@babel/eslint-parser',
|
||||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
|
@ -21,6 +21,7 @@ module.exports = {
|
|||
'generator-star-spacing': 0,
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'vue/require-prop-types': 0
|
||||
'vue/require-prop-types': 0,
|
||||
'vue/multi-word-component-names': 0
|
||||
}
|
||||
}
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -7,3 +7,6 @@ test/e2e/reports
|
|||
selenium-debug.log
|
||||
.idea/
|
||||
config/local.json
|
||||
static/emoji.json
|
||||
|
||||
.dccache
|
|
@ -1,7 +1,7 @@
|
|||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official framework image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/node/tags/
|
||||
image: node:12
|
||||
image: node:16
|
||||
|
||||
stages:
|
||||
- lint
|
||||
|
|
|
@ -1 +1 @@
|
|||
7.2.1
|
||||
16.16.0
|
||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -16,17 +16,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Attachments are ALWAYS in same order as user uploaded, no more "videos first"
|
||||
- Attachment description is prefilled with backend-provided default when uploading
|
||||
- Proper visual feedback that next image is loading when browsing
|
||||
- UI no longer lags when switching between mobile and desktop mode
|
||||
- Popovers no longer constrained by DOM hierarchy, shouldn't be cut off by anything
|
||||
- "Always show mobile button" is working now
|
||||
|
||||
### Changed
|
||||
- Using Vue 3 now
|
||||
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
|
||||
- User highlight background now also covers the `@`
|
||||
- Reverted back to textual `@`, svg version is opt-in.
|
||||
- Settings window has been throughly rearranged to make make more sense and make navication settings easier.
|
||||
- Settings window has been thoroughly rearranged to make more sense and make navigation settings easier.
|
||||
- Uploaded attachments are uniform with displayed attachments
|
||||
- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
|
||||
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
|
||||
- Slight width/spacing adjustments
|
||||
- More sizing stuff is font-size dependent now
|
||||
- Scrollbars are styled/colorized now
|
||||
- Scrollbars are toggleable (for stuff that didn't have visible scrollbars before) (opt-in)
|
||||
|
||||
### Added
|
||||
- 3 column mode: only enables when there's space for it (opt-out, customizable)
|
||||
- Options to show domains in mentions
|
||||
- Option to show user avatars in mention links (opt-in)
|
||||
- Option to disable the tooltip for mentions
|
||||
|
@ -37,6 +46,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
|
||||
- Ability to rearrange order of attachments when uploading
|
||||
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
||||
- Timelines/panels and conversations have sticky headers now
|
||||
- Added frontend ui for account migration
|
||||
|
||||
|
||||
## [2.4.2] - 2022-01-09
|
||||
|
|
|
@ -10,3 +10,5 @@ Contributors of this project.
|
|||
- shpuld (shpuld@shitposter.club): CSS and styling
|
||||
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
|
||||
- hj (hj@shigusegubu.club): Code
|
||||
- Sean King (seanking@freespeechextremist.com): Code
|
||||
- Tusooa Zhu (tusooa@kazv.moe): Code
|
||||
|
|
17
add-froth-key.sh
Normal file
17
add-froth-key.sh
Normal file
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
# only execute this script as part of the pipeline.
|
||||
[ -z "$CI" ] && echo "missing ci environment variable" && exit 2
|
||||
|
||||
# only execute the script when github token exists.
|
||||
[ -z "$SSH_KEY" ] && echo "missing ssh key" && exit 3
|
||||
|
||||
# write the ssh key.
|
||||
mkdir /root/.ssh
|
||||
echo -n "${SSH_KEY}" > /root/.ssh/id_ed25519
|
||||
chmod 600 /root/.ssh/id_ed25519
|
||||
|
||||
# add froth.zone to our known hosts.
|
||||
touch /root/.ssh/known_hosts
|
||||
chmod 600 /root/.ssh/known_hosts
|
||||
ssh-keyscan -H froth.zone > /etc/ssh/ssh_known_hosts 2> /dev/null
|
|
@ -18,6 +18,9 @@ console.log(
|
|||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
var updateEmoji = require('./update-emoji').updateEmoji
|
||||
updateEmoji()
|
||||
|
||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
|
|
|
@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
|
|||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
|
||||
var updateEmoji = require('./update-emoji').updateEmoji
|
||||
updateEmoji()
|
||||
|
||||
// default port where dev server listens for incoming traffic
|
||||
var port = process.env.PORT || config.dev.port
|
||||
// Define HTTP proxies to your custom API backend
|
||||
|
@ -28,18 +31,20 @@ var devMiddleware = require('webpack-dev-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' })
|
||||
// cb()
|
||||
})
|
||||
})
|
||||
// FIXME: The statement below gives error about hooks being required in webpack 5.
|
||||
// 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
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
|
@ -47,7 +52,7 @@ Object.keys(proxyTable).forEach(function (context) {
|
|||
if (typeof options === 'string') {
|
||||
options = { target: options }
|
||||
}
|
||||
app.use(proxyMiddleware(context, options))
|
||||
app.use(proxyMiddleware.createProxyMiddleware(context, options))
|
||||
})
|
||||
|
||||
// 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,8 +2,11 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||
var { VueLoaderPlugin } = require("vue-loader");
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin')
|
||||
var CopyPlugin = require('copy-webpack-plugin');
|
||||
var { VueLoaderPlugin } = require('vue-loader')
|
||||
var ESLintPlugin = require('eslint-webpack-plugin');
|
||||
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
|
@ -21,7 +24,8 @@ module.exports = {
|
|||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].js'
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
|
@ -29,7 +33,7 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.vue'],
|
||||
extensions: ['.mjs', '.js', '.jsx', '.vue'],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
|
@ -39,25 +43,15 @@ module.exports = {
|
|||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components'),
|
||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||
},
|
||||
fallback: {
|
||||
'querystring': require.resolve('querystring-es3'),
|
||||
'url': require.resolve('url/')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.(js|vue)$/,
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter'),
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
enforce: 'post',
|
||||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||
|
@ -89,24 +83,23 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
type: 'asset',
|
||||
generator: {
|
||||
filename: utils.assetsPath('img/[name].[hash:7][ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
type: 'asset',
|
||||
generator: {
|
||||
filename: utils.assetsPath('fonts/[name].[hash:7][ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
|
@ -114,6 +107,22 @@ module.exports = {
|
|||
entry: path.join(__dirname, '..', 'src/sw.js'),
|
||||
filename: 'sw-pleroma.js'
|
||||
}),
|
||||
new VueLoaderPlugin()
|
||||
new ESLintPlugin({
|
||||
extensions: ['js', 'vue'],
|
||||
formatter: require('eslint-formatter-friendly')
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "node_modules/@ruffle-rs/ruffle/**/*",
|
||||
to: "static/ruffle/[name][ext]"
|
||||
},
|
||||
],
|
||||
options: {
|
||||
concurrency: 100,
|
||||
},
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ module.exports = merge(baseWebpackConfig, {
|
|||
},
|
||||
mode: 'development',
|
||||
// eval-source-map is faster for development
|
||||
devtool: '#eval-source-map',
|
||||
devtool: 'eval-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
|
|
|
@ -5,6 +5,7 @@ var webpack = require('webpack')
|
|||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
|
@ -19,12 +20,16 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
devtool: config.build.productionSourceMap ? 'source-map' : false,
|
||||
optimization: {
|
||||
minimize: true,
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
}
|
||||
},
|
||||
minimizer: [
|
||||
`...`,
|
||||
new CssMinimizerPlugin()
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
|
@ -60,9 +65,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
ignoreCustomComments: [/server-generated-meta/]
|
||||
// more options:
|
||||
// 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
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
|
|
17
ci/add-key.sh
Executable file
17
ci/add-key.sh
Executable file
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
# only execute this script as part of the pipeline.
|
||||
[ -z "$CI" ] && echo "missing ci environment variable" && exit 2
|
||||
|
||||
# only execute the script when github token exists.
|
||||
[ -z "$SSH_KEY" ] && echo "missing ssh key" && exit 3
|
||||
|
||||
# write the ssh key.
|
||||
mkdir /root/.ssh
|
||||
echo -n "${SSH_KEY}" > /root/.ssh/id_ed25519
|
||||
chmod 600 /root/.ssh/id_ed25519
|
||||
|
||||
# add froth.zone to our known hosts.
|
||||
touch /root/.ssh/known_hosts
|
||||
chmod 600 /root/.ssh/known_hosts
|
||||
ssh-keyscan -H froth.zone > /etc/ssh/ssh_known_hosts 2> /dev/null
|
11
ci/deploy.sh
Executable file
11
ci/deploy.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
TARGET="pleroma@froth.zone:/opt/pleroma"
|
||||
|
||||
#rsync -ra public/ "${TARGET}/instance/static"
|
||||
#cp dist/index.html "${TARGET}/instance/static/index.html"
|
||||
rsync --update -Pr dist/ "${TARGET}/instance/static/"
|
||||
rsync --update -ra dist/static/ "${TARGET}/instance/static/static"
|
||||
#rsync --delete -ra images/ "${TARGET}/instance/static/images"
|
||||
#rsync --delete -ra sounds/ "${TARGET}/instance/static/sounds"
|
||||
rsync -ra instance/ "${TARGET}/instance/static/instance"
|
||||
#rsync --delete -ra pages/ "${TARGET}/instance/static/pages"
|
145
package.json
145
package.json
|
@ -16,111 +16,112 @@
|
|||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@babel/runtime": "7.18.9",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@vuelidate/core": "2.0.0-alpha.41",
|
||||
"@vuelidate/validators": "2.0.0-alpha.27",
|
||||
"body-scroll-lock": "2.7.1",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "^0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
|
||||
"@vuelidate/core": "2.0.0-alpha.44",
|
||||
"@vuelidate/validators": "2.0.0-alpha.31",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "1.5.12",
|
||||
"diff": "3.5.0",
|
||||
"escape-html": "1.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"js-cookie": "3.0.1",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "1.0.1",
|
||||
"lozad": "^1.16.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.6.2",
|
||||
"punycode.js": "2.1.0",
|
||||
"qrcode": "1",
|
||||
"ruffle-mirror": "2021.12.31",
|
||||
"vue": "^3.2.31",
|
||||
"vue-i18n": "^9.2.0-beta.34",
|
||||
"vue-router": "4.0.14",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"qrcode": "1.5.0",
|
||||
"querystring-es3": "0.2.1",
|
||||
"url": "0.11.0",
|
||||
"utf8": "3.0.0",
|
||||
"vue": "3.2.38",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.5",
|
||||
"vue-template-compiler": "2.7.10",
|
||||
"vuex": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/plugin-transform-runtime": "7.17.0",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/register": "7.17.7",
|
||||
"@intlify/vue-i18n-loader": "^5.0.0",
|
||||
"@babel/core": "7.18.13",
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@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",
|
||||
"@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/compiler-sfc": "^3.1.0",
|
||||
"@vue/test-utils": "2.0.0-rc.17",
|
||||
"autoprefixer": "6.7.7",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-loader": "8.2.4",
|
||||
"@vue/compiler-sfc": "3.2.38",
|
||||
"@vue/test-utils": "2.0.2",
|
||||
"autoprefixer": "10.4.8",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "3.5.0",
|
||||
"chai": "4.3.6",
|
||||
"chalk": "1.1.3",
|
||||
"chromedriver": "87.0.7",
|
||||
"connect-history-api-fallback": "1.6.0",
|
||||
"copy-webpack-plugin": "6.4.1",
|
||||
"cross-spawn": "4.0.2",
|
||||
"css-loader": "0.28.11",
|
||||
"chromedriver": "104.0.0",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"css-loader": "6.7.1",
|
||||
"css-minimizer-webpack-plugin": "4.0.0",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-standard": "12.0.0",
|
||||
"eslint-friendly-formatter": "2.0.7",
|
||||
"eslint-loader": "2.2.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-node": "7.0.1",
|
||||
"eslint-plugin-promise": "4.3.1",
|
||||
"eslint-plugin-standard": "4.1.0",
|
||||
"eslint-plugin-vue": "5.2.3",
|
||||
"eslint": "8.23.0",
|
||||
"eslint-config-standard": "17.0.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-n": "15.2.5",
|
||||
"eslint-plugin-promise": "6.0.1",
|
||||
"eslint-plugin-vue": "9.4.0",
|
||||
"eslint-webpack-plugin": "3.2.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.17.3",
|
||||
"file-loader": "3.0.1",
|
||||
"express": "4.18.1",
|
||||
"function-bind": "1.1.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"http-proxy-middleware": "0.21.0",
|
||||
"inject-loader": "2.0.1",
|
||||
"iso-639-1": "2.1.13",
|
||||
"isparta-loader": "2.0.0",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"iso-639-1": "2.1.15",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.3.17",
|
||||
"karma-coverage": "1.1.2",
|
||||
"karma-firefox-launcher": "1.3.0",
|
||||
"karma": "6.4.0",
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-firefox-launcher": "2.1.2",
|
||||
"karma-mocha": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-webpack": "4.0.2",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"lolex": "1.6.0",
|
||||
"mini-css-extract-plugin": "0.12.0",
|
||||
"mocha": "3.5.3",
|
||||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"mocha": "10.0.0",
|
||||
"nightwatch": "2.3.3",
|
||||
"opn": "5.5.0",
|
||||
"ora": "0.4.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "1.20.1",
|
||||
"sass-loader": "7.2.0",
|
||||
"postcss": "8.4.16",
|
||||
"postcss-loader": "7.0.1",
|
||||
"sass": "1.54.8",
|
||||
"sass-loader": "13.0.2",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "5.7.1",
|
||||
"serviceworker-webpack-plugin": "1.0.1",
|
||||
"semver": "7.3.7",
|
||||
"serviceworker-webpack5-plugin": "2.0.0",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
"sinon-chai": "2.14.0",
|
||||
"stylelint": "13.6.1",
|
||||
"sinon": "14.0.0",
|
||||
"sinon-chai": "3.7.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
"stylelint-rscss": "0.4.0",
|
||||
"url-loader": "1.1.2",
|
||||
"vue-loader": "^16.0.0",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"webpack": "4.46.0",
|
||||
"vue-loader": "17.0.0",
|
||||
"vue-style-loader": "4.1.3",
|
||||
"webpack": "5.74.0",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-hot-middleware": "2.24.3",
|
||||
"webpack-hot-middleware": "2.25.2",
|
||||
"webpack-merge": "0.20.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
20
src/App.js
20
src/App.js
|
@ -4,14 +4,15 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
|||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.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 StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
@ -32,9 +33,12 @@ export default {
|
|||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal,
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
|
@ -60,6 +64,13 @@ export default {
|
|||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
navClasses () {
|
||||
const { navbarColumnStretch } = this.$store.getters.mergedConfig
|
||||
return [
|
||||
'-' + this.layoutType,
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : [])
|
||||
]
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
|
@ -85,11 +96,16 @@ export default {
|
|||
isChats () {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
isListEdit () {
|
||||
return this.$route.name === 'lists-edit'
|
||||
},
|
||||
newPostButtonShown () {
|
||||
if (this.isChats) return false
|
||||
if (this.isListEdit) return false
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||
},
|
||||
|
|
67
src/App.scss
67
src/App.scss
|
@ -4,6 +4,13 @@
|
|||
:root {
|
||||
--navbar-height: 3.5rem;
|
||||
--post-line-height: 1.4;
|
||||
// Z-Index stuff
|
||||
--ZI_media_modal: 9000;
|
||||
--ZI_modals_popovers: 8500;
|
||||
--ZI_modals: 8000;
|
||||
--ZI_navbar_popovers: 7500;
|
||||
--ZI_navbar: 7000;
|
||||
--ZI_popovers: 6000;
|
||||
}
|
||||
|
||||
html {
|
||||
|
@ -110,14 +117,30 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.iconLetter {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
.svg-inline--fa,
|
||||
.iconLetter {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon);
|
||||
}
|
||||
|
||||
.button-unstyled:hover,
|
||||
a:hover {
|
||||
> i[class*=icon-],
|
||||
> .svg-inline--fa,
|
||||
> .iconLetter {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
z-index: 1000;
|
||||
z-index: var(--ZI_navbar);
|
||||
color: var(--topBarText);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--topBar, $fallback--fg);
|
||||
|
@ -134,6 +157,11 @@ nav {
|
|||
grid-area: sidebar;
|
||||
}
|
||||
|
||||
#modal {
|
||||
position: absolute;
|
||||
z-index: var(--ZI_modals);
|
||||
}
|
||||
|
||||
.column.-scrollable {
|
||||
top: var(--navbar-height);
|
||||
position: sticky;
|
||||
|
@ -175,13 +203,18 @@ nav {
|
|||
|
||||
.app-layout {
|
||||
--miniColumn: 25rem;
|
||||
--maxiColumn: minmax(var(--miniColumn), 45rem);
|
||||
--maxiColumn: 45rem;
|
||||
--columnGap: 1em;
|
||||
--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;
|
||||
display: grid;
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn);
|
||||
grid-template-columns:
|
||||
var(--effectiveSidebarColumnWidth)
|
||||
var(--effectiveContentColumnWidth);
|
||||
grid-template-areas: "sidebar content";
|
||||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
|
@ -275,15 +308,24 @@ nav {
|
|||
}
|
||||
|
||||
&.-reverse:not(.-wide):not(.-mobile) {
|
||||
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-columns:
|
||||
var(--effectiveContentColumnWidth)
|
||||
var(--effectiveSidebarColumnWidth);
|
||||
grid-template-areas: "content sidebar";
|
||||
}
|
||||
|
||||
&.-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";
|
||||
|
||||
&.-reverse {
|
||||
grid-template-columns:
|
||||
var(--effectiveNotifsColumnWidth)
|
||||
var(--effectiveContentColumnWidth)
|
||||
var(--effectiveSidebarColumnWidth);
|
||||
grid-template-areas: "notifs content sidebar";
|
||||
}
|
||||
}
|
||||
|
@ -310,7 +352,6 @@ nav {
|
|||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.underlay,
|
||||
#sidebar,
|
||||
#notifs-column {
|
||||
display: none;
|
||||
|
@ -740,17 +781,23 @@ option {
|
|||
}
|
||||
|
||||
.fa-scale-110 {
|
||||
&.svg-inline--fa {
|
||||
&.svg-inline--fa,
|
||||
&.iconLetter {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-old-padding {
|
||||
&.svg-inline--fa {
|
||||
&.iconLetter,
|
||||
&.svg-inline--fa, &-layer {
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.veryfaint {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
text-align: center;
|
||||
|
||||
|
@ -829,7 +876,7 @@ option {
|
|||
// Vue transitions
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
|
|
13
src/App.vue
13
src/App.vue
|
@ -8,7 +8,10 @@
|
|||
class="app-bg-wrapper"
|
||||
/>
|
||||
<MobileNav v-if="layoutType === 'mobile'" />
|
||||
<DesktopNav v-else />
|
||||
<DesktopNav
|
||||
v-else
|
||||
:class="navClasses"
|
||||
/>
|
||||
<Notifications v-if="currentUser" />
|
||||
<div
|
||||
id="content"
|
||||
|
@ -33,7 +36,7 @@
|
|||
<div
|
||||
id="main-scroller"
|
||||
class="column main"
|
||||
:class="{ '-full-height': isChats }"
|
||||
:class="{ '-full-height': isChats || isListEdit }"
|
||||
>
|
||||
<div
|
||||
v-if="!currentUser"
|
||||
|
@ -54,7 +57,7 @@
|
|||
:class="{ '-show-scrollbar': showScrollbars }"
|
||||
/>
|
||||
</div>
|
||||
<media-modal />
|
||||
<MediaModal />
|
||||
<shout-panel
|
||||
v-if="currentUser && shout && !hideShoutbox"
|
||||
:floating="true"
|
||||
|
@ -64,9 +67,13 @@
|
|||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<div id="modal" />
|
||||
<GlobalNoticeList />
|
||||
<div id="popovers" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
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 backendInteractorService from '../services/backend_interactor_service/backend_interactor_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 DomNodeToComponent from '../modules/domNodeToComponent.js'
|
||||
|
@ -158,7 +158,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
return store.dispatch('setTheme', config.theme)
|
||||
}
|
||||
|
||||
const getTOS = async ({ store }) => {
|
||||
|
@ -199,7 +199,7 @@ const getStickers = async ({ store }) => {
|
|||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
var meta = {}
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
|
@ -253,6 +253,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
|
||||
|
@ -321,6 +322,7 @@ const setConfig = async ({ store }) => {
|
|||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (store.getters.getUserToken()) {
|
||||
try {
|
||||
|
@ -361,6 +363,8 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
console.error('Failed to load any theme!')
|
||||
}
|
||||
|
||||
applyConfig(store.state.config)
|
||||
|
||||
// Now we can try getting the server settings and logging in
|
||||
// Most of these are preloaded into the index.html so blocking is minimized
|
||||
await Promise.all([
|
||||
|
@ -399,6 +403,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
app.component('FAIcon', FontAwesomeIcon)
|
||||
app.component('FALayers', FontAwesomeLayers)
|
||||
|
||||
// remove after vue 3.3
|
||||
app.config.unwrapInjectedRef = true
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
return app
|
||||
|
|
|
@ -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 About from 'components/about/about.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) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
|
@ -31,7 +35,8 @@ export default (store) => {
|
|||
}
|
||||
|
||||
let routes = [
|
||||
{ name: 'root',
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: _to => {
|
||||
return (store.state.users.currentUser
|
||||
|
@ -45,17 +50,19 @@ export default (store) => {
|
|||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'remote-user-profile-acct',
|
||||
{
|
||||
name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'remote-user-profile',
|
||||
{
|
||||
name: 'remote-user-profile',
|
||||
path: '/remote-users/:hostname/:username',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
|
@ -69,7 +76,13 @@ export default (store) => {
|
|||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: '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) {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js" ></script>
|
||||
<script src="./about.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.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 {
|
||||
faEllipsisV
|
||||
|
@ -19,7 +20,8 @@ const AccountActions = {
|
|||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover
|
||||
Popover,
|
||||
UserListMenu
|
||||
},
|
||||
methods: {
|
||||
showRepeats () {
|
||||
|
@ -34,6 +36,9 @@ const AccountActions = {
|
|||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
|
@ -28,6 +28,14 @@
|
|||
class="dropdown-divider"
|
||||
/>
|
||||
</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
|
||||
v-if="relationship.blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
|
@ -57,7 +65,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled ellipsis-button">
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
|
|
@ -127,6 +127,9 @@ const Attachment = {
|
|||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description' (newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription (newVal) {
|
||||
this.onEdit(newVal)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./avatar_list.js" ></script>
|
||||
<script src="./avatar_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserPopover from '../user_popover/user_popover.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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
@ -7,20 +8,13 @@ const BasicUserCard = {
|
|||
props: [
|
||||
'user'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
userExpanded: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserCard,
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent
|
||||
RichContent,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
<template>
|
||||
<div class="basic-user-card">
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<UserAvatar
|
||||
class="avatar"
|
||||
:user="user"
|
||||
@click.prevent="toggleUserExpanded"
|
||||
/>
|
||||
<router-link
|
||||
:to="userProfileLink(user)"
|
||||
@click.prevent
|
||||
>
|
||||
<UserPopover
|
||||
:user-id="user.id"
|
||||
:overlay-centers="true"
|
||||
overlay-centers-selector=".avatar"
|
||||
>
|
||||
<UserAvatar
|
||||
class="user-avatar avatar"
|
||||
:user="user"
|
||||
@click.prevent
|
||||
/>
|
||||
</UserPopover>
|
||||
</router-link>
|
||||
<div
|
||||
v-if="userExpanded"
|
||||
class="basic-user-card-expanded-content"
|
||||
>
|
||||
<UserCard
|
||||
:user-id="user.id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="basic-user-card-collapsed-content"
|
||||
>
|
||||
<div
|
||||
|
@ -32,12 +30,10 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
<user-link
|
||||
class="basic-user-card-screen-name"
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
@{{ user.screen_name_ui }}
|
||||
</router-link>
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -53,6 +49,8 @@
|
|||
margin: 0;
|
||||
padding: 0.6em 1em;
|
||||
|
||||
--emoji-size: 14px;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
text-align: left;
|
||||
|
|
|
@ -57,6 +57,7 @@ const Chat = {
|
|||
},
|
||||
unmounted () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
this.$store.dispatch('clearCurrentChat')
|
||||
},
|
||||
|
@ -107,7 +108,7 @@ const Chat = {
|
|||
}
|
||||
})
|
||||
},
|
||||
'$route': function () {
|
||||
$route: function () {
|
||||
this.startFetching()
|
||||
},
|
||||
mastoUserSocketStatus (newValue) {
|
||||
|
@ -135,7 +136,7 @@ const Chat = {
|
|||
},
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
handleResize (opts = {}) {
|
||||
const { expand = false, delayed = false } = opts
|
||||
const { delayed = false } = opts
|
||||
|
||||
if (delayed) {
|
||||
setTimeout(() => {
|
||||
|
@ -146,10 +147,10 @@ const Chat = {
|
|||
|
||||
this.$nextTick(() => {
|
||||
const { offsetHeight = undefined } = getScrollPosition()
|
||||
const diff = this.lastScrollPosition.offsetHeight - offsetHeight
|
||||
if (diff !== 0 || (!this.bottomedOut() && expand)) {
|
||||
const diff = offsetHeight - this.lastScrollPosition.offsetHeight
|
||||
if (diff !== 0 && !this.bottomedOut()) {
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({ top: window.scrollY + diff })
|
||||
window.scrollBy({ top: -Math.trunc(diff) })
|
||||
})
|
||||
}
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
|
@ -187,6 +188,7 @@ const Chat = {
|
|||
}, 5000)
|
||||
},
|
||||
handleScroll: _.throttle(function () {
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
if (!this.currentChat) { return }
|
||||
|
||||
if (this.reachedTop()) {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
class="timeline"
|
||||
>
|
||||
<List :items="sortedChatList">
|
||||
<template v-slot:item="{item}">
|
||||
<template #item="{item}">
|
||||
<ChatListItem
|
||||
:key="item.id"
|
||||
:compact="false"
|
||||
|
|
|
@ -6,7 +6,7 @@ import Gallery from '../gallery/gallery.vue'
|
|||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
|
@ -35,7 +35,8 @@ const ChatMessage = {
|
|||
UserAvatar,
|
||||
Gallery,
|
||||
LinkPreview,
|
||||
ChatMessageDate
|
||||
ChatMessageDate,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
computed: {
|
||||
// Returns HH:MM (hours and minutes) in local time.
|
||||
|
@ -49,9 +50,6 @@ const ChatMessage = {
|
|||
message () {
|
||||
return this.chatViewItem.data
|
||||
},
|
||||
userProfileLink () {
|
||||
return generateProfileLink(this.author.id, this.author.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
isMessage () {
|
||||
return this.chatViewItem.type === 'message'
|
||||
},
|
||||
|
|
|
@ -14,16 +14,16 @@
|
|||
v-if="!isCurrentUser"
|
||||
class="avatar-wrapper"
|
||||
>
|
||||
<router-link
|
||||
<UserPopover
|
||||
v-if="chatViewItem.isHead"
|
||||
:to="userProfileLink"
|
||||
:user-id="author.id"
|
||||
>
|
||||
<UserAvatar
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="author"
|
||||
/>
|
||||
</router-link>
|
||||
</UserPopover>
|
||||
</div>
|
||||
<div class="chat-message-inner">
|
||||
<div
|
||||
|
@ -44,13 +44,13 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
|
||||
bound-to-selector=".chat-view-inner"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:margin="popoverMarginStyle"
|
||||
@show="menuOpened = true"
|
||||
@close="menuOpened = false"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -60,7 +60,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="button-default menu-icon"
|
||||
:title="$t('chats.more')"
|
||||
|
@ -75,7 +75,7 @@
|
|||
:status="messageForStatusContent"
|
||||
:full-content="true"
|
||||
>
|
||||
<template v-slot:footer>
|
||||
<template #footer>
|
||||
<span
|
||||
class="created-at"
|
||||
>
|
||||
|
@ -96,7 +96,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./chat_message.js" ></script>
|
||||
<script src="./chat_message.js"></script>
|
||||
<style lang="scss">
|
||||
@import './chat_message.scss';
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'ChatTitle',
|
||||
components: {
|
||||
UserAvatar,
|
||||
RichContent
|
||||
RichContent,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
props: [
|
||||
'user', 'withAvatar'
|
||||
|
@ -18,10 +19,5 @@ export default {
|
|||
htmlTitle () {
|
||||
return this.user ? this.user.name_html : ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getUserProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
class="chat-title"
|
||||
:title="title"
|
||||
>
|
||||
<router-link
|
||||
<UserPopover
|
||||
v-if="withAvatar && user"
|
||||
class="avatar-container"
|
||||
:to="getUserProfileLink(user)"
|
||||
:user-id="user.id"
|
||||
>
|
||||
<UserAvatar
|
||||
class="titlebar-avatar"
|
||||
:user="user"
|
||||
/>
|
||||
</router-link>
|
||||
</UserPopover>
|
||||
<RichContent
|
||||
v-if="user"
|
||||
class="username"
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['update:modelValue'],
|
||||
props: [
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
]
|
||||
],
|
||||
emits: ['update:modelValue']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
|
@ -108,6 +107,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
|
||||
<style lang="scss">
|
||||
.color-control {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||
import Status from '../status/status.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 {
|
||||
|
@ -77,6 +81,9 @@ const conversation = {
|
|||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||
return maxDepth >= 1 ? maxDepth : 1
|
||||
},
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
},
|
||||
displayStyle () {
|
||||
return this.$store.getters.mergedConfig.conversationDisplay
|
||||
},
|
||||
|
@ -271,7 +278,7 @@ const conversation = {
|
|||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
name: `#${i}`,
|
||||
id: id
|
||||
id
|
||||
})
|
||||
}
|
||||
i++
|
||||
|
@ -339,11 +346,17 @@ const conversation = {
|
|||
},
|
||||
maybeHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
||||
})
|
||||
},
|
||||
components: {
|
||||
Status,
|
||||
ThreadTree
|
||||
ThreadTree,
|
||||
QuickFilterSettings,
|
||||
QuickViewSettings
|
||||
},
|
||||
watch: {
|
||||
statusId (newVal, oldVal) {
|
||||
|
@ -395,6 +408,11 @@ const conversation = {
|
|||
setHighlight (id) {
|
||||
if (!id) return
|
||||
this.highlight = id
|
||||
|
||||
if (!this.streamingEnabled) {
|
||||
this.$store.dispatch('fetchStatus', id)
|
||||
}
|
||||
|
||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
|
|
|
@ -17,6 +17,14 @@
|
|||
>
|
||||
{{ $t('timeline.collapse') }}
|
||||
</button>
|
||||
<QuickFilterSettings
|
||||
v-if="!collapsable"
|
||||
:conversation="true"
|
||||
/>
|
||||
<QuickViewSettings
|
||||
v-if="!collapsable"
|
||||
:conversation="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="conversation-body panel-body">
|
||||
<div
|
||||
|
|
|
@ -46,23 +46,27 @@ export default {
|
|||
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
||||
logoStyle () {
|
||||
return {
|
||||
'visibility': this.enableMask ? 'hidden' : 'visible'
|
||||
visibility: this.enableMask ? 'hidden' : 'visible'
|
||||
}
|
||||
},
|
||||
logoMaskStyle () {
|
||||
return this.enableMask ? {
|
||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||
} : {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
return this.enableMask
|
||||
? {
|
||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||
}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
},
|
||||
logoBgStyle () {
|
||||
return Object.assign({
|
||||
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
||||
margin: `${this.$store.state.instance.logoMargin} 0`,
|
||||
opacity: this.searchBarHidden ? 1 : 0
|
||||
}, this.enableMask ? {} : {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
})
|
||||
}, this.enableMask
|
||||
? {}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
})
|
||||
},
|
||||
logo () { return this.$store.state.instance.logo },
|
||||
sitename () { return this.$store.state.instance.name },
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
.DesktopNav {
|
||||
width: 100%;
|
||||
z-index: var(--ZI_navbar);
|
||||
|
||||
input {
|
||||
color: var(--inputTopbarText, var(--inputText));
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--topBarLink, $fallback--link);
|
||||
|
@ -18,6 +23,26 @@
|
|||
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 {
|
||||
grid-template-columns: auto 2fr 2fr;
|
||||
grid-template-areas: "logo sitename actions";
|
||||
|
@ -112,4 +137,8 @@
|
|||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
/>
|
||||
<button
|
||||
class="button-unstyled nav-icon"
|
||||
@click.stop="openSettingsModal"
|
||||
@click="openSettingsModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
@ -61,6 +61,7 @@
|
|||
:title="$t('nav.administration')"
|
||||
/>
|
||||
</a>
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-unstyled nav-icon"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
@ -19,7 +19,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
|
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 EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { take } from 'lodash'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSmileBeam
|
||||
|
@ -120,7 +121,8 @@ const EmojiInput = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker
|
||||
EmojiPicker,
|
||||
UnicodeDomainIndicator
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
|
@ -141,6 +143,51 @@ const EmojiInput = {
|
|||
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
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 () {
|
||||
|
@ -179,7 +226,7 @@ const EmojiInput = {
|
|||
const firstchar = newWord.charAt(0)
|
||||
this.suggestions = []
|
||||
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
|
||||
if (this.textAtCaret !== newWord) return
|
||||
if (matchedSuggestions.length <= 0) return
|
||||
|
@ -205,7 +252,6 @@ const EmojiInput = {
|
|||
},
|
||||
triggerShowPicker () {
|
||||
this.showPicker = true
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
this.$nextTick(() => {
|
||||
this.scrollIntoView()
|
||||
this.focusPickerInput()
|
||||
|
@ -321,7 +367,7 @@ const EmojiInput = {
|
|||
}
|
||||
},
|
||||
scrollIntoView () {
|
||||
const rootRef = this.$refs['picker'].$el
|
||||
const rootRef = this.$refs.picker.$el
|
||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||
* replies in notifs) or mobile post form. Note that getting and setting
|
||||
* scroll is different for `Window` and `Element`s
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
v-if="enableEmojiPicker"
|
||||
ref="picker"
|
||||
:class="{ hide: !showPicker }"
|
||||
:showing="showPicker"
|
||||
:enable-sticker-picker="enableStickerPicker"
|
||||
class="emoji-picker-panel"
|
||||
@emoji="insert"
|
||||
|
@ -50,7 +51,21 @@
|
|||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
* data: object providing source information for specific types of suggestions:
|
||||
* 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
|
||||
* updateUsersList - optional, a function to search and append to users
|
||||
*
|
||||
|
@ -13,10 +13,10 @@
|
|||
export default data => {
|
||||
const emojiCurry = suggestEmoji(data.emoji)
|
||||
const usersCurry = data.store && suggestUsers(data.store)
|
||||
return input => {
|
||||
return (input, nameKeywordLocalizer) => {
|
||||
const firstChar = input[0]
|
||||
if (firstChar === ':' && data.emoji) {
|
||||
return emojiCurry(input)
|
||||
return emojiCurry(input, nameKeywordLocalizer)
|
||||
}
|
||||
if (firstChar === '@' && usersCurry) {
|
||||
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)
|
||||
return emojis
|
||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
.map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||
.filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
|
||||
.map(k => {
|
||||
let score = 0
|
||||
|
||||
// An exact match always wins
|
||||
aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
|
||||
|
||||
// Prioritize custom emoji a lot
|
||||
aScore += a.imageUrl ? 100 : 0
|
||||
bScore += b.imageUrl ? 100 : 0
|
||||
score += k.imageUrl ? 100 : 0
|
||||
|
||||
// Prioritize prefix matches somewhat
|
||||
aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
|
||||
|
||||
// Sort by length
|
||||
aScore -= a.displayText.length
|
||||
bScore -= b.displayText.length
|
||||
score -= k.displayText.length
|
||||
|
||||
k.score = score
|
||||
return k
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Break ties alphabetically
|
||||
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
|
||||
/* eslint-disable camelcase */
|
||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
}).map((user) => ({
|
||||
user,
|
||||
displayText: user.screen_name_ui,
|
||||
detailText: user.name,
|
||||
imageUrl: user.profile_image_url_original,
|
||||
replacement: '@' + user.screen_name + ' '
|
||||
}))
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
|
|
@ -1,32 +1,76 @@
|
|||
import { defineAsyncComponent } from '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 {
|
||||
faBoxOpen,
|
||||
faStickyNote,
|
||||
faSmileBeam
|
||||
faSmileBeam,
|
||||
faSmile,
|
||||
faUser,
|
||||
faPaw,
|
||||
faIceCream,
|
||||
faBus,
|
||||
faBasketballBall,
|
||||
faLightbulb,
|
||||
faCode,
|
||||
faFlag
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { debounce, trim } from 'lodash'
|
||||
|
||||
library.add(
|
||||
faBoxOpen,
|
||||
faStickyNote,
|
||||
faSmileBeam
|
||||
faSmileBeam,
|
||||
faSmile,
|
||||
faUser,
|
||||
faPaw,
|
||||
faIceCream,
|
||||
faBus,
|
||||
faBasketballBall,
|
||||
faLightbulb,
|
||||
faCode,
|
||||
faFlag
|
||||
)
|
||||
|
||||
// At widest, approximately 20 emoji are visible in a row,
|
||||
// loading 3 rows, could be overkill for narrow picker
|
||||
const LOAD_EMOJI_BY = 60
|
||||
const UNICODE_EMOJI_GROUP_ICON = {
|
||||
'smileys-and-emotion': 'smile',
|
||||
'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 LOAD_EMOJI_MARGIN = 64
|
||||
const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
|
||||
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
|
||||
|
||||
const keywordLowercase = keyword.toLowerCase()
|
||||
let orderedEmojiList = []
|
||||
const orderedEmojiList = []
|
||||
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 (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
||||
orderedEmojiList[indexOfKeyword] = []
|
||||
|
@ -43,6 +87,10 @@ const EmojiPicker = {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showing: {
|
||||
required: true,
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
@ -52,16 +100,26 @@ const EmojiPicker = {
|
|||
showingStickers: false,
|
||||
groupsScrolledClass: 'scrolled-top',
|
||||
keepOpen: false,
|
||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
||||
customEmojiTimeout: null,
|
||||
customEmojiLoadAllConfirmed: false
|
||||
// Lazy-load only after the first time `showing` becomes true.
|
||||
contentLoaded: false,
|
||||
groupRefs: {},
|
||||
emojiRefs: {},
|
||||
filteredEmojiGroups: []
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
Checkbox
|
||||
Checkbox,
|
||||
StillImage
|
||||
},
|
||||
methods: {
|
||||
setGroupRef (name) {
|
||||
return el => { this.groupRefs[name] = el }
|
||||
},
|
||||
setEmojiRef (name) {
|
||||
return el => { this.emojiRefs[name] = el }
|
||||
},
|
||||
onStickerUploaded (e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
|
@ -76,10 +134,38 @@ const EmojiPicker = {
|
|||
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||
this.updateScrolledClass(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) {
|
||||
const ref = this.$refs['group-' + key]
|
||||
const ref = this.groupRefs['group-' + key]
|
||||
const top = ref.offsetTop
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = key
|
||||
|
@ -96,73 +182,90 @@ const EmojiPicker = {
|
|||
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 () {
|
||||
this.showingStickers = !this.showingStickers
|
||||
},
|
||||
setShowStickers (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: {
|
||||
keyword () {
|
||||
this.customEmojiLoadAllConfirmed = false
|
||||
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: {
|
||||
activeGroupView () {
|
||||
return this.showingStickers ? '' : this.activeGroup
|
||||
|
@ -173,39 +276,55 @@ const EmojiPicker = {
|
|||
}
|
||||
return 0
|
||||
},
|
||||
filteredEmoji () {
|
||||
return filterByKeyword(
|
||||
this.$store.state.instance.customEmoji || [],
|
||||
this.keyword
|
||||
)
|
||||
allCustomGroups () {
|
||||
return this.$store.getters.groupedCustomEmojis
|
||||
},
|
||||
customEmojiBuffer () {
|
||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
||||
defaultGroup () {
|
||||
return Object.keys(this.allCustomGroups)[0]
|
||||
},
|
||||
emojis () {
|
||||
const standardEmojis = this.$store.state.instance.emoji || []
|
||||
const customEmojis = this.customEmojiBuffer
|
||||
|
||||
return [
|
||||
{
|
||||
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, this.keyword)
|
||||
}
|
||||
]
|
||||
unicodeEmojiGroups () {
|
||||
return this.$store.getters.standardEmojiGroupList.map(group => ({
|
||||
id: `standard-${group.id}`,
|
||||
text: this.$t(`emoji.unicode_groups.${group.id}`),
|
||||
icon: UNICODE_EMOJI_GROUP_ICON[group.id],
|
||||
emojis: group.emojis
|
||||
}))
|
||||
},
|
||||
emojisView () {
|
||||
return this.emojis.filter(value => value.emojis.length > 0)
|
||||
allEmojiGroups () {
|
||||
return Object.entries(this.allCustomGroups)
|
||||
.map(([_, v]) => v)
|
||||
.concat(this.unicodeEmojiGroups)
|
||||
},
|
||||
stickerPickerEnabled () {
|
||||
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';
|
||||
|
||||
$emoji-picker-header-height: 36px;
|
||||
$emoji-picker-header-picture-width: 32px;
|
||||
$emoji-picker-header-picture-height: 32px;
|
||||
$emoji-picker-emoji-size: 32px;
|
||||
|
||||
.emoji-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -7,7 +12,8 @@
|
|||
right: 0;
|
||||
left: 0;
|
||||
margin: 0 !important;
|
||||
z-index: 100;
|
||||
// TODO: actually use popover in emoji picker
|
||||
z-index: var(--ZI_popovers);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--popover, $fallback--bg);
|
||||
color: $fallback--link;
|
||||
|
@ -18,6 +24,23 @@
|
|||
--lightText: var(--popoverLightText, $fallback--lightText);
|
||||
--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,
|
||||
.too-many-emoji {
|
||||
padding: 7px;
|
||||
|
@ -36,7 +59,6 @@
|
|||
|
||||
.heading {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: 10px 7px 5px;
|
||||
}
|
||||
|
||||
|
@ -49,6 +71,10 @@
|
|||
|
||||
.emoji-tabs {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.emoji-groups {
|
||||
|
@ -56,6 +82,8 @@
|
|||
}
|
||||
|
||||
.additional-tabs {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
border-left: 1px solid;
|
||||
border-left-color: $fallback--icon;
|
||||
border-left-color: var(--icon, $fallback--icon);
|
||||
|
@ -65,15 +93,20 @@
|
|||
|
||||
.additional-tabs,
|
||||
.emoji-tabs {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
flex-basis: auto;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
|
||||
&-item {
|
||||
padding: 0 7px;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
opacity: 0.5;
|
||||
|
@ -163,22 +196,26 @@
|
|||
}
|
||||
|
||||
&-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: $emoji-picker-emoji-size;
|
||||
height: $emoji-picker-emoji-size;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
font-size: 32px;
|
||||
line-height: $emoji-picker-emoji-size;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
.emoji-picker-emoji.-custom {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.emoji-picker-emoji.-unicode {
|
||||
font-size: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,19 +1,34 @@
|
|||
<template>
|
||||
<div class="emoji-picker panel panel-default panel-body">
|
||||
<div
|
||||
class="emoji-picker panel panel-default panel-body"
|
||||
>
|
||||
<div class="heading">
|
||||
<span class="emoji-tabs">
|
||||
<span
|
||||
ref="header"
|
||||
class="emoji-tabs"
|
||||
>
|
||||
<span
|
||||
v-for="group in emojis"
|
||||
v-for="group in filteredEmojiGroups"
|
||||
:ref="setGroupRef('group-header-' + group.id)"
|
||||
:key="group.id"
|
||||
class="emoji-tabs-item"
|
||||
:class="{
|
||||
active: activeGroupView === group.id,
|
||||
disabled: group.emojis.length === 0
|
||||
active: activeGroupView === group.id
|
||||
}"
|
||||
:title="group.text"
|
||||
@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
|
||||
v-else
|
||||
:icon="group.icon"
|
||||
fixed-width
|
||||
/>
|
||||
|
@ -36,7 +51,10 @@
|
|||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
v-if="contentLoaded"
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="emoji-content"
|
||||
:class="{hidden: showingStickers}"
|
||||
|
@ -47,6 +65,7 @@
|
|||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
@input="$event.target.composing = false"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
|
@ -56,12 +75,12 @@
|
|||
@scroll="onScroll"
|
||||
>
|
||||
<div
|
||||
v-for="group in emojisView"
|
||||
v-for="group in filteredEmojiGroups"
|
||||
:key="group.id"
|
||||
class="emoji-group"
|
||||
>
|
||||
<h6
|
||||
:ref="'group-' + group.id"
|
||||
:ref="setGroupRef('group-' + group.id)"
|
||||
class="emoji-group-title"
|
||||
>
|
||||
{{ group.text }}
|
||||
|
@ -69,17 +88,23 @@
|
|||
<span
|
||||
v-for="emoji in group.emojis"
|
||||
:key="group.id + emoji.displayText"
|
||||
:title="emoji.displayText"
|
||||
:title="maybeLocalizedEmojiName(emoji)"
|
||||
class="emoji-item"
|
||||
@click.stop.prevent="onEmoji(emoji)"
|
||||
>
|
||||
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
|
||||
<img
|
||||
<span
|
||||
v-if="!emoji.imageUrl"
|
||||
class="emoji-picker-emoji -unicode"
|
||||
>{{ emoji.replacement }}</span>
|
||||
<still-image
|
||||
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 :ref="'group-end-' + group.id" />
|
||||
<span :ref="setGroupRef('group-end-' + group.id)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="keep-open">
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_reactions.js" ></script>
|
||||
<script src="./emoji_reactions.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
|
|
|
@ -6,7 +6,10 @@ import {
|
|||
faEyeSlash,
|
||||
faThumbtack,
|
||||
faShareAlt,
|
||||
faExternalLinkAlt
|
||||
faExternalLinkAlt,
|
||||
faHistory,
|
||||
faPlus,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faBookmark as faBookmarkReg,
|
||||
|
@ -21,13 +24,27 @@ library.add(
|
|||
faThumbtack,
|
||||
faShareAlt,
|
||||
faExternalLinkAlt,
|
||||
faFlag
|
||||
faFlag,
|
||||
faHistory,
|
||||
faPlus,
|
||||
faTimes
|
||||
)
|
||||
|
||||
const ExtraButtons = {
|
||||
props: [ 'status' ],
|
||||
props: ['status'],
|
||||
components: { Popover },
|
||||
data () {
|
||||
return {
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onShow () {
|
||||
this.expanded = true
|
||||
},
|
||||
onClose () {
|
||||
this.expanded = false
|
||||
},
|
||||
deleteStatus () {
|
||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||
if (confirmed) {
|
||||
|
@ -71,6 +88,25 @@ const ExtraButtons = {
|
|||
},
|
||||
reportStatus () {
|
||||
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: {
|
||||
|
@ -89,9 +125,16 @@ const ExtraButtons = {
|
|||
canMute () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
canBookmark () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
statusLink () {
|
||||
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,8 +6,10 @@
|
|||
:offset="{ y: 5 }"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
@show="onShow"
|
||||
@close="onClose"
|
||||
>
|
||||
<template v-slot:content="{close}">
|
||||
<template #content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -51,27 +53,51 @@
|
|||
icon="thumbtack"
|
||||
/><span>{{ $t("status.unpin") }}</span>
|
||||
</button>
|
||||
<template v-if="canBookmark">
|
||||
<button
|
||||
v-if="!status.bookmarked"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="bookmarkStatus"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'bookmark']"
|
||||
/><span>{{ $t("status.bookmark") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.bookmarked"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unbookmarkStatus"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="bookmark"
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-if="!status.bookmarked"
|
||||
v-if="ownStatus && editingAvailable"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="bookmarkStatus"
|
||||
@click.prevent="editStatus"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'bookmark']"
|
||||
/><span>{{ $t("status.bookmark") }}</span>
|
||||
icon="pen"
|
||||
/><span>{{ $t("status.edit") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.bookmarked"
|
||||
v-if="isEdited && editingAvailable"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unbookmarkStatus"
|
||||
@click.prevent="showStatusHistory"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="bookmark"
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
icon="history"
|
||||
/><span>{{ $t("status.status_history") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
|
@ -118,21 +144,36 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<button class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</button>
|
||||
<template #trigger>
|
||||
<span class="button-unstyled popover-trigger">
|
||||
<FALayers class="fa-old-padding-layer">
|
||||
<FAIcon
|
||||
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>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./extra_buttons.js" ></script>
|
||||
<script src="./extra_buttons.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../_mixins.scss';
|
||||
|
||||
.ExtraButtons {
|
||||
/* override of popover internal stuff */
|
||||
|
@ -149,6 +190,21 @@
|
|||
color: $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>
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
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 {
|
||||
faStar as faStarRegular
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faStar,
|
||||
faStarRegular
|
||||
faStarRegular,
|
||||
faPlus,
|
||||
faMinus,
|
||||
faCheck
|
||||
)
|
||||
|
||||
const FavoriteButton = {
|
||||
|
|
|
@ -7,11 +7,31 @@
|
|||
:title="$t('tool_tip.favorite')"
|
||||
@click.prevent="favorite()"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
:icon="[status.favorited ? 'fas' : 'far', 'star']"
|
||||
:spin="animated"
|
||||
/>
|
||||
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||
<FAIcon
|
||||
class="fa-scale-110"
|
||||
: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>
|
||||
<span v-else>
|
||||
<FAIcon
|
||||
|
@ -29,10 +49,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./favorite_button.js" ></script>
|
||||
<script src="./favorite_button.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../_mixins.scss';
|
||||
|
||||
.FavoriteButton {
|
||||
display: flex;
|
||||
|
@ -57,6 +78,26 @@
|
|||
color: $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>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./features_panel.js" ></script>
|
||||
<script src="./features_panel.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.features-panel li {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import FollowButton from '../follow_button/follow_button.vue'
|
||||
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||
|
||||
const FollowCard = {
|
||||
props: [
|
||||
|
@ -10,7 +11,8 @@ const FollowCard = {
|
|||
components: {
|
||||
BasicUserCard,
|
||||
RemoteFollow,
|
||||
FollowButton
|
||||
FollowButton,
|
||||
RemoveFollowerButton
|
||||
},
|
||||
computed: {
|
||||
isMe () {
|
||||
|
|
|
@ -22,6 +22,11 @@
|
|||
class="follow-card-follow-button"
|
||||
:user="user"
|
||||
/>
|
||||
<RemoveFollowerButton
|
||||
v-if="noFollowsYou && relationship.followed_by"
|
||||
:relationship="relationship"
|
||||
class="follow-card-button"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</basic-user-card>
|
||||
|
@ -40,6 +45,12 @@
|
|||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
&-button {
|
||||
margin-top: 0.5em;
|
||||
padding: 0 1.5em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&-follow-button {
|
||||
margin-top: 0.5em;
|
||||
margin-left: auto;
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./font_control.js" ></script>
|
||||
<script src="./font_control.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -29,10 +29,10 @@
|
|||
|
||||
.global-notice-list {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
top: calc(var(--navbar-height) + 0.5em);
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1001;
|
||||
z-index: var(--ZI_navbar_popovers);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
@ -14,6 +14,6 @@
|
|||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./hashtag_link.js"/>
|
||||
<script src="./hashtag_link.js" />
|
||||
|
||||
<style lang="scss" src="./hashtag_link.scss"/>
|
||||
<style lang="scss" src="./hashtag_link.scss" />
|
||||
|
|
|
@ -95,7 +95,7 @@ const ImageCropper = {
|
|||
const fileInput = this.$refs.input
|
||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||
this.file = fileInput.files[0]
|
||||
let reader = new window.FileReader()
|
||||
const reader = new window.FileReader()
|
||||
reader.onload = (e) => {
|
||||
this.dataUrl = e.target.result
|
||||
this.$emit('open')
|
||||
|
|
|
@ -10,4 +10,4 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./instance_specific_panel.js" ></script>
|
||||
<script src="./instance_specific_panel.js"></script>
|
||||
|
|
|
@ -5,6 +5,8 @@ const tabModeDict = {
|
|||
mentions: ['mention'],
|
||||
'likes+repeats': ['repeat', 'like'],
|
||||
follows: ['follow'],
|
||||
reactions: ['pleroma:emoji_reaction'],
|
||||
reports: ['pleroma:report'],
|
||||
moves: ['move']
|
||||
}
|
||||
|
||||
|
@ -12,7 +14,8 @@ const Interactions = {
|
|||
data () {
|
||||
return {
|
||||
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: {
|
||||
|
|
|
@ -21,6 +21,15 @@
|
|||
key="follows"
|
||||
:label="$t('interactions.follows')"
|
||||
/>
|
||||
<span
|
||||
key="reactions"
|
||||
:label="$t('interactions.emoji_reactions')"
|
||||
/>
|
||||
<span
|
||||
v-if="canSeeReports"
|
||||
key="reports"
|
||||
:label="$t('interactions.reports')"
|
||||
/>
|
||||
<span
|
||||
v-if="!allowFollowingMove"
|
||||
key="moves"
|
||||
|
|
|
@ -25,6 +25,7 @@ import Select from '../select/select.vue'
|
|||
|
||||
export default {
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Select
|
||||
},
|
||||
props: {
|
||||
|
|
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>
|
|
@ -83,7 +83,7 @@ const LoginForm = {
|
|||
},
|
||||
clearError () { this.error = false },
|
||||
focusOnPasswordInput () {
|
||||
let passwordInput = this.$refs.passwordInput
|
||||
const passwordInput = this.$refs.passwordInput
|
||||
passwordInput.focus()
|
||||
passwordInput.setSelectionRange(0, passwordInput.value.length)
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./login_form.js" ></script>
|
||||
<script src="./login_form.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -114,7 +114,7 @@ $modal-view-button-icon-width: 3em;
|
|||
$modal-view-button-icon-margin: 0.5em;
|
||||
|
||||
.modal-view.media-modal-view {
|
||||
z-index: 9000;
|
||||
z-index: var(--ZI_media_modal);
|
||||
flex-direction: column;
|
||||
|
||||
.modal-view-button-arrow,
|
||||
|
|
|
@ -42,7 +42,8 @@ const mediaUpload = {
|
|||
.then((fileData) => {
|
||||
self.$emit('uploaded', fileData)
|
||||
self.decreaseUploadCount()
|
||||
}, (error) => { // eslint-disable-line handle-callback-err
|
||||
}, (error) => {
|
||||
console.error('Error uploading file', error)
|
||||
self.$emit('upload-failed', 'default')
|
||||
self.decreaseUploadCount()
|
||||
})
|
||||
|
@ -73,7 +74,7 @@ const mediaUpload = {
|
|||
'disabled'
|
||||
],
|
||||
watch: {
|
||||
'dropFiles': function (fileInfos) {
|
||||
dropFiles: function (fileInfos) {
|
||||
if (!this.uploading) {
|
||||
this.multiUpload(fileInfos)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</label>
|
||||
</template>
|
||||
|
||||
<script src="./media_upload.js" ></script>
|
||||
<script src="./media_upload.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -2,6 +2,8 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAt
|
||||
|
@ -14,7 +16,9 @@ library.add(
|
|||
const MentionLink = {
|
||||
name: 'MentionLink',
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
UnicodeDomainIndicator,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
|
@ -34,15 +38,30 @@ const MentionLink = {
|
|||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hasSelection: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
if (this.shouldShowTooltip) return
|
||||
const link = generateProfileLink(
|
||||
this.userId || this.user.id,
|
||||
this.userScreenName || this.user.screen_name
|
||||
)
|
||||
this.$router.push(link)
|
||||
},
|
||||
handleSelection () {
|
||||
this.hasSelection = document.getSelection().containsNode(this.$refs.full, true)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
document.addEventListener('selectionchange', this.handleSelection)
|
||||
},
|
||||
unmounted () {
|
||||
document.removeEventListener('selectionchange', this.handleSelection)
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
|
||||
|
@ -88,7 +107,8 @@ const MentionLink = {
|
|||
return [
|
||||
{
|
||||
'-you': this.isYou && this.shouldBoldenYou,
|
||||
'-highlighted': this.highlight
|
||||
'-highlighted': this.highlight,
|
||||
'-has-selection': this.hasSelection
|
||||
},
|
||||
this.highlightType
|
||||
]
|
||||
|
@ -110,7 +130,7 @@ const MentionLink = {
|
|||
}
|
||||
},
|
||||
shouldShowTooltip () {
|
||||
return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
|
||||
return this.mergedConfig.mentionLinkShowTooltip
|
||||
},
|
||||
shouldShowAvatar () {
|
||||
return this.mergedConfig.mentionLinkShowAvatar
|
||||
|
|
|
@ -55,11 +55,14 @@
|
|||
|
||||
.new {
|
||||
&.-you {
|
||||
& .shortName,
|
||||
& .full {
|
||||
.shortName {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
&.-has-selection {
|
||||
color: var(--alertNeutralText, $fallback--text);
|
||||
background-color: var(--alertNeutral, $fallback--fg);
|
||||
}
|
||||
|
||||
.at {
|
||||
color: var(--link);
|
||||
|
@ -72,8 +75,7 @@
|
|||
}
|
||||
|
||||
&.-striped {
|
||||
& .shortName,
|
||||
& .full {
|
||||
& .shortName {
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
135deg,
|
||||
|
@ -86,30 +88,29 @@
|
|||
}
|
||||
|
||||
&.-solid {
|
||||
& .shortName,
|
||||
& .full {
|
||||
.shortName {
|
||||
background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
|
||||
}
|
||||
}
|
||||
|
||||
&.-side {
|
||||
& .shortName,
|
||||
& .userNameFull {
|
||||
.shortName {
|
||||
box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .new .full {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
.full {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.serverName.-faded {
|
||||
color: var(--faintLink, $fallback--link);
|
||||
}
|
||||
|
||||
.full .-faded {
|
||||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
}
|
||||
|
||||
.mention-link-popover {
|
||||
max-width: 70ch;
|
||||
max-height: 20rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -9,69 +9,67 @@
|
|||
class="original"
|
||||
target="_blank"
|
||||
v-html="content"
|
||||
/><!-- eslint-enable vue/no-v-html --><span
|
||||
v-if="user"
|
||||
class="new"
|
||||
:style="style"
|
||||
:class="classnames"
|
||||
/><!-- eslint-enable vue/no-v-html -->
|
||||
<UserPopover
|
||||
v-else
|
||||
:user-id="user.id"
|
||||
:disabled="!shouldShowTooltip"
|
||||
>
|
||||
<a
|
||||
class="short button-unstyled"
|
||||
:class="{ '-with-tooltip': shouldShowTooltip }"
|
||||
:href="url"
|
||||
@click.prevent="onClick"
|
||||
<span
|
||||
v-if="user"
|
||||
class="new"
|
||||
:style="style"
|
||||
:class="classnames"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<UserAvatar
|
||||
v-if="shouldShowAvatar"
|
||||
class="mention-avatar"
|
||||
:user="user"
|
||||
/><span
|
||||
class="shortName"
|
||||
><FAIcon
|
||||
v-if="useAtIcon"
|
||||
size="sm"
|
||||
icon="at"
|
||||
class="at"
|
||||
/>{{ !useAtIcon ? '@' : '' }}<span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/><span
|
||||
v-if="shouldShowFullUserName"
|
||||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="isYou && shouldShowYous"
|
||||
:class="{ '-you': shouldBoldenYou }"
|
||||
> {{ ' ' + $t('status.you') }}</span>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</a><span
|
||||
v-if="shouldShowTooltip"
|
||||
class="full popover-default"
|
||||
:class="[highlightType]"
|
||||
>
|
||||
<span
|
||||
class="userNameFull"
|
||||
<a
|
||||
class="short button-unstyled"
|
||||
:class="{ '-with-tooltip': shouldShowTooltip }"
|
||||
:href="url"
|
||||
@click.prevent="onClick"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
@<span
|
||||
<UserAvatar
|
||||
v-if="shouldShowAvatar"
|
||||
class="mention-avatar"
|
||||
:user="user"
|
||||
/><span
|
||||
class="shortName"
|
||||
><FAIcon
|
||||
v-if="useAtIcon"
|
||||
size="sm"
|
||||
icon="at"
|
||||
class="at"
|
||||
/>{{ !useAtIcon ? '@' : '' }}<span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/><span
|
||||
v-if="shouldShowFullUserName"
|
||||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/><UnicodeDomainIndicator
|
||||
v-if="shouldShowFullUserName"
|
||||
:user="user"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="isYou && shouldShowYous"
|
||||
:class="{ '-you': shouldBoldenYou }"
|
||||
> {{ ' ' + $t('status.you') }}</span>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</a><span
|
||||
ref="full"
|
||||
class="full"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
@<span v-html="userName" /><span v-html="'@' + serverName" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</UserPopover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./mention_link.js"/>
|
||||
<script src="./mention_link.js" />
|
||||
|
||||
<style lang="scss" src="./mention_link.scss"/>
|
||||
<style lang="scss" src="./mention_link.scss" />
|
||||
|
|
|
@ -13,14 +13,13 @@
|
|||
<span
|
||||
v-if="expanded"
|
||||
class="fullExtraMentions"
|
||||
>
|
||||
<MentionLink
|
||||
v-for="mention in extraMentions"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
/>
|
||||
>{{ ' ' }}<MentionLink
|
||||
v-for="mention in extraMentions"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
/>
|
||||
</span><button
|
||||
v-if="!expanded"
|
||||
class="button-unstyled showMoreLess"
|
||||
|
@ -37,5 +36,5 @@
|
|||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<script src="./mentions_line.js" ></script>
|
||||
<script src="./mentions_line.js"></script>
|
||||
<style lang="scss" src="./mentions_line.scss" />
|
||||
|
|
|
@ -69,4 +69,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./recovery_form.js" ></script>
|
||||
<script src="./recovery_form.js"></script>
|
||||
|
|
|
@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue'
|
|||
import Notifications from '../notifications/notifications.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -19,7 +20,8 @@ library.add(
|
|||
const MobileNav = {
|
||||
components: {
|
||||
SideDrawer,
|
||||
Notifications
|
||||
Notifications,
|
||||
NavigationPins
|
||||
},
|
||||
data: () => ({
|
||||
notificationsCloseGesture: undefined,
|
||||
|
@ -47,7 +49,10 @@ const MobileNav = {
|
|||
isChat () {
|
||||
return this.$route.name === 'chat'
|
||||
},
|
||||
...mapGetters(['unreadChatCount'])
|
||||
...mapGetters(['unreadChatCount']),
|
||||
chatsPinned () {
|
||||
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMobileSidebar () {
|
||||
|
|
|
@ -17,20 +17,12 @@
|
|||
icon="bars"
|
||||
/>
|
||||
<div
|
||||
v-if="unreadChatCount"
|
||||
v-if="unreadChatCount && !chatsPinned"
|
||||
class="alert-dot"
|
||||
/>
|
||||
</button>
|
||||
<router-link
|
||||
v-if="!hideSitename"
|
||||
class="site-name"
|
||||
:to="{ name: 'root' }"
|
||||
active-class="home"
|
||||
>
|
||||
{{ sitename }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<NavigationPins class="pins" />
|
||||
</div> <div class="item right">
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-unstyled mobile-nav-button"
|
||||
|
@ -85,6 +77,8 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.MobileNav {
|
||||
z-index: var(--ZI_navbar);
|
||||
|
||||
.mobile-nav {
|
||||
display: grid;
|
||||
line-height: var(--navbar-height);
|
||||
|
@ -92,6 +86,7 @@
|
|||
grid-template-columns: 2fr auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
a {
|
||||
color: var(--topBarLink, $fallback--link);
|
||||
}
|
||||
|
@ -146,7 +141,7 @@
|
|||
transition-property: transform;
|
||||
transition-duration: 0.25s;
|
||||
transform: translateX(0);
|
||||
z-index: 1001;
|
||||
z-index: var(--ZI_navbar);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&.-closed {
|
||||
|
@ -159,7 +154,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
z-index: calc(var(--ZI_navbar) + 100);
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
|
@ -176,13 +171,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.pins {
|
||||
flex: 1;
|
||||
|
||||
.pinned-item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-notifications {
|
||||
margin-top: 50px;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
|
@ -192,14 +194,17 @@
|
|||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.panel:after {
|
||||
|
||||
.panel::after {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.panel .panel-heading {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
|
|
@ -10,7 +10,8 @@ library.add(
|
|||
|
||||
const HIDDEN_FOR_PAGES = new Set([
|
||||
'chats',
|
||||
'chat'
|
||||
'chat',
|
||||
'lists-edit'
|
||||
])
|
||||
|
||||
const MobilePostStatusButton = {
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
provide: {
|
||||
popoversZLayer: 'modals'
|
||||
},
|
||||
props: {
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
|
@ -26,7 +29,7 @@ export default {
|
|||
classes () {
|
||||
return {
|
||||
'modal-background': !this.noBackground,
|
||||
'open': this.isOpen
|
||||
open: this.isOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +38,7 @@ export default {
|
|||
|
||||
<style lang="scss">
|
||||
.modal-view {
|
||||
z-index: 2000;
|
||||
z-index: var(--ZI_modals);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
@show="setToggled(true)"
|
||||
@close="setToggled(false)"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<span v-if="user.is_local">
|
||||
<button
|
||||
|
@ -122,7 +122,7 @@
|
|||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="btn button-default btn-block moderation-tools-button"
|
||||
:class="{ toggled }"
|
||||
|
@ -137,11 +137,11 @@
|
|||
v-if="showDeleteUserDialog"
|
||||
:on-cancel="deleteUserDialog.bind(this, false)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<template #header>
|
||||
{{ $t('user_card.admin_menu.delete_user') }}
|
||||
</template>
|
||||
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
||||
<template v-slot:footer>
|
||||
<template #footer>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="deleteUserDialog(false)"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue