Compare commits

...

33 Commits

Author SHA1 Message Date
emma 13f92fa2b1 format missed files 2023-07-05 02:35:36 -04:00
emma 8e880c349e update ciu-lite 2023-07-05 02:34:03 -04:00
emma 3ca4c32b03 format 2023-07-05 02:33:10 -04:00
emma d25dd1cbd4 Merge remote-tracking branch 'upstream/develop' into develop 2023-07-05 02:29:15 -04:00
emma 3cee6c5934 more formatting 2023-02-15 00:12:10 +00:00
emma 5476a2794d fix missed conflicts 2023-02-15 00:03:00 +00:00
emma d8fa8c4ee4 Merge remote-tracking branch 'upstream/develop' into develop 2023-02-14 23:54:04 +00:00
emma fb317f2907 fix accidentally removed borders in timeline views 2022-12-14 02:39:20 +00:00
emma 153c4d251f update checked state as well 2022-12-14 02:29:40 +00:00
emma 1d01475f7a fix input and checkbox styling issue 2022-12-14 02:27:28 +00:00
emma a91e8d282d use svg as favicon 2022-12-14 02:18:20 +00:00
emma 1312b07e2e [wip] hide protocols in links from masto servers 2022-12-11 06:47:53 +00:00
emma 427e63cfc3 more minor padding, spacing, and link styling tweaks 2022-12-11 06:47:38 +00:00
emma 9b75ca414f Merge pull request 'Visual updates pt 1' (#1) from visual-updates into develop
Reviewed-on: emma/pleroma-fe#1
2022-12-08 19:48:44 +00:00
emma b07cf33a04 format (almost) everything remaining with prettier 2022-12-08 19:48:04 +00:00
emma 142f90c4cf add twitter theme preset 2022-12-08 19:36:27 +00:00
emma 83c6f7f9f9 use system mono in mfa backup code view 2022-12-08 19:25:12 +00:00
emma 65adfb01c3 add some nicer easings to favourite and rt button animations 2022-12-08 19:24:59 +00:00
emma 65511042e3 make left side of conversations clickable, à la birdsite 2022-12-08 19:24:22 +00:00
emma 235f3b2d94 allow popovers to escape the bounds of content 2022-12-08 19:23:46 +00:00
emma 2382696698 remove gradient mask on user card 2022-12-08 19:23:05 +00:00
emma ae2d72131b add consistent shadows to popover elements 2022-12-08 19:22:32 +00:00
emma 98d38e3b73 make emoji reaction buttons smaller 2022-12-08 19:21:35 +00:00
emma 47c05363f8 make tab switcher on profile look less messy with large border radii 2022-12-08 19:20:51 +00:00
emma 87d9c1ae15 fix various spacing issues 2022-12-08 19:20:09 +00:00
emma 5ad0da1766 update text color on nav to be less glaring 2022-12-08 19:18:13 +00:00
emma 97e9b2597a enable translucency on top bar & titles 2022-12-08 19:16:40 +00:00
emma 94bbf8f0a3 update unread notification overlay look 2022-12-08 19:12:58 +00:00
emma ce9d316a51 use system fonts by default 2022-12-08 19:11:38 +00:00
emma 6ce12fc153 update eslint & postcss 2022-12-08 19:08:18 +00:00
emma 6a2cdcfc15 remove custom scrollbars 2022-12-08 16:49:31 +00:00
emma d7688fafd3 format everything 2022-12-08 16:48:17 +00:00
emma 3d3425eda9 add prettier for formatting 2022-12-08 16:31:17 +00:00
425 changed files with 12067 additions and 8660 deletions

View File

@ -1,5 +1,9 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
"plugins": [
"@babel/plugin-transform-runtime",
"lodash",
"@vue/babel-plugin-jsx"
],
"comments": false
}

View File

@ -5,14 +5,9 @@ module.exports = {
sourceType: 'module'
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'plugin:vue/recommended'
],
extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
// required to lint *.vue files
plugins: [
'vue',
'import'
],
plugins: ['vue', 'import'],
// add your custom rules here
rules: {
// allow paren-less arrow functions

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "none",
"singleQuote": true,
"semi": false,
"singleAttributePerLine": true
}

View File

@ -11,14 +11,17 @@ var webpackConfig = require('./webpack.prod.conf')
console.log(
' Tip:\n' +
' Built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
' Built files are meant to be served over an HTTP server.\n' +
" Opening index.html over file:// won't work.\n"
)
var spinner = ora('building for production...')
spinner.start()
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
var assetsPath = path.join(
config.build.assetsRoot,
config.build.assetsSubDirectory
)
rm('-rf', assetsPath)
mkdir('-p', assetsPath)
cp('-R', 'static/*', assetsPath)
@ -26,11 +29,13 @@ cp('-R', 'static/*', assetsPath)
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n')
process.stdout.write(
stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n'
)
})

View File

@ -2,8 +2,7 @@ var semver = require('semver')
var chalk = require('chalk')
var packageConfig = require('../package.json')
var exec = function (cmd) {
return require('child_process')
.execSync(cmd).toString().trim()
return require('child_process').execSync(cmd).toString().trim()
}
var versionRequirements = [
@ -24,16 +23,23 @@ module.exports = function () {
for (var i = 0; i < versionRequirements.length; i++) {
var mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
warnings.push(
mod.name +
': ' +
chalk.red(mod.currentVersion) +
' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log(
chalk.yellow(
'To use this template, you must update following to modules:'
)
)
console.log()
for (var i = 0; i < warnings.length; i++) {
var warning = warnings[i]

View File

@ -6,9 +6,10 @@ var express = require('express')
var webpack = require('webpack')
var opn = require('opn')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
var webpackConfig =
process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
@ -50,7 +51,10 @@ app.use(devMiddleware)
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
var staticPath = path.posix.join(
config.dev.assetsPublicPath,
config.dev.assetsSubDirectory
)
app.use(staticPath, express.static('./static'))
module.exports = app.listen(port, function (err) {

View File

@ -4,7 +4,8 @@ var sass = require('sass')
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
var assetsSubDirectory =
process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
@ -13,7 +14,7 @@ exports.assetsPath = function (_path) {
exports.cssLoaders = function (options) {
options = options || {}
function generateLoaders (loaders) {
function generateLoaders(loaders) {
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
@ -27,11 +28,11 @@ exports.cssLoaders = function (options) {
return [
{
test: /\.(post)?css$/,
use: generateLoaders(['css-loader', 'postcss-loader']),
use: generateLoaders(['css-loader', 'postcss-loader'])
},
{
test: /\.less$/,
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']),
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader'])
},
{
test: /\.sass$/,
@ -52,8 +53,8 @@ exports.cssLoaders = function (options) {
},
{
test: /\.styl(us)?$/,
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
},
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader'])
}
]
}

View File

@ -7,8 +7,8 @@ var { VueLoaderPlugin } = require('vue-loader')
var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
// various preprocessor loaders added to vue-loader at the end of this file
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
var cssSourceMapDev = env === 'development' && config.dev.cssSourceMap
var cssSourceMapProd = env === 'production' && config.build.productionSourceMap
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
var now = Date.now()
@ -18,9 +18,12 @@ module.exports = {
app: './src/main.js'
},
output: {
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
hashFunction: 'sha256', // Workaround for builds with OpenSSL 3.
path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
publicPath:
process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath,
filename: '[name].js'
},
optimization: {
@ -30,17 +33,15 @@ module.exports = {
},
resolve: {
extensions: ['.js', '.jsx', '.vue', '.mjs'],
modules: [
path.join(__dirname, '../node_modules')
],
modules: [path.join(__dirname, '../node_modules')],
fallback: {
"url": require.resolve("url/"),
url: require.resolve('url/')
},
alias: {
'static': path.resolve(__dirname, '../static'),
'src': path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../src/assets'),
'components': path.resolve(__dirname, '../src/components'),
static: path.resolve(__dirname, '../static'),
src: path.resolve(__dirname, '../src'),
assets: path.resolve(__dirname, '../src/assets'),
components: path.resolve(__dirname, '../src/components'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
}
},
@ -66,14 +67,15 @@ module.exports = {
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
type: 'javascript/auto',
loader: '@intlify/vue-i18n-loader',
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
include: [
// Use `Rule.include` to specify the files of locale messages to be pre-compiled
path.resolve(__dirname, '../src/i18n')
]
},
{
test: /\.mjs$/,
include: /node_modules/,
type: "javascript/auto"
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto'
},
{
test: /\.vue$/,
@ -114,10 +116,8 @@ module.exports = {
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
},
}
]
},
plugins: [
new VueLoaderPlugin()
]
plugins: [new VueLoaderPlugin()]
}

View File

@ -7,7 +7,9 @@ var HtmlWebpackPlugin = require('html-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(
baseWebpackConfig.entry[name]
)
})
module.exports = merge(baseWebpackConfig, {
@ -20,10 +22,10 @@ module.exports = merge(baseWebpackConfig, {
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env,
'COMMIT_HASH': JSON.stringify('DEV'),
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
'__VUE_OPTIONS_API__': true,
'__VUE_PROD_DEVTOOLS__': false
COMMIT_HASH: JSON.stringify('DEV'),
DEV_OVERRIDES: JSON.stringify(config.dev.settings),
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),

View File

@ -2,23 +2,27 @@ var path = require('path')
var config = require('../config')
var utils = require('./utils')
var webpack = require('webpack')
const WorkboxPlugin = require('workbox-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin')
var { merge } = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var env = process.env.NODE_ENV === 'testing'
var env =
process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: config.build.env
let commitHash = require('child_process')
.execSync('git rev-parse --short HEAD')
.toString();
.execSync('git rev-parse --short HEAD')
.toString()
var webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
rules: utils.styleLoaders({
sourceMap: config.dev.cssSourceMap,
extract: true
})
},
devtool: 'source-map',
optimization: {
@ -36,15 +40,15 @@ var webpackConfig = merge(baseWebpackConfig, {
new WorkboxPlugin.InjectManifest({
swSrc: path.join(__dirname, '..', 'src/sw.js'),
swDest: 'sw-pleroma.js',
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024
}),
// http://vuejs.github.io/vue-loader/workflow/production.html
new webpack.DefinePlugin({
'process.env': env,
'COMMIT_HASH': JSON.stringify(commitHash),
'DEV_OVERRIDES': JSON.stringify(undefined),
'__VUE_OPTIONS_API__': true,
'__VUE_PROD_DEVTOOLS__': false
COMMIT_HASH: JSON.stringify(commitHash),
DEV_OVERRIDES: JSON.stringify(undefined),
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}),
// extract css into its own file
new MiniCssExtractPlugin({
@ -54,9 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, {
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
filename:
process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index,
template: 'index.html',
inject: true,
minify: {
@ -69,7 +72,7 @@ var webpackConfig = merge(baseWebpackConfig, {
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'auto'
}),
})
// split vendor js into its own file
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
@ -87,9 +90,7 @@ if (config.build.productionGzip) {
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
'\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
),
threshold: 10240,
minRatio: 0.8

View File

@ -38,7 +38,7 @@ module.exports = {
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/manifest.json': {
'/manifest.json': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
@ -59,7 +59,7 @@ module.exports = {
cookieDomainRewrite: 'localhost',
ws: true,
headers: {
'Origin': target
Origin: target
}
},
'/oauth/revoke': {
@ -76,7 +76,7 @@ module.exports = {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
}
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README

View File

@ -1,22 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<title>Akkoma</title>
<link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json">
</head>
<body class="hidden">
<noscript>To use Akkoma, please enable JavaScript.</noscript>
<div id="app"></div>
<div id="modal"></div>
<!-- built files will be auto injected -->
</body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no" />
<title>Akkoma</title>
<link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.svg" />
<link rel="manifest" href="/manifest.json" />
</head>
<body class="hidden">
<noscript>To use Akkoma, please enable JavaScript.</noscript>
<div id="app"></div>
<div id="modal"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -58,7 +58,7 @@
"@vue/babel-plugin-jsx": "1.1.1",
"@vue/compiler-sfc": "^3.1.0",
"@vue/test-utils": "^2.0.2",
"autoprefixer": "6.7.7",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.0",
"babel-plugin-lodash": "3.3.4",
"chai": "^4.3.7",
@ -69,11 +69,13 @@
"css-loader": "^6.7.2",
"custom-event-polyfill": "^1.0.7",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^9.7.0",
@ -102,9 +104,11 @@
"nightwatch": "0.9.21",
"opn": "4.0.2",
"ora": "0.4.1",
"postcss": "^8.4.19",
"postcss-html": "^1.5.0",
"postcss-loader": "3.0.0",
"postcss-loader": "^7.0.2",
"postcss-sass": "^0.5.0",
"prettier": "2.8.1",
"raw-loader": "0.5.1",
"sass": "^1.56.0",
"sass-loader": "^13.2.0",

View File

@ -1,5 +1,3 @@
module.exports = {
plugins: [
require('autoprefixer')
]
plugins: [require('autoprefixer')]
}

View File

@ -1,6 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
"extends": ["config:base"]
}

View File

@ -24,7 +24,9 @@ export default {
components: {
UserPanel,
NavPanel,
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
Notifications: defineAsyncComponent(() =>
import('./components/notifications/notifications.vue')
),
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
@ -44,17 +46,20 @@ export default {
data: () => ({
mobileActivePanel: 'timeline'
}),
created () {
created() {
// Load the locale from the storage
const val = this.$store.getters.mergedConfig.interfaceLanguage
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
this.$store.dispatch('setOption', {
name: 'interfaceLanguage',
value: val
})
window.addEventListener('resize', this.updateMobileState)
},
unmounted () {
unmounted() {
window.removeEventListener('resize', this.updateMobileState)
},
computed: {
classes () {
classes() {
return [
{
'-reverse': this.reverseLayout,
@ -64,48 +69,76 @@ export default {
'-' + this.layoutType
]
},
currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image },
instanceBackground () {
currentUser() {
return this.$store.state.users.currentUser
},
userBackground() {
return this.currentUser.background_image
},
instanceBackground() {
return this.mergedConfig.hideInstanceWallpaper
? null
: this.$store.state.instance.background
},
background () { return this.userBackground || this.instanceBackground },
bgStyle () {
background() {
return this.userBackground || this.instanceBackground
},
bgStyle() {
if (this.background) {
return {
'--body-background-image': `url(${this.background})`
}
}
},
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
suggestionsEnabled() {
return this.$store.state.instance.suggestionsEnabled
},
showInstanceSpecificPanel() {
return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
)
},
newPostButtonShown () {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
newPostButtonShown() {
return (
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
this.layoutType === 'mobile'
)
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
layoutType () { return this.$store.state.interface.layoutType },
privateMode () { return this.$store.state.instance.private },
reverseLayout () {
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
editingAvailable() {
return this.$store.state.instance.editingAvailable
},
layoutType() {
return this.$store.state.interface.layoutType
},
privateMode() {
return this.$store.state.instance.private
},
reverseLayout() {
const { thirdColumnMode, sidebarRight: reverseSetting } =
this.$store.getters.mergedConfig
if (this.layoutType !== 'wide') {
return reverseSetting
} else {
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
return thirdColumnMode === 'notifications'
? reverseSetting
: !reverseSetting
}
},
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
noSticky() {
return this.$store.getters.mergedConfig.disableStickyHeaders
},
showScrollbars() {
return this.$store.getters.mergedConfig.showScrollbars
},
...mapGetters(['mergedConfig'])
},
methods: {
updateMobileState () {
updateMobileState() {
this.$store.dispatch('setLayoutWidth', windowWidth())
this.$store.dispatch('setLayoutHeight', windowHeight())
}

View File

@ -13,8 +13,8 @@ html {
}
body {
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
font-family: $system-sans-serif;
font-family: var(--interfaceFont, $system-sans-serif);
margin: 0;
color: $fallback--text;
color: var(--text, $fallback--text);
@ -23,84 +23,13 @@ body {
overscroll-behavior-y: none;
overflow-x: clip;
overflow-y: scroll;
background: var(--bg);
&.hidden {
display: none;
}
}
// ## Custom scrollbars
// Only show custom scrollbars on devices which
// have a cursor/pointer to operate them
@media (any-pointer: fine) {
* {
scrollbar-color: var(--btn) transparent;
&::-webkit-scrollbar {
background: transparent;
}
&::-webkit-scrollbar-button,
&::-webkit-scrollbar-thumb {
background-color: var(--btn);
box-shadow: var(--buttonShadow);
border-radius: var(--btnRadius);
}
// horizontal/vertical/increment/decrement are webkit-specific stuff
// that indicates whether we're affecting vertical scrollbar, increase button etc
// stylelint-disable selector-pseudo-class-no-unknown
&::-webkit-scrollbar-button {
--___bgPadding: 2px;
color: var(--btnText);
background-repeat: no-repeat, no-repeat;
&:horizontal {
background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
&:increment {
background-image:
linear-gradient(45deg, var(--btnText) 50%, transparent 51%),
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);
background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
}
&:decrement {
background-image:
linear-gradient(45deg, transparent 50%, var(--btnText) 51%),
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);
background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
}
}
&:vertical {
background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
&:increment {
background-image:
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),
linear-gradient(45deg, transparent 50%, var(--btnText) 51%);
background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
}
&:decrement {
background-image:
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),
linear-gradient(45deg, var(--btnText) 50%, transparent 51%);
background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
}
}
}
// stylelint-enable selector-pseudo-class-no-unknown
}
// Body should have background to scrollbar otherwise it will use white (body color?)
html {
scrollbar-color: var(--selectedMenu) var(--wallpaper);
background: var(--wallpaper);
}
}
a {
text-decoration: none;
color: $fallback--link;
@ -111,7 +40,7 @@ h4 {
margin: 0;
}
i[class*=icon-],
i[class*='icon-'],
.svg-inline--fa {
color: $fallback--icon;
color: var(--icon, $fallback--icon);
@ -129,6 +58,7 @@ nav {
box-sizing: border-box;
height: var(--navbar-height);
position: fixed;
backdrop-filter: blur(12px) saturate(1.2);
}
#sidebar {
@ -183,7 +113,7 @@ nav {
position: relative;
display: grid;
grid-template-columns: var(--miniColumn) var(--maxiColumn);
grid-template-areas: "sidebar content";
grid-template-areas: 'sidebar content';
grid-template-rows: 1fr;
box-sizing: border-box;
margin: 0 auto;
@ -192,6 +122,7 @@ nav {
justify-content: center;
min-height: 100vh;
overflow-x: clip;
padding: 0 calc(var(--columnGap) / 2);
.column {
--___columnMargin: var(--columnGap);
@ -229,7 +160,9 @@ nav {
overflow-y: auto;
overflow-x: hidden;
margin-left: calc(var(--___paddingIncrease) * -1);
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
padding-left: calc(
var(--___paddingIncrease) + var(--___columnMargin) / 2
);
// On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
// might implement old style of hiding scrollbars later if there's demand
@ -237,7 +170,9 @@ nav {
&:not(.-show-scrollbar) {
scrollbar-width: none;
margin-right: calc(var(--___paddingIncrease) * -1);
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
padding-right: calc(
var(--___paddingIncrease) + var(--___columnMargin) / 2
);
&::-webkit-scrollbar {
display: block;
@ -277,21 +212,21 @@ nav {
&.-reverse:not(.-wide):not(.-mobile) {
grid-template-columns: var(--maxiColumn) var(--miniColumn);
grid-template-areas: "content sidebar";
grid-template-areas: 'content sidebar';
}
&.-wide {
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
grid-template-areas: "sidebar content notifs";
grid-template-areas: 'sidebar content notifs';
&.-reverse {
grid-template-areas: "notifs content sidebar";
grid-template-areas: 'notifs content sidebar';
}
}
&.-mobile {
grid-template-columns: 100vw;
grid-template-areas: "content";
grid-template-areas: 'content';
padding: 0;
.column {
@ -348,7 +283,7 @@ nav {
background: transparent;
}
i[class*=icon-],
i[class*='icon-'],
.svg-inline--fa {
color: $fallback--text;
color: var(--btnText, $fallback--text);
@ -364,7 +299,9 @@ nav {
}
&:active {
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
@ -397,7 +334,9 @@ nav {
color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggled, $fallback--fg);
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
svg,
@ -462,7 +401,8 @@ textarea,
border: none;
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
box-shadow: var(--inputShadow);
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
@ -480,13 +420,13 @@ textarea,
padding: 0 var(--_padding);
&:disabled,
&[disabled=disabled],
&[disabled='disabled'],
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
&[type=range] {
&[type='range'] {
background: none;
border: none;
margin: 0;
@ -494,12 +434,13 @@ textarea,
flex: 1;
}
&[type=radio] {
&[type='radio'] {
display: none;
&:checked + label::before {
box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset,
0 0 0 4px var(--fg, $fallback--fg) inset;
background-color: var(--accent, $fallback--link);
}
@ -520,7 +461,7 @@ textarea,
height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle
box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow);
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
margin-right: 0.5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
@ -534,7 +475,7 @@ textarea,
}
}
&[type=checkbox] {
&[type='checkbox'] {
display: none;
&:checked + label::before {
@ -560,7 +501,7 @@ textarea,
border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0 0 2px black inset;
box-shadow: var(--inputShadow);
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
margin-right: 0.5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
@ -595,8 +536,8 @@ option {
.hide-number-spinner {
-moz-appearance: textfield;
&[type=number]::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button {
&[type='number']::-webkit-inner-spin-button,
&[type='number']::-webkit-outer-spin-button {
opacity: 0;
display: none;
}

View File

@ -43,7 +43,7 @@
:to="{ name: 'login' }"
class="panel-body"
>
{{ $t("login.hint") }}
{{ $t('login.hint') }}
</router-link>
</div>
<router-view />

View File

@ -4,7 +4,7 @@ $darkened-background: whitesmoke;
$fallback--bg: #121a24;
$fallback--fg: #182230;
$fallback--faint: rgba(185, 185, 186, .5);
$fallback--faint: rgba(185, 185, 186, 0.5);
$fallback--text: #b9b9ba;
$fallback--link: #d8a070;
$fallback--icon: #666;
@ -16,8 +16,8 @@ $fallback--cBlue: #0095ff;
$fallback--cGreen: #0fa00f;
$fallback--cOrange: orange;
$fallback--alertError: rgba(211,16,20,.5);
$fallback--alertWarning: rgba(111,111,20,.5);
$fallback--alertError: rgba(211, 16, 20, 0.5);
$fallback--alertWarning: rgba(111, 111, 20, 0.5);
$fallback--panelRadius: 10px;
$fallback--checkboxRadius: 2px;
@ -28,6 +28,14 @@ $fallback--avatarRadius: 4px;
$fallback--avatarAltRadius: 10px;
$fallback--attachmentRadius: 10px;
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1),
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
$status-margin: 0.75em;
$system-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir,
segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
sans-serif;
$system-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
monospace;

View File

@ -3,15 +3,21 @@ import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core';
import {
FontAwesomeIcon,
FontAwesomeLayers
} from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core'
config.autoAddCss = false
import App from '../App.vue'
import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import {
windowWidth,
windowHeight
} from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
@ -25,7 +31,9 @@ const parsedInitialResults = () => {
return null
}
if (!staticInitialResults) {
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
staticInitialResults = JSON.parse(
document.getElementById('initial-results').textContent
)
}
return staticInitialResults
}
@ -73,18 +81,30 @@ const getInstanceConfig = async ({ store }) => {
const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
store.dispatch('setInstanceOption', {
name: 'textlimit',
value: textlimit
})
store.dispatch('setInstanceOption', {
name: 'accountApprovalRequired',
value: data.approval_required
})
// don't override cookie if set
if (!Cookies.get('userLanguage')) {
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
store.dispatch('setOption', {
name: 'interfaceLanguage',
value: resolveLanguage(data.languages)
})
}
if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
store.dispatch('setInstanceOption', {
name: 'vapidPublicKey',
value: vapidPublicKey
})
}
} else {
throw (res)
throw res
}
} catch (error) {
console.error('Could not load instance config, potentially fatal')
@ -99,10 +119,12 @@ const getBackendProvidedConfig = async ({ store }) => {
const data = await res.json()
return data.pleroma_fe
} else {
throw (res)
throw res
}
} catch (error) {
console.error('Could not load backend-provided frontend config, potentially fatal')
console.error(
'Could not load backend-provided frontend config, potentially fatal'
)
console.error(error)
}
}
@ -113,7 +135,7 @@ const getStaticConfig = async () => {
if (res.ok) {
return res.json()
} else {
throw (res)
throw res
}
} catch (error) {
console.warn('Failed to load static/config.json, continuing without it.')
@ -156,16 +178,12 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
store.dispatch('setInstanceOption', {
name: 'logoMask',
value: typeof config.logoMask === 'undefined'
? true
: config.logoMask
value: typeof config.logoMask === 'undefined' ? true : config.logoMask
})
store.dispatch('setInstanceOption', {
name: 'logoMargin',
value: typeof config.logoMargin === 'undefined'
? 0
: config.logoMargin
value: typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin
})
copyInstanceOption('logoLeft')
store.commit('authFlow/setInitialStrategy', config.loginMethod)
@ -193,7 +211,7 @@ const getTOS = async ({ store }) => {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else {
throw (res)
throw res
}
} catch (e) {
console.warn("Can't load TOS")
@ -206,9 +224,12 @@ const getInstancePanel = async ({ store }) => {
const res = await preloadFetch('/instance/panel.html')
if (res.ok) {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
store.dispatch('setInstanceOption', {
name: 'instanceSpecificPanelContent',
value: html
})
} else {
throw (res)
throw res
}
} catch (e) {
console.warn("Can't load instance panel")
@ -221,25 +242,30 @@ const getStickers = async ({ store }) => {
const res = await window.fetch('/static/stickers.json')
if (res.ok) {
const values = await res.json()
const stickers = (await Promise.all(
Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json')
var meta = {}
if (resPack.ok) {
meta = await resPack.json()
}
return {
pack: name,
path,
meta
}
})
)).sort((a, b) => {
const stickers = (
await Promise.all(
Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json')
var meta = {}
if (resPack.ok) {
meta = await resPack.json()
}
return {
pack: name,
path,
meta
}
})
)
).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title)
})
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
store.dispatch('setInstanceOption', {
name: 'stickers',
value: stickers
})
} else {
throw (res)
throw res
}
} catch (e) {
console.warn("Can't load stickers")
@ -254,13 +280,19 @@ const getAppSecret = async ({ store }) => {
.then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => {
commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
commit(
'setBackendInteractor',
backendInteractorService(store.getters.getToken())
)
})
}
const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop())
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
const nicknames = accounts.map((uri) => uri.split('/').pop())
store.dispatch('setInstanceOption', {
name: 'staffAccounts',
value: nicknames
})
}
const getNodeInfo = async ({ store }) => {
@ -270,68 +302,146 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json()
const metadata = data.metadata
const features = metadata.features
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
store.dispatch('setInstanceOption', {
name: 'name',
value: metadata.nodeName
})
store.dispatch('setInstanceOption', {
name: 'registrationOpen',
value: data.openRegistrations
})
store.dispatch('setInstanceOption', {
name: 'mediaProxyAvailable',
value: features.includes('media_proxy')
})
store.dispatch('setInstanceOption', {
name: 'safeDM',
value: features.includes('safe_dm_mentions')
})
store.dispatch('setInstanceOption', {
name: 'pollsAvailable',
value: features.includes('polls')
})
store.dispatch('setInstanceOption', {
name: 'editingAvailable',
value: features.includes('editing')
})
store.dispatch('setInstanceOption', {
name: 'pollLimits',
value: metadata.pollLimits
})
store.dispatch('setInstanceOption', {
name: 'mailerEnabled',
value: metadata.mailerEnabled
})
store.dispatch('setInstanceOption', {
name: 'translationEnabled',
value: features.includes('akkoma:machine_translation')
})
const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
store.dispatch('setInstanceOption', {
name: 'uploadlimit',
value: parseInt(uploadLimits.general)
})
store.dispatch('setInstanceOption', {
name: 'avatarlimit',
value: parseInt(uploadLimits.avatar)
})
store.dispatch('setInstanceOption', {
name: 'backgroundlimit',
value: parseInt(uploadLimits.background)
})
store.dispatch('setInstanceOption', {
name: 'bannerlimit',
value: parseInt(uploadLimits.banner)
})
store.dispatch('setInstanceOption', {
name: 'fieldsLimits',
value: metadata.fieldsLimits
})
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
store.dispatch('setInstanceOption', {
name: 'restrictedNicknames',
value: metadata.restrictedNicknames
})
store.dispatch('setInstanceOption', {
name: 'postFormats',
value: metadata.postFormats
})
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
store.dispatch('setInstanceOption', {
name: 'suggestionsEnabled',
value: suggestions.enabled
})
store.dispatch('setInstanceOption', {
name: 'suggestionsWeb',
value: suggestions.web
})
const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
store.dispatch('setInstanceOption', {
name: 'backendVersion',
value: software.version
})
store.dispatch('setInstanceOption', {
name: 'pleromaBackend',
value: software.name === 'pleroma'
})
const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', {
name: 'frontendVersion',
value: frontendVersion
})
const federation = metadata.federation
store.dispatch('setInstanceOption', {
name: 'tagPolicyAvailable',
value: typeof federation.mrf_policies === 'undefined'
? false
: metadata.federation.mrf_policies.includes('TagPolicy')
value:
typeof federation.mrf_policies === 'undefined'
? false
: metadata.federation.mrf_policies.includes('TagPolicy')
})
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
store.dispatch('setInstanceOption', {
name: 'federationPolicy',
value: federation
})
store.dispatch('setInstanceOption', {
name: 'localBubbleInstances',
value: metadata.localBubbleInstances
})
store.dispatch('setInstanceOption', {
name: 'federating',
value: typeof federation.enabled === 'undefined'
? true
: federation.enabled
value:
typeof federation.enabled === 'undefined' ? true : federation.enabled
})
store.dispatch('setInstanceOption', { name: 'publicTimelineVisibility', value: metadata.publicTimelineVisibility })
store.dispatch('setInstanceOption', { name: 'federatedTimelineAvailable', value: metadata.federatedTimelineAvailable })
store.dispatch('setInstanceOption', {
name: 'publicTimelineVisibility',
value: metadata.publicTimelineVisibility
})
store.dispatch('setInstanceOption', {
name: 'federatedTimelineAvailable',
value: metadata.federatedTimelineAvailable
})
const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
store.dispatch('setInstanceOption', {
name: 'accountActivationRequired',
value: accountActivationRequired
})
const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts })
} else {
throw (res)
throw res
}
} catch (e) {
console.warn('Could not load nodeinfo')
@ -341,11 +451,16 @@ const getNodeInfo = async ({ store }) => {
const setConfig = async ({ store }) => {
// apiConfig, staticConfig
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
const configInfos = await Promise.all([
getBackendProvidedConfig({ store }),
getStaticConfig()
])
const apiConfig = configInfos[0]
const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
await setSettings({ store, apiConfig, staticConfig }).then(
getAppSecret({ store })
)
}
const checkOAuthToken = async ({ store }) => {
@ -368,7 +483,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
FaviconService.initFaviconService()
const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
const server =
typeof overrides.target !== 'undefined'
? overrides.target
: window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store })
@ -378,7 +496,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
const customThemePresent = customThemeSource || customTheme
if (customThemePresent) {
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
if (
customThemeSource &&
customThemeSource.themeEngineVersion === CURRENT_VERSION
) {
applyTheme(customThemeSource)
} else {
applyTheme(customTheme)
@ -406,7 +527,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
history: createWebHistory(),
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
if (to.matched.some((m) => m.meta.dontScroll)) {
return {}
}

View File

@ -35,51 +35,145 @@ export default (store) => {
}
let routes = [
{ name: 'root',
{
name: 'root',
path: '/',
redirect: _to => {
return (store.state.users.currentUser
? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all'
redirect: (_to) => {
return (
(store.state.users.currentUser
? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all'
)
}
},
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{
name: 'public-external-timeline',
path: '/main/all',
component: PublicAndExternalTimeline
},
{
name: 'public-timeline',
path: '/main/public',
component: PublicTimeline
},
{
name: 'bubble-timeline',
path: '/main/bubble',
component: BubbleTimeline
},
{
name: 'friends',
path: '/main/friends',
component: FriendsTimeline,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'remote-user-profile-acct',
{
name: 'conversation',
path: '/notice/:id',
component: ConversationPage,
meta: { dontScroll: true }
},
{
name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
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, meta: { dontScroll: true } },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{
name: 'external-user-profile',
path: '/users/:id',
component: UserProfile,
meta: { dontScroll: true }
},
{
name: 'interactions',
path: '/users/:username/interactions',
component: Interactions,
beforeEnter: validateAuthenticatedRoute
},
{
name: 'dms',
path: '/users/:username/dms',
component: DMs,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent },
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
{
name: 'registration-request-sent',
path: '/registration-request-sent',
component: RegistrationRequestSent
},
{
name: 'awaiting-email-confirmation',
path: '/awaiting-email-confirmation',
component: AwaitingEmailConfirmation
},
{
name: 'password-reset',
path: '/password-reset',
component: PasswordReset,
props: true
},
{
name: 'registration-token',
path: '/registration/:token',
component: Registration
},
{
name: 'friend-requests',
path: '/friend-requests',
component: FollowRequests,
beforeEnter: validateAuthenticatedRoute
},
{
name: 'notifications',
path: '/:username/notifications',
component: Notifications,
props: () => ({ disableTeleport: true }),
beforeEnter: validateAuthenticatedRoute
},
{ name: 'login', path: '/login', component: AuthForm },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
{
name: 'oauth-callback',
path: '/oauth-callback',
component: OAuthCallback,
props: (route) => ({ code: route.query.code })
},
{
name: 'search',
path: '/search',
component: Search,
props: (route) => ({ query: route.query.query })
},
{
name: 'who-to-follow',
path: '/who-to-follow',
component: WhoToFollow,
beforeEnter: validateAuthenticatedRoute
},
{ name: 'about', path: '/about', component: About },
{ name: 'lists', path: '/lists', component: Lists },
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } }
{
name: 'announcements',
path: '/announcements',
component: AnnouncementsPage
},
{
name: 'user-profile',
path: '/:_(users)?/:name',
component: UserProfile,
meta: { dontScroll: true }
}
]
return routes

View File

@ -15,13 +15,17 @@ const About = {
LocalBubblePanel
},
computed: {
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
showInstanceSpecificPanel() {
return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
)
},
showLocalBubblePanel () {
showLocalBubblePanel() {
return this.$store.state.instance.localBubbleInstances.length > 0
}
}

View File

@ -9,7 +9,6 @@
</div>
</template>
<script src="./about.js" ></script>
<script src="./about.js"></script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@ -3,19 +3,13 @@ import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { mapState } from 'vuex'
import {
faEllipsisV
} from '@fortawesome/free-solid-svg-icons'
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
library.add(
faEllipsisV
)
library.add(faEllipsisV)
const AccountActions = {
props: [
'user', 'relationship'
],
data () {
props: ['user', 'relationship'],
data() {
return {
showingConfirmBlock: false
}
@ -26,56 +20,59 @@ const AccountActions = {
ConfirmModal
},
methods: {
refetchRelationship () {
refetchRelationship() {
return this.$store.dispatch('fetchUserRelationship', this.user.id)
},
showConfirmBlock () {
showConfirmBlock() {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
hideConfirmBlock() {
this.showingConfirmBlock = false
},
showRepeats () {
showRepeats() {
this.$store.dispatch('showReblogs', this.user.id)
},
hideRepeats () {
hideRepeats() {
this.$store.dispatch('hideReblogs', this.user.id)
},
blockUser () {
blockUser() {
if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showConfirmBlock()
}
},
doBlockUser () {
doBlockUser() {
this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock()
},
unblockUser () {
unblockUser() {
this.$store.dispatch('unblockUser', this.user.id)
},
removeUserFromFollowers () {
removeUserFromFollowers() {
this.$store.dispatch('removeUserFromFollowers', this.user.id)
},
reportUser () {
reportUser() {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
},
muteDomain () {
this.$store.dispatch('muteDomain', this.user.screen_name.split('@')[1])
muteDomain() {
this.$store
.dispatch('muteDomain', this.user.screen_name.split('@')[1])
.then(() => this.refetchRelationship())
},
unmuteDomain () {
this.$store.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
unmuteDomain() {
this.$store
.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
.then(() => this.refetchRelationship())
}
},
computed: {
shouldConfirmBlock () {
shouldConfirmBlock() {
return this.$store.getters.mergedConfig.modalOnBlock
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
pleromaChatMessagesAvailable: (state) =>
state.instance.pleromaChatMessagesAvailable
})
}
}

View File

@ -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
@ -71,7 +71,7 @@
</button>
</div>
</template>
<template v-slot:trigger>
<template #trigger>
<button class="button-unstyled ellipsis-button">
<FAIcon
class="icon"
@ -93,10 +93,8 @@
keypath="user_card.block_confirm"
tag="span"
>
<template v-slot:user>
<span
v-text="user.screen_name_ui"
/>
<template #user>
<span v-text="user.screen_name_ui" />
</template>
</i18n-t>
</confirm-modal>

View File

@ -8,7 +8,7 @@ const Announcement = {
AnnouncementEditor,
RichContent
},
data () {
data() {
return {
editing: false,
editedAnnouncement: {
@ -25,78 +25,93 @@ const Announcement = {
},
computed: {
...mapState({
currentUser: state => state.users.currentUser
currentUser: (state) => state.users.currentUser
}),
content () {
content() {
return this.announcement.content
},
isRead () {
isRead() {
return this.announcement.read
},
publishedAt () {
publishedAt() {
const time = this.announcement['published_at']
if (!time) {
return
}
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
},
startsAt () {
startsAt() {
const time = this.announcement['starts_at']
if (!time) {
return
}
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
},
endsAt () {
endsAt() {
const time = this.announcement['ends_at']
if (!time) {
return
}
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
return this.formatTimeOrDate(
time,
localeService.internalToBrowserLocale(this.$i18n.locale)
)
},
inactive () {
inactive() {
return this.announcement.inactive
}
},
methods: {
markAsRead () {
markAsRead() {
if (!this.isRead) {
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
return this.$store.dispatch(
'markAnnouncementAsRead',
this.announcement.id
)
}
},
deleteAnnouncement () {
deleteAnnouncement() {
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
},
formatTimeOrDate (time, locale) {
formatTimeOrDate(time, locale) {
const d = new Date(time)
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
return this.announcement['all_day']
? d.toLocaleDateString(locale)
: d.toLocaleString(locale)
},
enterEditMode () {
enterEditMode() {
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
this.editedAnnouncement.startsAt = this.announcement['starts_at']
this.editedAnnouncement.endsAt = this.announcement['ends_at']
this.editedAnnouncement.allDay = this.announcement['all_day']
this.editing = true
},
submitEdit () {
this.$store.dispatch('editAnnouncement', {
id: this.announcement.id,
...this.editedAnnouncement
})
submitEdit() {
this.$store
.dispatch('editAnnouncement', {
id: this.announcement.id,
...this.editedAnnouncement
})
.then(() => {
this.editing = false
})
.catch(error => {
.catch((error) => {
this.editError = error.error
})
},
cancelEdit () {
cancelEdit() {
this.editing = false
},
clearError () {
clearError() {
this.editError = undefined
}
}

View File

@ -21,7 +21,9 @@
class="times"
>
<span v-if="publishedAt">
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
{{
$t('announcements.published_time_display', { time: publishedAt })
}}
</span>
<span v-if="startsAt">
{{ $t('announcements.start_time_display', { time: startsAt }) }}
@ -99,7 +101,7 @@
<script src="./announcement.js"></script>
<style lang="scss">
@import "../../variables";
@import '../../variables';
.announcement {
border-bottom-width: 1px;
@ -108,7 +110,8 @@
border-radius: 0;
padding: var(--status-margin, $status-margin);
.heading, .body {
.heading,
.body {
margin-bottom: var(--status-margin, $status-margin);
}

View File

@ -10,22 +10,26 @@
:disabled="disabled"
/>
<span class="announcement-metadata">
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
<label for="announcement-start-time">{{
$t('announcements.start_time_prompt')
}}</label>
<input
id="announcement-start-time"
v-model="announcement.startsAt"
:type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled"
>
/>
</span>
<span class="announcement-metadata">
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
<label for="announcement-end-time">{{
$t('announcements.end_time_prompt')
}}</label>
<input
id="announcement-end-time"
v-model="announcement.endsAt"
:type="announcement.allDay ? 'date' : 'datetime-local'"
:disabled="disabled"
>
/>
</span>
<span class="announcement-metadata">
<Checkbox
@ -33,7 +37,9 @@
v-model="announcement.allDay"
:disabled="disabled"
/>
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
<label for="announcement-all-day">{{
$t('announcements.all_day_prompt')
}}</label>
</span>
</div>
</template>

View File

@ -7,7 +7,7 @@ const AnnouncementsPage = {
Announcement,
AnnouncementEditor
},
data () {
data() {
return {
newAnnouncement: {
content: '',
@ -19,34 +19,35 @@ const AnnouncementsPage = {
error: undefined
}
},
mounted () {
mounted() {
this.$store.dispatch('fetchAnnouncements')
},
computed: {
...mapState({
currentUser: state => state.users.currentUser
currentUser: (state) => state.users.currentUser
}),
announcements () {
announcements() {
return this.$store.state.announcements.announcements
}
},
methods: {
postAnnouncement () {
postAnnouncement() {
this.posting = true
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
this.$store
.dispatch('postAnnouncement', this.newAnnouncement)
.then(() => {
this.newAnnouncement.content = ''
this.startsAt = undefined
this.endsAt = undefined
})
.catch(error => {
.catch((error) => {
this.error = error.error
})
.finally(() => {
this.posting = false
})
},
clearError () {
clearError() {
this.error = undefined
}
}

View File

@ -6,9 +6,7 @@
</div>
</div>
<div class="panel-body">
<section
v-if="currentUser && currentUser.role === 'admin'"
>
<section v-if="currentUser && currentUser.role === 'admin'">
<div class="post-form">
<div class="heading">
<h4>{{ $t('announcements.post_form_header') }}</h4>
@ -50,9 +48,7 @@
v-for="announcement in announcements"
:key="announcement.id"
>
<announcement
:announcement="announcement"
/>
<announcement :announcement="announcement" />
</section>
</div>
</div>
@ -61,13 +57,14 @@
<script src="./announcements_page.js"></script>
<style lang="scss">
@import "../../variables";
@import '../../variables';
.announcements-page {
.post-form {
padding: var(--status-margin, $status-margin);
.heading, .body {
.heading,
.body {
margin-bottom: var(--status-margin, $status-margin);
}

View File

@ -21,7 +21,7 @@
export default {
emits: ['resetAsyncComponent'],
methods: {
retry () {
retry() {
this.$emit('resetAsyncComponent')
}
}
@ -35,8 +35,8 @@ export default {
align-items: center;
justify-content: center;
.btn {
margin: .5em;
padding: .5em 2em;
margin: 0.5em;
padding: 0.5em 2em;
}
}
</style>

View File

@ -47,14 +47,16 @@ const Attachment = {
'shiftDn',
'edit'
],
data () {
data() {
return {
localDescription: this.description || this.attachment.description,
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
img:
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
document.createElement('img'),
modalOpen: false,
showHidden: false,
flashLoaded: false,
@ -68,7 +70,7 @@ const Attachment = {
Blurhash
},
computed: {
classNames () {
classNames() {
return [
{
'-loading': this.loading,
@ -80,40 +82,40 @@ const Attachment = {
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
]
},
usePlaceholder () {
usePlaceholder() {
return this.size === 'hide'
},
useContainFit () {
useContainFit() {
return this.$store.getters.mergedConfig.useContainFit
},
useBlurhash () {
useBlurhash() {
return this.$store.getters.mergedConfig.useBlurhash
},
placeholderName () {
placeholderName() {
if (this.attachment.description === '' || !this.attachment.description) {
return this.type.toUpperCase()
}
return this.attachment.description
},
placeholderIconClass () {
placeholderIconClass() {
if (this.type === 'image') return 'image'
if (this.type === 'video') return 'video'
if (this.type === 'audio') return 'music'
return 'file'
},
referrerpolicy () {
referrerpolicy() {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
},
type () {
type() {
return fileTypeService.fileType(this.attachment.mimetype)
},
hidden () {
hidden() {
return this.nsfw && this.hideNsfwLocal && !this.showHidden
},
isEmpty () {
return (this.type === 'html' && !this.attachment.oembed)
isEmpty() {
return this.type === 'html' && !this.attachment.oembed
},
useModal () {
useModal() {
let modalTypes = []
switch (this.size) {
case 'hide':
@ -128,29 +130,29 @@ const Attachment = {
}
return modalTypes.includes(this.type)
},
videoTag () {
videoTag() {
return this.useModal ? 'button' : 'span'
},
statusForm () {
statusForm() {
return this.$parent.$parent
},
...mapGetters(['mergedConfig'])
},
watch: {
'attachment.description' (newVal) {
'attachment.description'(newVal) {
this.localDescription = newVal
},
localDescription (newVal) {
localDescription(newVal) {
this.onEdit(newVal)
}
},
methods: {
linkClicked ({ target }) {
linkClicked({ target }) {
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
openModal (event) {
openModal(event) {
if (this.useModal) {
this.$emit('setMedia')
this.$store.dispatch('setCurrentMedia', this.attachment)
@ -158,34 +160,35 @@ const Attachment = {
window.open(this.attachment.url)
}
},
openModalForce (event) {
openModalForce(event) {
this.$emit('setMedia')
this.$store.dispatch('setCurrentMedia', this.attachment)
},
onEdit (event) {
onEdit(event) {
this.edit && this.edit(this.attachment, event)
},
onRemove () {
onRemove() {
this.remove && this.remove(this.attachment)
},
onShiftUp () {
onShiftUp() {
this.shiftUp && this.shiftUp(this.attachment)
},
onShiftDn () {
onShiftDn() {
this.shiftDn && this.shiftDn(this.attachment)
},
stopFlash () {
stopFlash() {
this.$refs.flash.closePlayer()
},
setFlashLoaded (event) {
setFlashLoaded(event) {
this.flashLoaded = event
},
toggleDescription () {
toggleDescription() {
this.showDescription = !this.showDescription
},
toggleHidden (event) {
toggleHidden(event) {
if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
this.mergedConfig.useOneClickNsfw &&
!this.showHidden &&
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
) {
this.openModal(event)
@ -206,14 +209,16 @@ const Attachment = {
this.showHidden = !this.showHidden
}
},
onImageLoad (image) {
onImageLoad(image) {
const width = image.naturalWidth
const height = image.naturalHeight
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
},
resize (e) {
resize(e) {
const target = e.target || e
if (!(target instanceof window.Element)) { return }
if (!(target instanceof window.Element)) {
return
}
// Reset to default height for empty form, nothing else to do here.
if (target.value === '') {
@ -224,14 +229,16 @@ const Attachment = {
const paddingString = getComputedStyle(target)['padding']
// remove -px suffix
const padding = Number(paddingString.substring(0, paddingString.length - 2))
const padding = Number(
paddingString.substring(0, paddingString.length - 2)
)
target.style.height = 'auto'
const newHeight = Math.floor(target.scrollHeight - padding * 2)
target.style.height = `${newHeight}px`
this.$emit('resize', newHeight)
},
postStatus (event) {
postStatus(event) {
this.statusForm.postStatus(event, this.statusForm.newStatus)
}
}

View File

@ -117,7 +117,6 @@
padding-top: 0.5em;
}
.play-icon {
position: absolute;
font-size: 64px;

View File

@ -15,7 +15,8 @@
@click.prevent
>
<FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
<b>{{ nsfw ? 'NSFW / ' : '' }}</b
>{{ edit ? '' : placeholderName }}
</a>
<div
v-if="edit || remove"
@ -30,7 +31,11 @@
</button>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
v-if="
size !== 'hide' &&
!hideDescription &&
(edit || localDescription || showDescription)
"
class="description-container"
:class="{ '-static': !edit }"
>
@ -41,7 +46,7 @@
class="description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
/>
<p v-else>
{{ localDescription }}
</p>
@ -76,7 +81,7 @@
:key="nsfwImage"
class="nsfw"
:src="nsfwImage"
>
/>
<FAIcon
v-if="type === 'video'"
class="play-icon"
@ -96,7 +101,12 @@
<FAIcon icon="stop" />
</button>
<button
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
v-if="
attachment.description &&
size !== 'small' &&
!edit &&
type !== 'unknown'
"
class="button-unstyled attachment-button"
:title="$t('status.show_attachment_description')"
@click.prevent="toggleDescription"
@ -148,7 +158,7 @@
<a
v-if="type === 'image' && (!hidden || preloadImage)"
class="image-container"
:class="{'-hidden': hidden && preloadImage }"
:class="{ '-hidden': hidden && preloadImage }"
:href="attachment.url"
target="_blank"
@click.stop.prevent="openModal"
@ -226,11 +236,13 @@
v-if="attachment.thumb_url"
class="image"
>
<img :src="attachment.thumb_url">
<img :src="attachment.thumb_url" />
</div>
<div class="text">
<!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
<h1>
<a :href="attachment.url">{{ attachment.oembed.title }}</a>
</h1>
<div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enable vue/no-v-html -->
</div>
@ -252,7 +264,11 @@
</span>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
v-if="
size !== 'hide' &&
!hideDescription &&
(edit || (localDescription && showDescription))
"
class="description-container"
:class="{ '-static': !edit }"
>

View File

@ -6,13 +6,17 @@ import { mapGetters } from 'vuex'
const AuthForm = {
name: 'AuthForm',
render () {
render() {
return h(resolveComponent(this.authForm))
},
computed: {
authForm () {
if (this.requiredTOTP) { return 'MFATOTPForm' }
if (this.requiredRecovery) { return 'MFARecoveryForm' }
authForm() {
if (this.requiredTOTP) {
return 'MFATOTPForm'
}
if (this.requiredRecovery) {
return 'MFARecoveryForm'
}
return 'LoginForm'
},
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])

View File

@ -2,11 +2,13 @@ const debounceMilliseconds = 500
export default {
props: {
query: { // function to query results and return a promise
query: {
// function to query results and return a promise
type: Function,
required: true
},
filter: { // function to filter results in real time
filter: {
// function to filter results in real time
type: Function
},
placeholder: {
@ -14,7 +16,7 @@ export default {
default: 'Search...'
}
},
data () {
data() {
return {
term: '',
timeout: null,
@ -23,29 +25,31 @@ export default {
}
},
computed: {
filtered () {
filtered() {
return this.filter ? this.filter(this.results) : this.results
}
},
watch: {
term (val) {
term(val) {
this.fetchResults(val)
}
},
methods: {
fetchResults (term) {
fetchResults(term) {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.results = []
if (term) {
this.query(term).then((results) => { this.results = results })
this.query(term).then((results) => {
this.results = results
})
}
}, debounceMilliseconds)
},
onInputClick () {
onInputClick() {
this.resultsVisible = true
},
onClickOutside () {
onClickOutside() {
this.resultsVisible = false
}
}

View File

@ -8,7 +8,7 @@
:placeholder="placeholder"
class="autosuggest-input"
@click="onInputClick"
>
/>
<div
v-if="resultsVisible && filtered.length > 0"
class="autosuggest-results"

View File

@ -4,7 +4,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
const AvatarList = {
props: ['users'],
computed: {
slicedUsers () {
slicedUsers() {
return this.users ? this.users.slice(0, 15) : []
}
},
@ -12,8 +12,12 @@ const AvatarList = {
UserAvatar
},
methods: {
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
userProfileLink(user) {
return generateProfileLink(
user.id,
user.screen_name,
this.$store.state.instance.restrictedNicknames
)
}
}
}

View File

@ -14,7 +14,7 @@
</div>
</template>
<script src="./avatar_list.js" ></script>
<script src="./avatar_list.js"></script>
<style lang="scss">
@import '../../_variables.scss';

View File

@ -1,4 +1,3 @@
export default {
computed: {
}
computed: {}
}

View File

@ -4,10 +4,8 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = {
props: [
'user'
],
data () {
props: ['user'],
data() {
return {
userExpanded: false
}
@ -18,11 +16,15 @@ const BasicUserCard = {
RichContent
},
methods: {
toggleUserExpanded () {
toggleUserExpanded() {
this.userExpanded = !this.userExpanded
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
userProfileLink(user) {
return generateProfileLink(
user.id,
user.screen_name,
this.$store.state.instance.restrictedNicknames
)
}
}
}

View File

@ -2,19 +2,19 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const BlockCard = {
props: ['userId'],
data () {
data() {
return {
progress: false
}
},
computed: {
user () {
user() {
return this.$store.getters.findUser(this.userId)
},
relationship () {
relationship() {
return this.$store.getters.relationship(this.userId)
},
blocked () {
blocked() {
return this.relationship.blocking
}
},
@ -22,13 +22,13 @@ const BlockCard = {
BasicUserCard
},
methods: {
unblockUser () {
unblockUser() {
this.progress = true
this.$store.dispatch('unblockUser', this.user.id).then(() => {
this.progress = false
})
},
blockUser () {
blockUser() {
this.progress = true
this.$store.dispatch('blockUser', this.user.id).then(() => {
this.progress = false

View File

@ -6,61 +6,59 @@
</template>
<script>
import { decode } from "blurhash";
import { decode } from 'blurhash'
export default {
name: 'Blurhash',
props: {
hash: {
type: String,
required: true,
required: true
},
width: {
type: Number,
required: true,
required: true
},
height: {
type: Number,
required: true,
required: true
},
punch: {
type: Number,
default: null,
},
default: null
}
},
data() {
return {
canvas: null,
ctx: null,
};
ctx: null
}
},
mounted() {
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext('2d');
this.canvas.width = 1024;
this.canvas.height = 512;
this.draw();
this.canvas = this.$refs.canvas
this.ctx = this.canvas.getContext('2d')
this.canvas.width = 1024
this.canvas.height = 512
this.draw()
},
methods: {
draw() {
const pixels = decode(this.hash, this.width, this.height, this.punch);
const imageData = this.ctx.createImageData(this.width, this.height);
imageData.data.set(pixels);
this.ctx.putImageData(imageData, 0, 0);
fetch("/static/blurhash-overlay.png")
const pixels = decode(this.hash, this.width, this.height, this.punch)
const imageData = this.ctx.createImageData(this.width, this.height)
imageData.data.set(pixels)
this.ctx.putImageData(imageData, 0, 0)
fetch('/static/blurhash-overlay.png')
.then((response) => response.blob())
.then((blob) => {
const img = new Image();
img.src = URL.createObjectURL(blob);
const img = new Image()
img.src = URL.createObjectURL(blob)
img.onload = () => {
this.ctx.drawImage(img, 0, 0, this.width, this.height);
};
});
},
this.ctx.drawImage(img, 0, 0, this.width, this.height)
}
})
}
}
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -2,14 +2,14 @@ import Timeline from '../timeline/timeline.vue'
const Bookmarks = {
computed: {
timeline () {
timeline() {
return this.$store.state.statuses.timelines.bookmarks
}
},
components: {
Timeline
},
unmounted () {
unmounted() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
}
}

View File

@ -4,15 +4,16 @@ const PublicTimeline = {
Timeline
},
computed: {
timeline () { return this.$store.state.statuses.timelines.bubble }
timeline() {
return this.$store.state.statuses.timelines.bubble
}
},
created () {
created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
},
unmounted () {
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble')
}
}
export default PublicTimeline

View File

@ -11,14 +11,17 @@ export default {
name: 'Timeago',
props: ['date'],
computed: {
displayDate () {
displayDate() {
const today = new Date()
today.setHours(0, 0, 0, 0)
if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today')
} else {
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
return this.date.toLocaleDateString(
localeService.internalToBrowserLocale(this.$i18n.locale),
{ day: 'numeric', month: 'long' }
)
}
}
}

View File

@ -9,7 +9,7 @@
:checked="modelValue"
:indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)"
>
/>
<i class="checkbox-indicator" />
<span
v-if="!!$slots.default"
@ -22,12 +22,8 @@
<script>
export default {
emits: ['update:modelValue'],
props: [
'modelValue',
'indeterminate',
'disabled'
]
props: ['modelValue', 'indeterminate', 'disabled'],
emits: ['update:modelValue']
}
</script>
@ -56,7 +52,7 @@ export default {
border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
@ -71,7 +67,7 @@ export default {
&.disabled {
.checkbox-indicator::before,
.label {
opacity: .5;
opacity: 0.5;
}
.label {
color: $fallback--faint;
@ -79,7 +75,7 @@ export default {
}
}
input[type=checkbox] {
input[type='checkbox'] {
display: none;
&:checked + .checkbox-indicator::before {
@ -92,11 +88,10 @@ export default {
color: $fallback--text;
color: var(--inputText, $fallback--text);
}
}
& > span {
margin-left: .5em;
margin-left: 0.5em;
}
}
</style>

View File

@ -8,7 +8,7 @@
flex: 0 0 0;
max-width: 9em;
align-items: stretch;
padding: .2em 8px;
padding: 0.2em 8px;
input {
background: none;
@ -40,9 +40,10 @@
}
.transparentIndicator {
// forgot to install counter-strike source, ooops
background-color: #FF00FF;
background-color: #ff00ff;
position: relative;
&::before, &::after {
&::before,
&::after {
display: block;
content: '';
background-color: #000000;
@ -64,5 +65,4 @@
.label {
flex: 1 1 auto;
}
}

View File

@ -14,7 +14,12 @@
:model-value="present"
:disabled="disabled"
class="opt"
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
@update:modelValue="
$emit(
'update:modelValue',
typeof modelValue === 'undefined' ? fallback : undefined
)
"
/>
<div class="input color-input-field">
<input
@ -24,7 +29,7 @@
:value="modelValue || fallback"
:disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)"
>
/>
<input
v-if="validColor"
:id="name"
@ -33,7 +38,7 @@
:value="modelValue || fallback"
:disabled="!present || disabled"
@input="$emit('update:modelValue', $event.target.value)"
>
/>
<div
v-if="transparentColor"
class="transparentIndicator"
@ -41,12 +46,11 @@
<div
v-if="computedColor"
class="computedIndicator"
:style="{backgroundColor: fallback}"
:style="{ backgroundColor: fallback }"
/>
</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'
@ -93,21 +97,22 @@ export default {
},
emits: ['update:modelValue'],
computed: {
present () {
present() {
return typeof this.modelValue !== 'undefined'
},
validColor () {
validColor() {
return hex2rgb(this.modelValue || this.fallback)
},
transparentColor () {
transparentColor() {
return this.modelValue === 'transparent'
},
computedColor () {
computedColor() {
return this.modelValue && this.modelValue.startsWith('--')
}
}
}
</script>
<style lang="scss" src="./color_input.scss"></style>
<style lang="scss">
.color-control {

View File

@ -22,13 +22,12 @@ const ConfirmModal = {
type: String
}
},
computed: {
},
computed: {},
methods: {
onCancel () {
onCancel() {
this.$emit('cancelled')
},
onAccept () {
onAccept() {
this.$emit('accepted')
}
}

View File

@ -25,6 +25,8 @@
</dialog-modal>
</template>
<script src="./confirm_modal.js"></script>
<style lang="scss" scoped>
@import '../../_variables';
@ -35,5 +37,3 @@
}
}
</style>
<script src="./confirm_modal.js"></script>

View File

@ -43,11 +43,7 @@ import {
faThumbsUp
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAdjust,
faExclamationTriangle,
faThumbsUp
)
library.add(faAdjust, faExclamationTriangle, faThumbsUp)
export default {
props: {
@ -65,19 +61,35 @@ export default {
}
},
computed: {
hint () {
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
hint() {
const levelVal = this.contrast.aaa
? 'aaa'
: this.contrast.aa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.text')
const ratio = this.contrast.text
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
return this.$t('settings.style.common.contrast.hint', {
level,
context,
ratio
})
},
hint_18pt () {
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad')
hint_18pt() {
const levelVal = this.contrast.laaa
? 'aaa'
: this.contrast.laa
? 'aa'
: 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.18pt')
const ratio = this.contrast.text
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
return this.$t('settings.style.common.contrast.hint', {
level,
context,
ratio
})
}
}
}

View File

@ -5,7 +5,7 @@ const conversationPage = {
Conversation
},
computed: {
statusId () {
statusId() {
return this.$route.params.id
}
}

View File

@ -11,11 +11,7 @@ import {
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
)
library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@ -39,16 +35,17 @@ const sortAndFilterConversation = (conversation, statusoid) => {
if (statusoid.type === 'retweet') {
conversation = filter(
conversation,
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
(status) =>
status.type === 'retweet' || status.id !== statusoid.retweeted_status.id
)
} else {
conversation = filter(conversation, (status) => status.type !== 'retweet')
}
return conversation.filter(_ => _).sort(sortById)
return conversation.filter((_) => _).sort(sortById)
}
const conversation = {
data () {
data() {
return {
highlight: null,
expanded: false,
@ -66,74 +63,78 @@ const conversation = {
'profileUserId',
'virtualHidden'
],
created () {
created() {
if (this.isPage) {
this.fetchConversation()
}
},
computed: {
maxDepthToShowByDefault () {
maxDepthToShowByDefault() {
// maxDepthInThread = max number of depths that is *visible*
// since our depth starts with 0 and "showing" means "showing children"
// there is a -2 here
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1
},
streamingEnabled () {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
streamingEnabled() {
return (
this.mergedConfig.useStreamingApi &&
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
)
},
displayStyle () {
displayStyle() {
return this.$store.getters.mergedConfig.conversationDisplay
},
isTreeView () {
isTreeView() {
return !this.isLinearView
},
treeViewIsSimple () {
treeViewIsSimple() {
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
},
isLinearView () {
isLinearView() {
return this.displayStyle === 'linear'
},
shouldFadeAncestors () {
shouldFadeAncestors() {
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
},
otherRepliesButtonPosition () {
otherRepliesButtonPosition() {
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
},
showOtherRepliesButtonBelowStatus () {
showOtherRepliesButtonBelowStatus() {
return this.otherRepliesButtonPosition === 'below'
},
showOtherRepliesButtonInsideStatus () {
showOtherRepliesButtonInsideStatus() {
return this.otherRepliesButtonPosition === 'inside'
},
suspendable () {
suspendable() {
if (this.isTreeView) {
return Object.entries(this.statusContentProperties)
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
return Object.entries(this.statusContentProperties).every(
([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0
)
}
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.$refs.statusComponent.every(s => s.suspendable)
return this.$refs.statusComponent.every((s) => s.suspendable)
} else {
return true
}
},
hideStatus () {
hideStatus() {
return this.virtualHidden && this.suspendable
},
status () {
status() {
return this.$store.state.statuses.allStatusesObject[this.statusId]
},
originalStatusId () {
originalStatusId() {
if (this.status.retweeted_status) {
return this.status.retweeted_status.id
} else {
return this.statusId
}
},
conversationId () {
conversationId() {
return this.getConversationId(this.statusId)
},
conversation () {
conversation() {
if (!this.status) {
return []
}
@ -142,155 +143,203 @@ const conversation = {
return [this.status]
}
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
const conversation = clone(
this.$store.state.statuses.conversationsObject[this.conversationId]
)
const statusIndex = findIndex(conversation, {
id: this.originalStatusId
})
if (statusIndex !== -1) {
conversation[statusIndex] = this.status
}
return sortAndFilterConversation(conversation, this.status)
},
statusMap () {
statusMap() {
return this.conversation.reduce((res, s) => {
res[s.id] = s
return res
}, {})
},
threadTree () {
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
table[status.id] = index
return table
}, {})
threadTree() {
const reverseLookupTable = this.conversation.reduce(
(table, status, index) => {
table[status.id] = index
return table
},
{}
)
const threads = this.conversation.reduce((a, cur) => {
const id = cur.id
a.forest[id] = this.getReplies(id)
.map(s => s.id)
const threads = this.conversation.reduce(
(a, cur) => {
const id = cur.id
a.forest[id] = this.getReplies(id).map((s) => s.id)
return a
}, {
forest: {}
})
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
if (processed[id]) {
return []
return a
},
{
forest: {}
}
)
processed[id] = true
return [{
status: this.conversation[reverseLookupTable[id]],
id,
depth
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
}).reduce((a, b) => a.concat(b), [])
const walk = (forest, topLevel, depth = 0, processed = {}) =>
topLevel
.map((id) => {
if (processed[id]) {
return []
}
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
processed[id] = true
return [
{
status: this.conversation[reverseLookupTable[id]],
id,
depth
},
walk(forest, forest[id], depth + 1, processed)
].reduce((a, b) => a.concat(b), [])
})
.reduce((a, b) => a.concat(b), [])
const linearized = walk(
threads.forest,
this.topLevel.map((k) => k.id)
)
return linearized
},
replyIds () {
return this.conversation.map(k => k.id)
replyIds() {
return this.conversation
.map((k) => k.id)
.reduce((res, id) => {
res[id] = (this.replies[id] || []).map(k => k.id)
res[id] = (this.replies[id] || []).map((k) => k.id)
return res
}, {})
},
totalReplyCount () {
totalReplyCount() {
const sizes = {}
const subTreeSizeFor = (id) => {
if (sizes[id]) {
return sizes[id]
}
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
sizes[id] =
1 +
this.replyIds[id]
.map((cid) => subTreeSizeFor(cid))
.reduce((a, b) => a + b, 0)
return sizes[id]
}
this.conversation.map(k => k.id).map(subTreeSizeFor)
this.conversation.map((k) => k.id).map(subTreeSizeFor)
return Object.keys(sizes).reduce((res, id) => {
res[id] = sizes[id] - 1 // exclude itself
return res
}, {})
},
totalReplyDepth () {
totalReplyDepth() {
const depths = {}
const subTreeDepthFor = (id) => {
if (depths[id]) {
return depths[id]
}
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
depths[id] =
1 +
this.replyIds[id]
.map((cid) => subTreeDepthFor(cid))
.reduce((a, b) => (a > b ? a : b), 0)
return depths[id]
}
this.conversation.map(k => k.id).map(subTreeDepthFor)
this.conversation.map((k) => k.id).map(subTreeDepthFor)
return Object.keys(depths).reduce((res, id) => {
res[id] = depths[id] - 1 // exclude itself
return res
}, {})
},
depths () {
depths() {
return this.threadTree.reduce((a, k) => {
a[k.id] = k.depth
return a
}, {})
},
topLevel () {
const topLevel = this.conversation.reduce((tl, cur) =>
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
topLevel() {
const topLevel = this.conversation.reduce(
(tl, cur) =>
tl.filter(
(k) =>
this.getReplies(cur.id)
.map((v) => v.id)
.indexOf(k.id) === -1
),
this.conversation
)
return topLevel
},
otherTopLevelCount () {
otherTopLevelCount() {
return this.topLevel.length - 1
},
showingTopLevel () {
showingTopLevel() {
if (this.canDive && this.diveRoot) {
return [this.statusMap[this.diveRoot]]
}
return this.topLevel
},
diveRoot () {
diveRoot() {
const statusId = this.inlineDivePosition || this.statusId
const isTopLevel = !this.parentOf(statusId)
return isTopLevel ? null : statusId
},
diveDepth () {
diveDepth() {
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
},
diveMode () {
diveMode() {
return this.canDive && !!this.diveRoot
},
shouldShowAllConversationButton () {
shouldShowAllConversationButton() {
// The "show all conversation" button tells the user that there exist
// other toplevel statuses, so do not show it if there is only a single root
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
return (
this.isTreeView &&
this.isExpanded &&
this.diveMode &&
this.topLevel.length > 1
)
},
shouldShowAncestors () {
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
shouldShowAncestors() {
return (
this.isTreeView &&
this.isExpanded &&
this.ancestorsOf(this.diveRoot).length
)
},
replies () {
replies() {
let i = 1
// eslint-disable-next-line camelcase
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
/* eslint-disable camelcase */
const irid = in_reply_to_status_id
/* eslint-enable camelcase */
if (irid) {
result[irid] = result[irid] || []
result[irid].push({
name: `#${i}`,
id: id
})
}
i++
return result
}, {})
return reduce(
this.conversation,
(result, { id, in_reply_to_status_id }) => {
/* eslint-disable camelcase */
const irid = in_reply_to_status_id
/* eslint-enable camelcase */
if (irid) {
result[irid] = result[irid] || []
result[irid].push({
name: `#${i}`,
id: id
})
}
i++
return result
},
{}
)
},
isExpanded () {
isExpanded() {
return !!(this.expanded || this.isPage)
},
hiddenStyle () {
hiddenStyle() {
const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {}
},
threadDisplayStatus () {
threadDisplayStatus() {
return this.conversation.reduce((a, k) => {
const id = k.id
const depth = this.depths[id]
@ -298,7 +347,7 @@ const conversation = {
if (this.threadDisplayStatusObject[id]) {
return this.threadDisplayStatusObject[id]
}
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
return 'showing'
} else {
return 'hidden'
@ -309,7 +358,7 @@ const conversation = {
return a
}, {})
},
statusContentProperties () {
statusContentProperties() {
return this.conversation.reduce((a, k) => {
const id = k.id
const props = (() => {
@ -334,20 +383,20 @@ const conversation = {
return a
}, {})
},
canDive () {
canDive() {
return this.isTreeView && this.isExpanded
},
focused () {
focused() {
return (id) => {
return (this.isExpanded) && id === this.highlight
return this.isExpanded && id === this.highlight
}
},
maybeHighlight () {
maybeHighlight() {
return this.isExpanded ? this.highlight : null
},
...mapGetters(['mergedConfig']),
...mapState({
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus
})
},
components: {
@ -355,53 +404,59 @@ const conversation = {
ThreadTree
},
watch: {
statusId (newVal, oldVal) {
statusId(newVal, oldVal) {
const newConversationId = this.getConversationId(newVal)
const oldConversationId = this.getConversationId(oldVal)
if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
if (
newConversationId &&
oldConversationId &&
newConversationId === oldConversationId
) {
this.setHighlight(this.originalStatusId)
} else {
this.fetchConversation()
}
},
expanded (value) {
expanded(value) {
if (value) {
this.fetchConversation()
} else {
this.resetDisplayState()
}
},
virtualHidden (value) {
this.$store.dispatch(
'setVirtualHeight',
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
)
virtualHidden(value) {
this.$store.dispatch('setVirtualHeight', {
statusId: this.statusId,
height: `${this.$el.clientHeight}px`
})
}
},
methods: {
fetchConversation () {
fetchConversation() {
if (this.status) {
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
this.$store.state.api.backendInteractor
.fetchConversation({ id: this.statusId })
.then(({ ancestors, descendants }) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.$store.dispatch('addNewStatuses', { statuses: descendants })
this.setHighlight(this.originalStatusId)
})
} else {
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
this.$store.state.api.backendInteractor
.fetchStatus({ id: this.statusId })
.then((status) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] })
this.fetchConversation()
})
}
},
getReplies (id) {
getReplies(id) {
return this.replies[id] || []
},
getHighlight () {
getHighlight() {
return this.isExpanded ? this.highlight : null
},
setHighlight (id) {
setHighlight(id) {
if (!id) return
this.highlight = id
@ -412,32 +467,38 @@ const conversation = {
this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id)
},
toggleExpanded () {
toggleExpanded() {
this.expanded = !this.expanded
},
getConversationId (statusId) {
getConversationId(statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId]
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
return get(
status,
'retweeted_status.statusnet_conversation_id',
get(status, 'statusnet_conversation_id')
)
},
setThreadDisplay (id, nextStatus) {
setThreadDisplay(id, nextStatus) {
this.threadDisplayStatusObject = {
...this.threadDisplayStatusObject,
[id]: nextStatus
}
},
toggleThreadDisplay (id) {
toggleThreadDisplay(id) {
const curStatus = this.threadDisplayStatus[id]
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
this.setThreadDisplay(id, nextStatus)
},
setThreadDisplayRecursively (id, nextStatus) {
setThreadDisplayRecursively(id, nextStatus) {
this.setThreadDisplay(id, nextStatus)
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
this.getReplies(id)
.map((k) => k.id)
.map((id) => this.setThreadDisplayRecursively(id, nextStatus))
},
showThreadRecursively (id) {
showThreadRecursively(id) {
this.setThreadDisplayRecursively(id, 'showing')
},
setStatusContentProperty (id, name, value) {
setStatusContentProperty(id, name, value) {
this.statusContentPropertiesObject = {
...this.statusContentPropertiesObject,
[id]: {
@ -446,10 +507,14 @@ const conversation = {
}
}
},
toggleStatusContentProperty (id, name) {
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
toggleStatusContentProperty(id, name) {
this.setStatusContentProperty(
id,
name,
!this.statusContentProperties[id][name]
)
},
leastVisibleAncestor (id) {
leastVisibleAncestor(id) {
let cur = id
let parent = this.parentOf(cur)
while (cur) {
@ -463,18 +528,20 @@ const conversation = {
// nothing found, fall back to toplevel
return this.topLevel[0] ? this.topLevel[0].id : undefined
},
diveIntoStatus (id, preventScroll) {
diveIntoStatus(id, preventScroll) {
this.tryScrollTo(id)
},
diveToTopLevel () {
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
diveToTopLevel() {
this.tryScrollTo(
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id
)
},
// only used when we are not on a page
undive () {
undive() {
this.inlineDivePosition = null
this.setHighlight(this.statusId)
},
tryScrollTo (id) {
tryScrollTo(id) {
if (!id) {
return
}
@ -503,13 +570,13 @@ const conversation = {
this.setHighlight(id)
})
},
goToCurrent () {
goToCurrent() {
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
},
statusById (id) {
statusById(id) {
return this.statusMap[id]
},
parentOf (id) {
parentOf(id) {
const status = this.statusById(id)
if (!status) {
return undefined
@ -520,11 +587,11 @@ const conversation = {
}
return parentId
},
parentOrSelf (id) {
parentOrSelf(id) {
return this.parentOf(id) || id
},
// Ancestors of some status, from top to bottom
ancestorsOf (id) {
ancestorsOf(id) {
const ancestors = []
let cur = this.parentOf(id)
while (cur) {
@ -533,7 +600,7 @@ const conversation = {
}
return ancestors
},
topLevelAncestorOrSelfId (id) {
topLevelAncestorOrSelfId(id) {
let cur = id
let parent = this.parentOf(id)
while (parent) {
@ -542,7 +609,7 @@ const conversation = {
}
return cur
},
resetDisplayState () {
resetDisplayState() {
this.undive()
this.threadDisplayStatusObject = {}
}

View File

@ -3,7 +3,7 @@
v-if="!hideStatus"
:style="hiddenStyle"
class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
:class="{ '-expanded': isExpanded, panel: isExpanded }"
>
<div
v-if="isExpanded"
@ -35,13 +35,15 @@
@click.prevent="diveToTopLevel"
>
<template #icon>
<FAIcon
icon="angle-double-left"
/>
<FAIcon icon="angle-double-left" />
</template>
<template #text>
<span>
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
{{
$tc('status.show_all_conversation', otherTopLevelCount, {
numStatus: otherTopLevelCount
})
}}
</span>
</template>
</i18n-t>
@ -54,14 +56,20 @@
v-for="status in ancestorsOf(diveRoot)"
:key="status.id"
class="thread-ancestor"
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
:class="{
'thread-ancestor-has-other-replies':
getReplies(status.id).length > 1,
'-faded': shouldFadeAncestors
}"
>
<status
ref="statusComponent"
:inline-expanded="collapsable && isExpanded"
:statusoid="status"
:expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:show-pinned="
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
"
:focused="focused(status.id)"
:in-conversation="isExpanded"
:highlight="getHighlight()"
@ -69,7 +77,6 @@
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body"
:simple-tree="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus"
@ -78,28 +85,47 @@
:total-reply-depth="totalReplyDepth"
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
:dive="() => diveIntoStatus(status.id)"
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
:controlled-showing-tall="
statusContentProperties[status.id].showingTall
"
:controlled-expanding-subject="
statusContentProperties[status.id].expandingSubject
"
:controlled-showing-long-subject="
statusContentProperties[status.id].showingLongSubject
"
:controlled-replying="statusContentProperties[status.id].replying"
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
:controlled-media-playing="
statusContentProperties[status.id].mediaPlaying
"
:controlled-toggle-showing-tall="
() => toggleStatusContentProperty(status.id, 'showingTall')
"
:controlled-toggle-expanding-subject="
() => toggleStatusContentProperty(status.id, 'expandingSubject')
"
:controlled-toggle-showing-long-subject="
() =>
toggleStatusContentProperty(status.id, 'showingLongSubject')
"
:controlled-toggle-replying="
() => toggleStatusContentProperty(status.id, 'replying')
"
:controlled-set-media-playing="
(newVal) =>
toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)
"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
/>
<div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
v-if="
showOtherRepliesButtonBelowStatus &&
getReplies(status.id).length > 1
"
class="thread-ancestor-dive-box"
>
<div
class="thread-ancestor-dive-box-inner"
>
<div class="thread-ancestor-dive-box-inner">
<i18n-t
tag="button"
scope="global"
@ -108,13 +134,17 @@
@click.prevent="diveIntoStatus(status.id)"
>
<template #icon>
<FAIcon
icon="angle-double-right"
/>
<FAIcon icon="angle-double-right" />
</template>
<template #text>
<span>
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
{{
$tc(
'status.ancestor_follow',
getReplies(status.id).length - 1,
{ numReplies: getReplies(status.id).length - 1 }
)
}}
</span>
</template>
</i18n-t>
@ -127,7 +157,6 @@
:key="status.id"
ref="statusComponent"
:depth="0"
:status="status"
:in-profile="inProfile"
:conversation="conversation"
@ -135,13 +164,11 @@
:is-expanded="isExpanded"
:pinned-status-ids-object="pinnedStatusIdsObject"
:profile-user-id="profileUserId"
:focused="focused"
:get-replies="getReplies"
:highlight="maybeHighlight"
:set-highlight="setHighlight"
:toggle-expanded="toggleExpanded"
:simple="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus"
@ -165,7 +192,9 @@
:inline-expanded="collapsable && isExpanded"
:statusoid="status"
:expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:show-pinned="
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
"
:focused="focused(status.id)"
:in-conversation="isExpanded"
:highlight="getHighlight()"
@ -173,7 +202,6 @@
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="conversation-status status-fadein panel-body"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively"
@ -182,7 +210,6 @@
:status-content-properties="statusContentProperties"
:set-status-content-property="setStatusContentProperty"
:toggle-status-content-property="toggleStatusContentProperty"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
/>
@ -233,7 +260,8 @@
border-bottom-color: var(--border, $fallback--border);
border-radius: 0;
/* Make the button stretch along the whole row */
&, &-inner {
&,
&-inner {
display: flex;
align-items: stretch;
flex-direction: column;
@ -253,8 +281,7 @@
.thread-ancestor-has-other-replies .conversation-status,
.thread-ancestor:last-child .conversation-status,
.thread-ancestor:last-child .thread-ancestor-dive-box,
&:last-child .conversation-status,
&.-expanded .thread-tree .conversation-status {
&:last-child .conversation-status {
border-bottom: none;
}
@ -271,7 +298,8 @@
border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed);
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius)
var(--panelRadius, $fallback--panelRadius);
border-bottom: 1px solid var(--border, $fallback--border);
}

View File

@ -4,7 +4,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import {
publicTimelineVisible,
federatedTimelineVisible,
bubbleTimelineVisible,
bubbleTimelineVisible
} from '../../lib/timeline_visibility'
import {
faSignInAlt,
@ -52,81 +52,103 @@ export default {
},
data: () => ({
searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') ||
supportsMask:
window.CSS &&
window.CSS.supports &&
(window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain')
),
window.CSS.supports('-o-mask-size', 'contain')),
showingConfirmLogout: false
}),
computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
logoStyle () {
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'
}
logoMaskStyle() {
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`,
opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
logoBgStyle() {
return Object.assign(
{
margin: `${this.$store.state.instance.logoMargin} 0`,
opacity: this.searchBarHidden ? 1 : 0
},
this.enableMask
? {}
: {
'background-color': this.enableMask ? '' : 'transparent'
}
)
},
logo () { return this.$store.state.instance.logo },
mergedConfig () {
logo() {
return this.$store.state.instance.logo
},
mergedConfig() {
return this.$store.getters.mergedConfig
},
sitename () { return this.$store.state.instance.name },
showNavShortcuts () {
sitename() {
return this.$store.state.instance.name
},
showNavShortcuts() {
return this.mergedConfig.showNavShortcuts
},
showWiderShortcuts () {
showWiderShortcuts() {
return this.mergedConfig.showWiderShortcuts
},
hideSiteFavicon () {
hideSiteFavicon() {
return this.mergedConfig.hideSiteFavicon
},
hideSiteName () {
hideSiteName() {
return this.mergedConfig.hideSiteName
},
hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private },
shouldConfirmLogout () {
hideSitename() {
return this.$store.state.instance.hideSitename
},
logoLeft() {
return this.$store.state.instance.logoLeft
},
currentUser() {
return this.$store.state.users.currentUser
},
privateMode() {
return this.$store.state.instance.private
},
shouldConfirmLogout() {
return this.$store.getters.mergedConfig.modalOnLogout
},
showBubbleTimeline () {
showBubbleTimeline() {
return this.$store.state.instance.localBubbleInstances.length > 0
},
...mapState({
publicTimelineVisible,
federatedTimelineVisible,
bubbleTimelineVisible,
bubbleTimelineVisible
})
},
methods: {
scrollToTop () {
scrollToTop() {
window.scrollTo(0, 0)
},
onSearchBarToggled (hidden) {
onSearchBarToggled(hidden) {
this.searchBarHidden = hidden
},
openSettingsModal () {
openSettingsModal() {
this.$store.dispatch('openSettingsModal')
},
openModModal () {
openModModal() {
this.$store.dispatch('openModModal')
}
}

View File

@ -15,7 +15,7 @@
display: grid;
grid-template-rows: var(--navbar-height);
grid-template-columns: 2fr auto 2fr;
grid-template-areas: "nav-left logo actions";
grid-template-areas: 'nav-left logo actions';
box-sizing: border-box;
padding: 0 1.2em;
margin: auto;
@ -24,11 +24,12 @@
&.-logoLeft .inner-nav {
grid-template-columns: auto 2fr 2fr;
grid-template-areas: "logo nav-left actions";
grid-template-areas: 'logo nav-left actions';
}
.button-default {
&, svg {
&,
svg {
color: $fallback--text;
color: var(--btnTopBarText, $fallback--text);
}
@ -49,7 +50,7 @@
color: $fallback--text;
color: var(--btnToggledTopBarText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggledTopBar, $fallback--fg)
background-color: var(--btnToggledTopBar, $fallback--fg);
}
}
@ -88,13 +89,25 @@
width: 2em;
height: 100%;
text-align: center;
display: inline-block;
&.router-link-active {
font-size: 1.2em;
margin-top: 0.05em;
// box-shadow: 0 -2px 0 var(--selectedMenuText, $fallback--text) inset;
position: relative;
&:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
}
.svg-inline--fa {
font-weight: bolder;
color: $fallback--text;
color: var(--selectedMenuText, $fallback--text);
--lightText: var(--selectedMenuLightText, $fallback--lightText);

View File

@ -18,8 +18,8 @@
<img
v-if="!hideSiteFavicon"
class="favicon"
src="/favicon.png"
>
src="/favicon.svg"
/>
<span
v-if="!hideSiteName"
class="site-name"
@ -44,9 +44,9 @@
/>
</router-link>
<router-link
v-if="publicTimelineVisible"
:to="{ name: 'public-timeline' }"
class="nav-icon"
v-if="publicTimelineVisible"
>
<FAIcon
fixed-width
@ -68,9 +68,9 @@
/>
</router-link>
<router-link
v-if="federatedTimelineVisible"
:to="{ name: 'public-external-timeline' }"
class="nav-icon"
v-if="federatedTimelineVisible"
>
<FAIcon
fixed-width
@ -93,7 +93,7 @@
<img
:src="logo"
:style="logoStyle"
>
/>
</router-link>
<div class="item right actions">
<search-bar
@ -108,7 +108,10 @@
<router-link
v-if="currentUser"
class="nav-icon"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
:to="{
name: 'interactions',
params: { username: currentUser.screen_name }
}"
>
<FAIcon
fixed-width
@ -154,7 +157,10 @@
/>
</button>
<button
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
v-if="
(currentUser && currentUser.role === 'admin') ||
currentUser.role === 'moderator'
"
class="button-unstyled nav-icon"
@click.stop="openModModal"
>

View File

@ -31,14 +31,14 @@
.dark-overlay {
&::before {
bottom: 0;
content: " ";
content: ' ';
display: block;
cursor: default;
left: 0;
position: fixed;
right: 0;
top: 0;
background: rgba(27,31,35,.5);
background: rgba(27, 31, 35, 0.5);
z-index: 2000;
}
}
@ -74,7 +74,7 @@
.dialog-modal-footer {
margin: 0;
padding: .5em .5em;
padding: 0.5em 0.5em;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
border-top: 1px solid $fallback--border;
@ -84,9 +84,8 @@
button {
width: auto;
margin-left: .5rem;
margin-left: 0.5rem;
}
}
}
</style>

View File

@ -2,7 +2,7 @@ import Timeline from '../timeline/timeline.vue'
const DMs = {
computed: {
timeline () {
timeline() {
return this.$store.state.statuses.timelines.dms
}
},

View File

@ -6,18 +6,18 @@ const DomainMuteCard = {
ProgressButton
},
computed: {
user () {
user() {
return this.$store.state.users.currentUser
},
muted () {
muted() {
return this.user.domainMutes.includes(this.domain)
}
},
methods: {
unmuteDomain () {
unmuteDomain() {
return this.$store.dispatch('unmuteDomain', this.domain)
},
muteDomain () {
muteDomain() {
return this.$store.dispatch('muteDomain', this.domain)
}
}

View File

@ -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>

View File

@ -8,27 +8,27 @@ const EditStatusModal = {
PostStatusForm,
Modal
},
data () {
data() {
return {
resettingForm: false
}
},
computed: {
isLoggedIn () {
isLoggedIn() {
return !!this.$store.state.users.currentUser
},
modalActivated () {
modalActivated() {
return this.$store.state.editStatus.modalActivated
},
isFormVisible () {
isFormVisible() {
return this.isLoggedIn && !this.resettingForm && this.modalActivated
},
params () {
params() {
return this.$store.state.editStatus.params || {}
}
},
watch: {
params (newVal, oldVal) {
params(newVal, oldVal) {
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
this.resettingForm = true
this.$nextTick(() => {
@ -36,14 +36,16 @@ const EditStatusModal = {
})
}
},
isFormVisible (val) {
isFormVisible(val) {
if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
this.$nextTick(
() => this.$el && this.$el.querySelector('textarea').focus()
)
}
}
},
methods: {
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
const params = {
store: this.$store,
statusId: this.$store.state.editStatus.params.statusId,
@ -55,7 +57,8 @@ const EditStatusModal = {
contentType
}
return statusPosterService.editStatus(params)
return statusPosterService
.editStatus(params)
.then((data) => {
return data
})
@ -66,7 +69,7 @@ const EditStatusModal = {
}
})
},
closeModal () {
closeModal() {
this.$store.dispatch('closeEditStatusModal')
}
}

View File

@ -11,10 +11,10 @@
<PostStatusForm
class="panel-body"
v-bind="params"
@posted="closeModal"
:disablePolls="true"
:disableVisibilitySelector="true"
:disable-polls="true"
:disable-visibility-selector="true"
:post-handler="doEditStatus"
@posted="closeModal"
/>
</div>
</Modal>

View File

@ -9,7 +9,7 @@ const EmojiGrid = {
type: Array
}
},
data () {
data() {
return {
containerWidth: 0,
containerHeight: 0,
@ -17,7 +17,7 @@ const EmojiGrid = {
resizeObserver: null
}
},
mounted () {
mounted() {
const rect = this.$refs.container.getBoundingClientRect()
this.containerWidth = rect.width
this.containerHeight = rect.height
@ -29,29 +29,29 @@ const EmojiGrid = {
})
this.resizeObserver.observe(this.$refs.container)
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.disconnect()
this.resizeObserver = null
},
watch: {
groups () {
groups() {
// Scroll to top when grid content changes
if (this.$refs.container) {
this.$refs.container.scrollTo(0, 0)
}
},
activeGroup (group) {
activeGroup(group) {
this.$emit('activeGroup', group)
}
},
methods: {
onScroll () {
onScroll() {
this.scrollPos = this.$refs.container.scrollTop
},
onEmoji (emoji) {
onEmoji(emoji) {
this.$emit('emoji', emoji)
},
scrollToItem (itemId) {
scrollToItem(itemId) {
const container = this.$refs.container
if (!container) return
@ -65,7 +65,7 @@ const EmojiGrid = {
},
computed: {
// Total height of scroller content
gridHeight () {
gridHeight() {
if (this.itemList.length === 0) return 0
const lastItem = this.itemList[this.itemList.length - 1]
return (
@ -73,7 +73,7 @@ const EmojiGrid = {
('title' in lastItem ? GROUP_TITLE_HEIGHT : EMOJI_SIZE)
)
},
activeGroup () {
activeGroup() {
const items = this.itemList
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i]
@ -83,7 +83,7 @@ const EmojiGrid = {
}
return null
},
itemList () {
itemList() {
const items = []
let x = 0
let y = 0
@ -111,14 +111,14 @@ const EmojiGrid = {
}
return items
},
visibleItems () {
visibleItems() {
const startPos = this.scrollPos - BUFFER_SIZE
const endPos = this.scrollPos + this.containerHeight + BUFFER_SIZE
return this.itemList.filter((i) => {
return i.position.y >= startPos && i.position.y < endPos
})
},
scrolledClass () {
scrolledClass() {
if (this.scrollPos <= 5) {
return 'scrolled-top'
} else if (this.scrollPos >= this.gridHeight - this.containerHeight - 5) {

View File

@ -7,7 +7,7 @@
>
<div
:style="{
height: `${gridHeight}px`,
height: `${gridHeight}px`
}"
>
<template v-for="item in visibleItems">
@ -37,7 +37,7 @@
<img
v-else
:src="item.emoji.imageUrl"
>
/>
</span>
</template>
</div>

View File

@ -4,13 +4,9 @@ import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSmileBeam
} from '@fortawesome/free-regular-svg-icons'
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
library.add(
faSmileBeam
)
library.add(faSmileBeam)
/**
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
@ -105,7 +101,7 @@ const EmojiInput = {
default: false
}
},
data () {
data() {
return {
input: undefined,
highlighted: 0,
@ -123,29 +119,34 @@ const EmojiInput = {
EmojiPicker
},
computed: {
padEmoji () {
padEmoji() {
return this.$store.getters.mergedConfig.padEmoji
},
showSuggestions () {
return this.focused &&
showSuggestions() {
return (
this.focused &&
this.suggestions &&
this.suggestions.length > 0 &&
!this.showPicker &&
!this.temporarilyHideSuggestions
)
},
textAtCaret () {
textAtCaret() {
return (this.wordAtCaret || {}).word || ''
},
wordAtCaret () {
wordAtCaret() {
if (this.modelValue && this.caret) {
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
const word =
Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
return word
}
}
},
mounted () {
mounted() {
const { root } = this.$refs
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
const input =
root.querySelector('.emoji-input > input') ||
root.querySelector('.emoji-input > textarea')
if (!input) return
this.input = input
this.resize()
@ -158,7 +159,7 @@ const EmojiInput = {
input.addEventListener('transitionend', this.onTransition)
input.addEventListener('input', this.onInput)
},
unmounted () {
unmounted() {
const { input } = this
if (input) {
input.removeEventListener('blur', this.onBlur)
@ -183,27 +184,28 @@ const EmojiInput = {
// Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return
if (matchedSuggestions.length <= 0) return
this.suggestions = take(matchedSuggestions, 5)
.map(({ imageUrl, ...rest }) => ({
this.suggestions = take(matchedSuggestions, 5).map(
({ imageUrl, ...rest }) => ({
...rest,
img: imageUrl || ''
}))
})
)
},
suggestions: {
handler (newValue) {
handler(newValue) {
this.$nextTick(this.resize)
},
deep: true
}
},
methods: {
focusPickerInput () {
focusPickerInput() {
const pickerEl = this.$refs.picker.$el
if (!pickerEl) return
const pickerInput = pickerEl.querySelector('input')
if (pickerInput) pickerInput.focus()
},
triggerShowPicker () {
triggerShowPicker() {
this.showPicker = true
this.$nextTick(() => {
this.scrollIntoView()
@ -217,7 +219,7 @@ const EmojiInput = {
this.disableClickOutside = false
}, 0)
},
togglePicker () {
togglePicker() {
this.input.focus()
this.showPicker = !this.showPicker
if (this.showPicker) {
@ -225,12 +227,16 @@ const EmojiInput = {
this.$nextTick(this.focusPickerInput)
}
},
replace (replacement) {
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
replace(replacement) {
const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement
)
this.$emit('update:modelValue', newValue)
this.caret = 0
},
insert ({ insertion, keepOpen, surroundingSpace = true }) {
insert({ insertion, keepOpen, surroundingSpace = true }) {
const before = this.modelValue.substring(0, this.caret) || ''
const after = this.modelValue.substring(this.caret) || ''
@ -249,19 +255,25 @@ const EmojiInput = {
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
*/
const isSpaceRegex = /\s/
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
const spaceBefore =
surroundingSpace &&
!isSpaceRegex.exec(before.slice(-1)) &&
before.length &&
this.padEmoji > 0
? ' '
: ''
const spaceAfter =
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
? ' '
: ''
const newValue = [
before,
spaceBefore,
insertion,
spaceAfter,
after
].join('')
const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
''
)
this.keepOpen = keepOpen
this.$emit('update:modelValue', newValue)
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
const position =
this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) {
this.input.focus()
}
@ -273,12 +285,17 @@ const EmojiInput = {
this.caret = position
})
},
replaceText (e, suggestion) {
replaceText(e, suggestion) {
const len = this.suggestions.length || 0
if (len > 0 || suggestion) {
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
const chosenSuggestion =
suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
const newValue = Completion.replaceWord(
this.modelValue,
this.wordAtCaret,
replacement
)
this.$emit('update:modelValue', newValue)
this.highlighted = 0
const position = this.wordAtCaret.start + replacement.length
@ -293,7 +310,7 @@ const EmojiInput = {
e.preventDefault()
}
},
cycleBackward (e) {
cycleBackward(e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted -= 1
@ -305,7 +322,7 @@ const EmojiInput = {
this.highlighted = 0
}
},
cycleForward (e) {
cycleForward(e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted += 1
@ -317,26 +334,28 @@ const EmojiInput = {
this.highlighted = 0
}
},
scrollIntoView () {
scrollIntoView() {
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
*/
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
this.$el.closest('.post-form-modal-view') ||
window
const currentScroll = scrollerRef === window
? scrollerRef.scrollY
: scrollerRef.scrollTop
const scrollerHeight = scrollerRef === window
? scrollerRef.innerHeight
: scrollerRef.offsetHeight
const scrollerRef =
this.$el.closest('.sidebar-scroller') ||
this.$el.closest('.post-form-modal-view') ||
window
const currentScroll =
scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
const scrollerHeight =
scrollerRef === window
? scrollerRef.innerHeight
: scrollerRef.offsetHeight
const scrollerBottomBorder = currentScroll + scrollerHeight
// We check where the bottom border of root element is, this uses findOffset
// to find offset relative to scrollable container (scroller)
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
const rootBottomBorder =
rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
// could also check top delta but there's no case for it
@ -358,10 +377,10 @@ const EmojiInput = {
}
})
},
onTransition (e) {
onTransition(e) {
this.resize()
},
onBlur (e) {
onBlur(e) {
// Clicking on any suggestion removes focus from autocomplete,
// preventing click handler ever executing.
this.blurTimeout = setTimeout(() => {
@ -370,10 +389,10 @@ const EmojiInput = {
this.resize()
}, 200)
},
onClick (e, suggestion) {
onClick(e, suggestion) {
this.replaceText(e, suggestion)
},
onFocus (e) {
onFocus(e) {
if (this.blurTimeout) {
clearTimeout(this.blurTimeout)
this.blurTimeout = null
@ -387,7 +406,7 @@ const EmojiInput = {
this.resize()
this.temporarilyHideSuggestions = false
},
onKeyUp (e) {
onKeyUp(e) {
const { key } = e
this.setCaret(e)
this.resize()
@ -400,11 +419,11 @@ const EmojiInput = {
this.temporarilyHideSuggestions = false
}
},
onPaste (e) {
onPaste(e) {
this.setCaret(e)
this.resize()
},
onKeyDown (e) {
onKeyDown(e) {
const { ctrlKey, shiftKey, key } = e
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
this.insert({ insertion: '\n', surroundingSpace: false })
@ -451,31 +470,31 @@ const EmojiInput = {
this.showPicker = false
this.resize()
},
onInput (e) {
onInput(e) {
this.showPicker = false
this.setCaret(e)
this.resize()
this.$emit('update:modelValue', e.target.value)
},
onClickInput (e) {
onClickInput(e) {
this.showPicker = false
},
onClickOutside (e) {
onClickOutside(e) {
if (this.disableClickOutside) return
this.showPicker = false
},
onStickerUploaded (e) {
onStickerUploaded(e) {
this.showPicker = false
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
onStickerUploadFailed(e) {
this.showPicker = false
this.$emit('sticker-upload-Failed', e)
},
setCaret ({ target: { selectionStart } }) {
setCaret({ target: { selectionStart } }) {
this.caret = selectionStart
},
resize () {
resize() {
const panel = this.$refs.panel
if (!panel) return
const picker = this.$refs.picker.$el
@ -486,9 +505,12 @@ const EmojiInput = {
this.setPlacement(panelBody, panel, offsetBottom)
this.setPlacement(picker, picker, offsetBottom)
},
setPlacement (container, target, offsetBottom) {
setPlacement(container, target, offsetBottom) {
if (!container || !target) return
if (this.placement === 'bottom' || (this.placement === 'auto' && !this.overflowsBottom(container))) {
if (
this.placement === 'bottom' ||
(this.placement === 'auto' && !this.overflowsBottom(container))
) {
target.style.top = offsetBottom + 'px'
target.style.bottom = 'auto'
} else {
@ -496,7 +518,7 @@ const EmojiInput = {
target.style.bottom = this.input.offsetHeight + 'px'
}
},
overflowsBottom (el) {
overflowsBottom(el) {
return el.getBoundingClientRect().bottom > window.innerHeight
}
}

View File

@ -43,11 +43,14 @@
:class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)"
>
<span v-if="!suggestion.mfm" class="image">
<span
v-if="!suggestion.mfm"
class="image"
>
<img
v-if="suggestion.img"
:src="suggestion.img"
>
/>
<span v-else>{{ suggestion.replacement }}</span>
</span>
<div class="label">
@ -78,7 +81,7 @@
position: absolute;
top: 0;
right: 0;
margin: .2em .25em;
margin: 0.2em 0.25em;
font-size: 1.3em;
cursor: pointer;
line-height: 24px;
@ -94,7 +97,7 @@
margin-top: 2px;
&.hide {
display: none
display: none;
}
}
@ -105,7 +108,7 @@
margin-top: 2px;
&.hide {
display: none
display: none;
}
&-body {
@ -179,7 +182,8 @@
}
}
input, textarea {
input,
textarea {
flex: 1 0 auto;
}
}

View File

@ -1,5 +1,26 @@
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
const MFM_TAGS = [
'blur',
'bounce',
'flip',
'font',
'jelly',
'jump',
'rainbow',
'rotate',
'shake',
'sparkle',
'spin',
'tada',
'twitch',
'x2',
'x3',
'x4'
].map((tag) => ({
displayText: tag,
detailText: '$[' + tag + ' ]',
replacement: '$[' + tag + ' ]',
mfm: true
}))
/**
* suggest - generates a suggestor function to be used by emoji-input
@ -13,10 +34,10 @@ const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow',
* doesn't support user linking you can just provide only emoji.
*/
export default data => {
export default (data) => {
const emojiCurry = suggestEmoji(data.emoji)
const usersCurry = data.store && suggestUsers(data.store)
return input => {
return (input) => {
const firstChar = input[0]
if (firstChar === ':' && data.emoji) {
return emojiCurry(input)
@ -25,14 +46,15 @@ export default data => {
return usersCurry(input)
}
if (firstChar === '$') {
return MFM_TAGS
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
return MFM_TAGS.filter(
({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1
)
}
return []
}
}
export const suggestEmoji = emojis => input => {
export const suggestEmoji = (emojis) => (input) => {
const noPrefix = input.toLowerCase().substr(1)
return emojis
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
@ -85,7 +107,7 @@ export const suggestUsers = ({ dispatch, state }) => {
})
}
return async input => {
return async (input) => {
const noPrefix = input.toLowerCase().substr(1)
if (previousQuery === noPrefix) return suggestions
@ -99,36 +121,47 @@ export const suggestUsers = ({ dispatch, state }) => {
await debounceUserSearch(noPrefix)
}
const newSuggestions = state.users.users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
).slice(0, 20).sort((a, b) => {
let aScore = 0
let bScore = 0
const newSuggestions = state.users.users
.filter(
(user) =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
)
.slice(0, 20)
.sort((a, b) => {
let aScore = 0
let bScore = 0
// Matches on screen name (i.e. user@instance) makes a priority
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
// Matches on screen name (i.e. user@instance) makes a priority
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
// Matches on name takes second priority
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
// Matches on name takes second priority
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
const diff = (bScore - aScore) * 10
const diff = (bScore - aScore) * 10
// Then sort alphabetically
const nameAlphabetically = a.name > b.name ? 1 : -1
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
// Then sort alphabetically
const nameAlphabetically = a.name > b.name ? 1 : -1
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
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 + ' '
}))
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 + ' '
})
)
/* eslint-enable camelcase */
suggestions = newSuggestions || []

View File

@ -9,11 +9,7 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import { trim, escapeRegExp, startCase } from 'lodash'
library.add(
faBoxOpen,
faStickyNote,
faSmileBeam
)
library.add(faBoxOpen, faStickyNote, faSmileBeam)
const EmojiPicker = {
props: {
@ -28,7 +24,7 @@ const EmojiPicker = {
default: false
}
},
data () {
data() {
return {
keyword: '',
activeGroup: 'standard',
@ -37,66 +33,71 @@ const EmojiPicker = {
}
},
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
StickerPicker: defineAsyncComponent(() =>
import('../sticker_picker/sticker_picker.vue')
),
Checkbox,
EmojiGrid
},
methods: {
onStickerUploaded (e) {
onStickerUploaded(e) {
this.$emit('sticker-uploaded', e)
},
onStickerUploadFailed (e) {
onStickerUploadFailed(e) {
this.$emit('sticker-upload-failed', e)
},
onEmoji (emoji) {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
onEmoji(emoji) {
const value = emoji.imageUrl
? `:${emoji.displayText}:`
: emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
this.$store.commit('emojiUsed', emoji)
},
onWheel (e) {
onWheel(e) {
e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
},
highlight (key) {
highlight(key) {
this.setShowStickers(false)
this.activeGroup = key
if (this.keyword.length) {
this.$refs.emojiGrid.scrollToItem(key)
}
},
onActiveGroup (group) {
onActiveGroup(group) {
this.activeGroup = group
},
toggleStickers () {
toggleStickers() {
this.showingStickers = !this.showingStickers
},
setShowStickers (value) {
setShowStickers(value) {
this.showingStickers = value
},
filterByKeyword (list) {
filterByKeyword(list) {
if (this.keyword === '') return list
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
return list.filter(emoji => {
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
return list.filter((emoji) => {
return (
regex.test(emoji.displayText) ||
(!emoji.imageUrl && emoji.replacement === this.keyword)
)
})
}
},
computed: {
activeGroupView () {
activeGroupView() {
return this.showingStickers ? '' : this.activeGroup
},
stickersAvailable () {
stickersAvailable() {
if (this.$store.state.instance.stickers) {
return this.$store.state.instance.stickers.length > 0
}
return 0
},
filteredEmoji () {
return this.filterByKeyword(
this.$store.state.instance.customEmoji || []
)
filteredEmoji() {
return this.filterByKeyword(this.$store.state.instance.customEmoji || [])
},
emojis () {
emojis() {
const recentEmojis = this.$store.getters.recentEmojis
const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji
@ -115,7 +116,7 @@ const EmojiPicker = {
text: this.$t('emoji.recent'),
first: {
imageUrl: '',
replacement: '🕒',
replacement: '🕒'
},
emojis: this.filterByKeyword(recentEmojis)
},
@ -130,7 +131,7 @@ const EmojiPicker = {
}
].concat(emojiPacks)
},
sortedEmoji () {
sortedEmoji() {
const customEmojis = this.$store.state.instance.customEmoji || []
const sortedEmojiGroups = new Map()
customEmojis.forEach((emoji) => {
@ -142,19 +143,22 @@ const EmojiPicker = {
})
return new Map([...sortedEmojiGroups.entries()].sort())
},
emojisView () {
emojisView() {
if (this.keyword === '') {
return this.emojis.filter(pack => {
return this.emojis.filter((pack) => {
return pack.id === this.activeGroup
})
} else {
return this.emojis.filter(pack => {
return this.emojis.filter((pack) => {
return pack.emojis.length > 0
})
}
},
stickerPickerEnabled () {
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
stickerPickerEnabled() {
return (
(this.$store.state.instance.stickers || []).length !== 0 &&
this.enableStickerPicker
)
}
}
}

View File

@ -3,7 +3,14 @@
// The worst query selector ever
// selects ONLY emojis pickers in replies in notifications
// who thought this was a good idea?
.notification > .Status > .status-container > .post-status-form > form > .form-group > .emoji-input > .emoji-picker {
.notification
> .Status
> .status-container
> .post-status-form
> form
> .form-group
> .emoji-input
> .emoji-picker {
max-width: 100%;
left: 0;
@media (min-width: 1300px) {
@ -111,7 +118,7 @@
justify-content: center;
width: 32px;
height: 32px;
padding: .4em;
padding: 0.4em;
cursor: pointer;
img {
@ -144,7 +151,7 @@
}
.sticker-picker {
flex: 1 1 auto
flex: 1 1 auto;
}
.stickers,
@ -170,5 +177,65 @@
input {
width: 100%;
}
&-groups {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
}
&-group {
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 5px;
justify-content: left;
&-title {
font-size: 0.85em;
width: 100%;
margin: 0;
&.disabled {
display: none;
}
}
}
&-item {
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
}
}

View File

@ -17,16 +17,18 @@
:title="group.text"
@click.prevent="highlight(group.id)"
>
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
<span v-if="!group.first.imageUrl">{{
group.first.replacement
}}</span>
<img
v-else
:src="group.first.imageUrl"
>
/>
</span>
<span
v-if="stickerPickerEnabled"
class="stickers-tab-icon emoji-tabs-item"
:class="{active: showingStickers}"
:class="{ active: showingStickers }"
:title="$t('emoji.stickers')"
@click.prevent="toggleStickers"
>
@ -40,7 +42,7 @@
<div class="content">
<div
class="emoji-content"
:class="{hidden: showingStickers}"
:class="{ hidden: showingStickers }"
>
<div class="emoji-search">
<input
@ -49,7 +51,7 @@
class="form-control"
:placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false"
>
/>
</div>
<EmojiGrid
ref="emojiGrid"
@ -61,6 +63,34 @@
v-if="showKeepOpen"
class="keep-open"
>
<div
v-for="group in emojisView"
:key="group.id"
class="emoji-group"
>
<h6
:ref="'group-' + group.id"
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
/>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<div class="keep-open">
<Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }}
</Checkbox>

View File

@ -5,7 +5,7 @@ const EMOJI_REACTION_COUNT_CUTOFF = 12
const findEmojiByReplacement = (state, replacement) => {
const allEmojis = state.instance.emoji.concat(state.instance.customEmoji)
return allEmojis.find(emoji => emoji.replacement === replacement)
return allEmojis.find((emoji) => emoji.replacement === replacement)
}
const EmojiReactions = {
@ -19,18 +19,20 @@ const EmojiReactions = {
showAll: false
}),
computed: {
tooManyReactions () {
tooManyReactions() {
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
},
emojiReactions () {
emojiReactions() {
return this.showAll
? this.status.emoji_reactions
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
},
showMoreString () {
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
showMoreString() {
return `+${
this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF
}`
},
accountsForEmoji () {
accountsForEmoji() {
return this.status.emoji_reactions.reduce((acc, reaction) => {
if (reaction.url) {
acc[reaction.url] = reaction.accounts || []
@ -40,32 +42,32 @@ const EmojiReactions = {
return acc
}, {})
},
loggedIn () {
loggedIn() {
return !!this.$store.state.users.currentUser
}
},
methods: {
toggleShowAll () {
toggleShowAll() {
this.showAll = !this.showAll
},
reactedWith (emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me
reactedWith(emoji) {
return this.status.emoji_reactions.find((r) => r.name === emoji).me
},
fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
fetchEmojiReactionsByIfMissing() {
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
if (hasNoAccounts) {
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
}
},
reactWith (emoji) {
reactWith(emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
const emojiObject = findEmojiByReplacement(this.$store.state, emoji)
this.$store.commit('emojiUsed', emojiObject)
},
unreact (emoji) {
unreact(emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
},
emojiOnClick (emoji, event) {
emojiOnClick(emoji, event) {
if (!this.loggedIn) return
if (this.reactedWith(emoji)) {

View File

@ -1,28 +1,35 @@
<template>
<div class="emoji-reactions">
<UserListPopover
v-for="(reaction) in emojiReactions"
v-for="reaction in emojiReactions"
:key="reaction.url || reaction.name"
:users="accountsForEmoji[reaction.url || reaction.name]"
>
<button
class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
:class="{
'picked-reaction': reactedWith(reaction.name),
'not-clickable': !loggedIn
}"
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<span
v-if="reaction.url !== null"
class="emoji-button-inner"
>
<img
:src="reaction.url"
:title="reaction.name"
class="reaction-emoji"
height="2.55em"
>
width="2.55em"
/>
{{ reaction.count }}
</span>
<span v-else>
<span
v-else
class="emoji-button-inner"
>
<span class="reaction-emoji unicode-emoji">
{{ reaction.name }}
</span>
@ -41,7 +48,7 @@
</div>
</template>
<script src="./emoji_reactions.js" ></script>
<script src="./emoji_reactions.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@ -53,7 +60,7 @@
}
.unicode-emoji {
font-size: 210%;
font-size: 128%;
}
.emoji-reaction {
@ -61,6 +68,7 @@
margin-right: 0.5em;
margin-top: 0.5em;
display: flex;
height: 28px;
align-items: center;
justify-content: center;
box-sizing: border-box;
@ -70,6 +78,10 @@
height: 2.55em !important;
margin-right: 0.25em;
}
img.reaction-emoji {
width: 1.55em !important;
display: block;
}
&:focus {
outline: none;
}
@ -96,9 +108,12 @@
}
.button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
}
background: none;
padding: 1px 0.5em;
.emoji-button-inner {
display: flex;
align-items: center;
}
}
</style>

View File

@ -1,9 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
)
library.add(faCircleNotch)
const Exporter = {
props: {
@ -18,26 +16,30 @@ const Exporter = {
exportButtonLabel: { type: String },
processingMessage: { type: String }
},
data () {
data() {
return {
processing: false
}
},
methods: {
process () {
process() {
this.processing = true
this.getContent()
.then((content) => {
const fileToDownload = document.createElement('a')
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
fileToDownload.setAttribute('download', this.filename)
fileToDownload.style.display = 'none'
document.body.appendChild(fileToDownload)
fileToDownload.click()
document.body.removeChild(fileToDownload)
// Add delay before hiding processing state since browser takes some time to handle file download
setTimeout(() => { this.processing = false }, 2000)
})
this.getContent().then((content) => {
const fileToDownload = document.createElement('a')
fileToDownload.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(content)
)
fileToDownload.setAttribute('download', this.filename)
fileToDownload.style.display = 'none'
document.body.appendChild(fileToDownload)
fileToDownload.click()
document.body.removeChild(fileToDownload)
// Add delay before hiding processing state since browser takes some time to handle file download
setTimeout(() => {
this.processing = false
}, 2000)
})
}
}
}

View File

@ -36,7 +36,7 @@ const ExtraButtons = {
Popover,
ConfirmModal
},
data () {
data() {
return {
expanded: false,
showingDeleteDialog: false,
@ -44,155 +44,206 @@ const ExtraButtons = {
}
},
methods: {
deleteStatus () {
deleteStatus() {
if (this.shouldConfirmDelete) {
this.showDeleteStatusConfirmDialog()
} else {
this.doDeleteStatus()
}
},
doDeleteStatus () {
doDeleteStatus() {
this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog()
},
showDeleteStatusConfirmDialog () {
showDeleteStatusConfirmDialog() {
this.showingDeleteDialog = true
},
hideDeleteStatusConfirmDialog () {
hideDeleteStatusConfirmDialog() {
this.showingDeleteDialog = false
},
translateStatus () {
translateStatus() {
if (this.noTranslationTargetSet) {
this.$store.dispatch('pushGlobalNotice', { messageKey: 'toast.no_translation_target_set', level: 'info' })
this.$store.dispatch('pushGlobalNotice', {
messageKey: 'toast.no_translation_target_set',
level: 'info'
})
}
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo })
const translateTo =
this.$store.getters.mergedConfig.translationLanguage ||
this.$store.state.instance.interfaceLanguage
this.$store
.dispatch('translateStatus', {
id: this.status.id,
language: translateTo
})
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
pinStatus () {
this.$store.dispatch('pinStatus', this.status.id)
pinStatus() {
this.$store
.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
unpinStatus () {
this.$store.dispatch('unpinStatus', this.status.id)
unpinStatus() {
this.$store
.dispatch('unpinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
muteConversation () {
this.$store.dispatch('muteConversation', this.status.id)
muteConversation() {
this.$store
.dispatch('muteConversation', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
unmuteConversation () {
this.$store.dispatch('unmuteConversation', this.status.id)
unmuteConversation() {
this.$store
.dispatch('unmuteConversation', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
copyLink () {
navigator.clipboard.writeText(this.statusLink)
copyLink() {
navigator.clipboard
.writeText(this.statusLink)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
bookmarkStatus () {
this.$store.dispatch('bookmark', { id: this.status.id })
bookmarkStatus() {
this.$store
.dispatch('bookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
unbookmarkStatus () {
this.$store.dispatch('unbookmark', { id: this.status.id })
unbookmarkStatus() {
this.$store
.dispatch('unbookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.catch((err) => this.$emit('onError', err.error.error))
},
reportStatus () {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
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
}))
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 () {
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])
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)
},
redraftStatus () {
redraftStatus() {
if (this.shouldConfirmDelete) {
this.showRedraftStatusConfirmDialog()
} else {
this.doRedraftStatus()
}
},
doRedraftStatus () {
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
.then(data => this.$store.dispatch('openPostStatusModal', {
isRedraft: true,
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type
}))
doRedraftStatus() {
this.$store
.dispatch('fetchStatusSource', { id: this.status.id })
.then((data) =>
this.$store.dispatch('openPostStatusModal', {
isRedraft: true,
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type
})
)
this.doDeleteStatus()
},
showRedraftStatusConfirmDialog () {
showRedraftStatusConfirmDialog() {
this.showingRedraftDialog = true
},
hideRedraftStatusConfirmDialog () {
hideRedraftStatusConfirmDialog() {
this.showingRedraftDialog = false
}
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
canDelete () {
if (!this.currentUser) { return }
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
currentUser() {
return this.$store.state.users.currentUser
},
canDelete() {
if (!this.currentUser) {
return
}
const superuser =
this.currentUser.rights.moderator || this.currentUser.rights.admin
return superuser || this.status.user.id === this.currentUser.id
},
ownStatus () {
ownStatus() {
return this.status.user.id === this.currentUser.id
},
canPin () {
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
canPin() {
return (
this.ownStatus &&
(this.status.visibility === 'public' ||
this.status.visibility === 'unlisted')
)
},
canMute () {
canMute() {
return !!this.currentUser
},
canTranslate () {
canTranslate() {
return this.$store.state.instance.translationEnabled === true
},
noTranslationTargetSet () {
noTranslationTargetSet() {
return this.$store.getters.mergedConfig.translationLanguage === undefined
},
statusLink () {
statusLink() {
if (this.status.is_local) {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
return `${this.$store.state.instance.server}${
this.$router.resolve({
name: 'conversation',
params: { id: this.status.id }
}).href
}`
} else {
return this.status.external_url
}
},
shouldConfirmDelete () {
shouldConfirmDelete() {
return this.$store.getters.mergedConfig.modalOnDelete
},
isEdited () {
isEdited() {
return this.status.edited_at !== null
},
editingAvailable () { return this.$store.state.instance.editingAvailable },
editingAvailable() {
return this.$store.state.instance.editingAvailable
}
}
}

View File

@ -7,7 +7,7 @@
:bound-to="{ x: 'container' }"
remove-padding
>
<template v-slot:content="{close}">
<template #content="{ close }">
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
@ -17,7 +17,7 @@
<FAIcon
fixed-width
icon="eye-slash"
/><span>{{ $t("status.mute_conversation") }}</span>
/><span>{{ $t('status.mute_conversation') }}</span>
</button>
<button
v-if="canMute && status.thread_muted"
@ -27,7 +27,7 @@
<FAIcon
fixed-width
icon="eye-slash"
/><span>{{ $t("status.unmute_conversation") }}</span>
/><span>{{ $t('status.unmute_conversation') }}</span>
</button>
<button
v-if="!status.pinned && canPin"
@ -38,7 +38,7 @@
<FAIcon
fixed-width
icon="thumbtack"
/><span>{{ $t("status.pin") }}</span>
/><span>{{ $t('status.pin') }}</span>
</button>
<button
v-if="status.pinned && canPin"
@ -49,7 +49,7 @@
<FAIcon
fixed-width
icon="thumbtack"
/><span>{{ $t("status.unpin") }}</span>
/><span>{{ $t('status.unpin') }}</span>
</button>
<button
v-if="!status.bookmarked"
@ -60,7 +60,7 @@
<FAIcon
fixed-width
:icon="['far', 'bookmark']"
/><span>{{ $t("status.bookmark") }}</span>
/><span>{{ $t('status.bookmark') }}</span>
</button>
<button
v-if="status.bookmarked"
@ -71,7 +71,7 @@
<FAIcon
fixed-width
icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span>
/><span>{{ $t('status.unbookmark') }}</span>
</button>
<button
v-if="ownStatus && editingAvailable"
@ -82,7 +82,7 @@
<FAIcon
fixed-width
icon="pen"
/><span>{{ $t("status.edit") }}</span>
/><span>{{ $t('status.edit') }}</span>
</button>
<button
v-if="isEdited && editingAvailable"
@ -93,7 +93,7 @@
<FAIcon
fixed-width
icon="history"
/><span>{{ $t("status.edit_history") }}</span>
/><span>{{ $t('status.edit_history') }}</span>
</button>
<button
v-if="ownStatus"
@ -104,7 +104,7 @@
<FAIcon
fixed-width
icon="file-pen"
/><span>{{ $t("status.redraft") }}</span>
/><span>{{ $t('status.redraft') }}</span>
</button>
<button
v-if="canDelete"
@ -115,7 +115,7 @@
<FAIcon
fixed-width
icon="times"
/><span>{{ $t("status.delete") }}</span>
/><span>{{ $t('status.delete') }}</span>
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@ -125,7 +125,7 @@
<FAIcon
fixed-width
icon="share-alt"
/><span>{{ $t("status.copy_link") }}</span>
/><span>{{ $t('status.copy_link') }}</span>
</button>
<a
v-if="!status.is_local"
@ -137,7 +137,7 @@
<FAIcon
fixed-width
icon="external-link-alt"
/><span>{{ $t("status.external_source") }}</span>
/><span>{{ $t('status.external_source') }}</span>
</a>
<button
class="button-default dropdown-item dropdown-item-icon"
@ -147,7 +147,7 @@
<FAIcon
fixed-width
:icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span>
/><span>{{ $t('user_card.report') }}</span>
</button>
<button
v-if="canTranslate"
@ -158,7 +158,7 @@
<FAIcon
fixed-width
icon="globe"
/><span>{{ $t("status.translate") }}</span>
/><span>{{ $t('status.translate') }}</span>
<template v-if="noTranslationTargetSet">
<span class="dropdown-item-icon__badge warning">
@ -172,7 +172,7 @@
</button>
</div>
</template>
<template v-slot:trigger>
<template #trigger>
<button class="button-unstyled popover-trigger">
<FAIcon
class="fa-scale-110 fa-old-padding"
@ -205,7 +205,7 @@
</Popover>
</template>
<script src="./extra_buttons.js" ></script>
<script src="./extra_buttons.js"></script>
<style lang="scss">
@import '../../_variables.scss';

View File

@ -1,24 +1,19 @@
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faStar } from '@fortawesome/free-solid-svg-icons'
import {
faStar as faStarRegular
} from '@fortawesome/free-regular-svg-icons'
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons'
library.add(
faStar,
faStarRegular
)
library.add(faStar, faStarRegular)
const FavoriteButton = {
props: ['status', 'loggedIn'],
data () {
data() {
return {
animated: false
}
},
methods: {
favorite () {
favorite() {
if (!this.status.favorited) {
this.$store.dispatch('favorite', { id: this.status.id })
} else {
@ -32,8 +27,10 @@ const FavoriteButton = {
},
computed: {
...mapGetters(['mergedConfig']),
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
remoteInteractionLink() {
return this.$store.getters.remoteInteractionLink({
statusId: this.status.id
})
}
}
}

View File

@ -35,7 +35,7 @@
</div>
</template>
<script src="./favorite_button.js" ></script>
<script src="./favorite_button.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@ -56,6 +56,7 @@
.interactive {
.svg-inline--fa {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}
&:hover .svg-inline--fa,

View File

@ -2,10 +2,20 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
const FeaturesPanel = {
computed: {
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
textlimit: function () { return this.$store.state.instance.textlimit },
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
whoToFollow: function () {
return this.$store.state.instance.suggestionsEnabled
},
mediaProxy: function () {
return this.$store.state.instance.mediaProxyAvailable
},
textlimit: function () {
return this.$store.state.instance.textlimit
},
uploadlimit: function () {
return fileSizeFormatService.fileSizeFormat(
this.$store.state.instance.uploadlimit
)
}
}
}

View File

@ -16,17 +16,20 @@
</li>
<li>{{ $t('features_panel.scope_options') }}</li>
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li>
<li>
{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }}
{{ $t('upload.file_size_units.' + uploadlimit.unit) }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script src="./features_panel.js" ></script>
<script src="./features_panel.js"></script>
<style lang="scss">
.features-panel li {
line-height: 24px;
}
.features-panel li {
line-height: 24px;
}
</style>

View File

@ -5,14 +5,11 @@ import {
faExclamationTriangle
} from '@fortawesome/free-solid-svg-icons'
library.add(
faStop,
faExclamationTriangle
)
library.add(faStop, faExclamationTriangle)
const Flash = {
props: [ 'src' ],
data () {
props: ['src'],
data() {
return {
player: false, // can be true, "hidden", false. hidden = element exists
loaded: false,
@ -20,7 +17,7 @@ const Flash = {
}
},
methods: {
openPlayer () {
openPlayer() {
if (this.player) return // prevent double-loading, or re-loading on failure
this.player = 'hidden'
RuffleService.getRuffle().then((ruffle) => {
@ -32,17 +29,20 @@ const Flash = {
container.appendChild(player)
player.style.width = '100%'
player.style.height = '100%'
player.load(this.src).then(() => {
this.player = true
}).catch((e) => {
console.error('Error loading ruffle', e)
this.player = 'error'
})
player
.load(this.src)
.then(() => {
this.player = true
})
.catch((e) => {
console.error('Error loading ruffle', e)
this.player = 'error'
})
this.ruffleInstance = player
this.$emit('playerOpened')
})
},
closePlayer () {
closePlayer() {
this.ruffleInstance && this.ruffleInstance.remove()
this.player = false
this.$emit('playerClosed')

View File

@ -1,24 +1,27 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import {
requestFollow,
requestUnfollow
} from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: {
ConfirmModal
},
data () {
data() {
return {
inProgress: false,
showingConfirmUnfollow: false
}
},
computed: {
shouldConfirmUnfollow () {
shouldConfirmUnfollow() {
return this.$store.getters.mergedConfig.modalOnUnfollow
},
isPressed () {
isPressed() {
return this.inProgress || this.relationship.following
},
title () {
title() {
if (this.inProgress || this.relationship.following) {
return this.$t('user_card.follow_unfollow')
} else if (this.relationship.requested) {
@ -27,7 +30,7 @@ export default {
return this.$t('user_card.follow')
}
},
label () {
label() {
if (this.inProgress) {
return this.$t('user_card.follow_progress')
} else if (this.relationship.following) {
@ -38,39 +41,44 @@ export default {
return this.$t('user_card.follow')
}
},
disabled () {
disabled() {
return this.inProgress || this.user.deactivated
}
},
methods: {
showConfirmUnfollow () {
showConfirmUnfollow() {
this.showingConfirmUnfollow = true
},
hideConfirmUnfollow () {
hideConfirmUnfollow() {
this.showingConfirmUnfollow = false
},
onClick () {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
onClick() {
this.relationship.following || this.relationship.requested
? this.unfollow()
: this.follow()
},
follow () {
follow() {
this.inProgress = true
requestFollow(this.relationship.id, this.$store).then(() => {
this.inProgress = false
})
},
unfollow () {
unfollow() {
if (this.shouldConfirmUnfollow) {
this.showConfirmUnfollow()
} else {
this.doUnfollow()
}
},
doUnfollow () {
doUnfollow() {
const store = this.$store
this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
store.commit('removeStatus', {
timeline: 'friends',
userId: this.relationship.id
})
})
this.hideConfirmUnfollow()

View File

@ -21,9 +21,7 @@
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
<span v-text="user.screen_name_ui" />
</template>
</i18n-t>
</confirm-modal>

View File

@ -4,10 +4,7 @@ import FollowButton from '../follow_button/follow_button.vue'
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
const FollowCard = {
props: [
'user',
'noFollowsYou'
],
props: ['user', 'noFollowsYou'],
components: {
BasicUserCard,
RemoteFollow,
@ -15,13 +12,13 @@ const FollowCard = {
RemoveFollowerButton
},
computed: {
isMe () {
isMe() {
return this.$store.state.users.currentUser.id === this.user.id
},
loggedIn () {
loggedIn() {
return this.$store.state.users.currentUser
},
relationship () {
relationship() {
return this.$store.getters.relationship(this.user.id)
}
}

View File

@ -8,39 +8,41 @@ const FollowRequestCard = {
BasicUserCard,
ConfirmModal
},
data () {
data() {
return {
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
},
methods: {
findFollowRequestNotificationId () {
findFollowRequestNotificationId() {
const notif = notificationsFromStore(this.$store).find(
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
(notif) =>
notif.from_profile.id === this.user.id &&
notif.type === 'follow_request'
)
return notif && notif.id
},
showApproveConfirmDialog () {
showApproveConfirmDialog() {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
hideApproveConfirmDialog() {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
showDenyConfirmDialog() {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
hideDenyConfirmDialog() {
this.showingDenyConfirmDialog = false
},
approveUser () {
approveUser() {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
doApprove() {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('decrementFollowRequestsCount')
@ -49,22 +51,23 @@ const FollowRequestCard = {
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
this.$store.dispatch('updateNotification', {
id: notifId,
updater: notification => {
updater: (notification) => {
notification.type = 'follow'
}
})
this.hideApproveConfirmDialog()
},
denyUser () {
denyUser() {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
doDeny() {
const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
this.$store.state.api.backendInteractor
.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('decrementFollowRequestsCount')
@ -74,17 +77,19 @@ const FollowRequestCard = {
}
},
computed: {
mergedConfig () {
mergedConfig() {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
shouldConfirmDeny() {
return this.mergedConfig.modalOnDenyFollow
},
show () {
const notifId = this.$store.state.api.followRequests.find(req => req.id === this.user.id)
show() {
const notifId = this.$store.state.api.followRequests.find(
(req) => req.id === this.user.id
)
return notifId !== undefined
}

View File

@ -1,5 +1,8 @@
<template>
<basic-user-card :user="user" v-if="show">
<basic-user-card
v-if="show"
:user="user"
>
<div class="follow-request-card-content-container">
<button
class="btn button-default"

View File

@ -5,12 +5,14 @@ import get from 'lodash/get'
const FollowRequestList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFollowRequests'),
select: (props, $store) => get($store.state.api, 'followRequests', []).map(req => $store.getters.findUser(req.id)),
select: (props, $store) =>
get($store.state.api, 'followRequests', []).map((req) =>
$store.getters.findUser(req.id)
),
destroy: (props, $store) => $store.dispatch('clearFollowRequests'),
childPropName: 'items',
additionalPropNames: ['userId']
})(List);
})(List)
const FollowRequests = {
components: {
@ -18,10 +20,10 @@ const FollowRequests = {
FollowRequestList
},
computed: {
userId () {
userId() {
return this.$store.state.users.currentUser.id
},
requests () {
requests() {
return this.$store.state.api.followRequests
}
}

View File

@ -7,7 +7,7 @@
</div>
<div class="panel-body">
<FollowRequestList :user-id="userId">
<template #item="{item}">
<template #item="{ item }">
<FollowRequestCard :user="item" />
</template>
</FollowRequestList>

View File

@ -1,7 +1,7 @@
<template>
<div class="followed-tag-card">
<span>
<router-link :to="{ name: 'tag-timeline', params: {tag: tag.name}}">
<router-link :to="{ name: 'tag-timeline', params: { tag: tag.name } }">
<span class="tag-link">#{{ tag.name }}</span>
</router-link>
<span class="unfollow-tag">
@ -33,22 +33,22 @@ export default {
tag: {
type: Object,
required: true
},
}
},
// this is a hack to update the state of the button
// for some reason, List does not update on changes to the tag object
data: () => ({
isFollowing: true
}),
mounted () {
mounted() {
this.isFollowing = this.tag.following
},
methods: {
unfollowTag (tag) {
unfollowTag(tag) {
this.$store.dispatch('unfollowTag', tag)
this.isFollowing = false
},
followTag (tag) {
followTag(tag) {
this.$store.dispatch('followTag', tag)
this.isFollowing = true
}
@ -57,21 +57,22 @@ export default {
</script>
<style scoped>
.followed-tag-card {
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.unfollow-tag {
position: absolute;
right: 1rem;
}
.followed-tag-card {
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.unfollow-tag {
position: absolute;
right: 1rem;
}
.tag-link {
font-size: large;
}
.tag-link {
font-size: large;
}
.unfollow-tag-button, .follow-tag-button {
font-size: medium;
}
.unfollow-tag-button,
.follow-tag-button {
font-size: medium;
}
</style>

View File

@ -5,11 +5,9 @@ export default {
components: {
Select
},
props: [
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
],
props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
emits: ['update:modelValue'],
data () {
data() {
return {
lValue: this.modelValue,
availableOptions: [
@ -19,43 +17,45 @@ export default {
'serif',
'monospace',
'sans-serif'
].filter(_ => _)
].filter((_) => _)
}
},
beforeUpdate () {
beforeUpdate() {
this.lValue = this.modelValue
},
computed: {
present () {
present() {
return typeof this.lValue !== 'undefined'
},
dValue () {
dValue() {
return this.lValue || this.fallback || {}
},
family: {
get () {
get() {
return this.dValue.family
},
set (v) {
set(v) {
set(this.lValue, 'family', v)
this.$emit('update:modelValue', this.lValue)
}
},
isCustom () {
isCustom() {
return this.preset === 'custom'
},
preset: {
get () {
if (this.family === 'serif' ||
this.family === 'sans-serif' ||
this.family === 'monospace' ||
this.family === 'inherit') {
get() {
if (
this.family === 'serif' ||
this.family === 'sans-serif' ||
this.family === 'monospace' ||
this.family === 'inherit'
) {
return this.family
} else {
return 'custom'
}
},
set (v) {
set(v) {
this.family = v === 'custom' ? '' : v
}
}

View File

@ -15,8 +15,13 @@
class="opt exlcude-disabled"
type="checkbox"
:checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
@change="
$emit(
'update:modelValue',
typeof modelValue === 'undefined' ? fallback : undefined
)
"
/>
<label
v-if="typeof fallback !== 'undefined'"
class="opt-l"
@ -43,11 +48,11 @@
v-model="family"
class="custom-font"
type="text"
>
/>
</div>
</template>
<script src="./font_control.js" ></script>
<script src="./font_control.js"></script>
<style lang="scss">
@import '../../_variables.scss';

View File

@ -4,7 +4,9 @@ const FriendsTimeline = {
Timeline
},
computed: {
timeline () { return this.$store.state.statuses.timelines.friends }
timeline() {
return this.$store.state.statuses.timelines.friends
}
}
}

View File

@ -17,7 +17,7 @@ const Gallery = {
'editAttachment',
'grid'
],
data () {
data() {
return {
sizes: {},
hidingLong: true
@ -25,42 +25,61 @@ const Gallery = {
},
components: { Attachment },
computed: {
rows () {
rows() {
if (!this.attachments) {
return []
}
const attachments = this.limit > 0
? this.attachments.slice(0, this.limit)
: this.attachments
const attachments =
this.limit > 0
? this.attachments.slice(0, this.limit)
: this.attachments
if (this.size === 'hide') {
return attachments.map(item => ({ minimal: true, items: [item] }))
return attachments.map((item) => ({ minimal: true, items: [item] }))
}
const rows = this.grid
? [{ grid: true, items: attachments }]
: attachments.reduce((acc, attachment, i) => {
if (attachment.mimetype.includes('audio')) {
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
}
if (!(
attachment.mimetype.includes('image') ||
attachment.mimetype.includes('video') ||
attachment.mimetype.includes('flash')
)) {
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
}
const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items
currentRow.push(attachment)
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
return [...acc, { items: [] }]
} else {
return acc
}
}, [{ items: [] }]).filter(_ => _.items.length > 0)
: attachments
.reduce(
(acc, attachment, i) => {
if (attachment.mimetype.includes('audio')) {
return [
...acc,
{ audio: true, items: [attachment] },
{ items: [] }
]
}
if (
!(
attachment.mimetype.includes('image') ||
attachment.mimetype.includes('video') ||
attachment.mimetype.includes('flash')
)
) {
return [
...acc,
{ minimal: true, items: [attachment] },
{ items: [] }
]
}
const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items
currentRow.push(attachment)
if (
currentRow.length >= maxPerRow &&
attachmentsRemaining > maxPerRow
) {
return [...acc, { items: [] }]
} else {
return acc
}
},
[{ items: [] }]
)
.filter((_) => _.items.length > 0)
return rows
},
attachmentsDimensionalScore () {
attachmentsDimensionalScore() {
return this.rows.reduce((acc, row) => {
let size = 0
if (row.minimal) {
@ -73,7 +92,7 @@ const Gallery = {
return acc + size
}, 0)
},
tooManyAttachments () {
tooManyAttachments() {
if (this.editable || this.size === 'small') {
return false
} else if (this.size === 'hide') {
@ -84,32 +103,32 @@ const Gallery = {
}
},
methods: {
onNaturalSizeLoad ({ id, width, height }) {
onNaturalSizeLoad({ id, width, height }) {
set(this.sizes, id, { width, height })
},
rowStyle (row) {
rowStyle(row) {
if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
return { 'padding-bottom': `${100 / (row.items.length + 0.6)}%` }
}
},
itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id))
itemStyle(id, row) {
const total = sumBy(row, (item) => this.getAspectRatio(item.id))
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
},
getAspectRatio (id) {
getAspectRatio(id) {
const size = this.sizes[id]
return size ? size.width / size.height : 1
},
toggleHidingLong (event) {
toggleHidingLong(event) {
this.hidingLong = event
},
openGallery () {
openGallery() {
this.$store.dispatch('setMedia', this.attachments)
this.$store.dispatch('setCurrentMedia', this.attachments[0])
},
onMedia () {
onMedia() {
this.$store.dispatch('setMedia', this.attachments)
}
}

View File

@ -25,11 +25,20 @@
:size="size"
:editable="editable"
:remove="removeAttachment"
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
:shift-up="
!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment
"
:shift-dn="
!(
attachmentIndex === row.items.length - 1 &&
rowIndex === rows.length - 1
) && shiftDnAttachment
"
:edit="editAttachment"
:description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
:hide-description="
size === 'small' || (tooManyAttachments && hidingLong)
"
:style="itemStyle(attachment.id, row.items)"
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
@ -42,7 +51,7 @@
class="many-attachments"
>
<div class="many-attachments-text">
{{ $t("status.many_attachments", { number: attachments.length }) }}
{{ $t('status.many_attachments', { number: attachments.length }) }}
</div>
<div class="many-attachments-buttons">
<span
@ -53,7 +62,7 @@
class="button-unstyled -link"
@click="toggleHidingLong(true)"
>
{{ $t("status.collapse_attachments") }}
{{ $t('status.collapse_attachments') }}
</button>
</span>
<span
@ -64,7 +73,7 @@
class="button-unstyled -link"
@click="toggleHidingLong(false)"
>
{{ $t("status.show_all_attachments") }}
{{ $t('status.show_all_attachments') }}
</button>
</span>
<span
@ -75,7 +84,7 @@
class="button-unstyled -link"
@click="openGallery"
>
{{ $t("status.open_gallery") }}
{{ $t('status.open_gallery') }}
</button>
</span>
</div>
@ -83,7 +92,7 @@
</div>
</template>
<script src='./gallery.js'></script>
<script src="./gallery.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@ -109,8 +118,8 @@
.gallery-rows {
max-height: 25em;
overflow: hidden;
mask:
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
mask: linear-gradient(to top, white, transparent) bottom/100% 70px
no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */

View File

@ -1,20 +1,16 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
library.add(
faTimes
)
library.add(faTimes)
const GlobalNoticeList = {
computed: {
notices () {
notices() {
return this.$store.state.interface.globalNotices
}
},
methods: {
closeNotice (notice) {
closeNotice(notice) {
this.$store.dispatch('removeGlobalNotice', notice)
}
}

View File

@ -18,7 +18,7 @@ const HashtagLink = {
}
},
methods: {
onClick () {
onClick() {
const tag = this.tag || extractTagFromUrl(this.url)
if (tag) {
const link = this.generateTagLink(tag)
@ -27,7 +27,7 @@ const HashtagLink = {
window.open(this.url, '_blank')
}
},
generateTagLink (tag) {
generateTagLink(tag) {
return `/tag/${tag}`
}
}

View File

@ -1,7 +1,5 @@
<template>
<span
class="HashtagLink"
>
<span class="HashtagLink">
<!-- eslint-disable vue/no-v-html -->
<a
:href="url"
@ -14,6 +12,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" />

View File

@ -1,13 +1,9 @@
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
)
library.add(faCircleNotch)
const ImageCropper = {
props: {
@ -21,7 +17,7 @@ const ImageCropper = {
},
cropperOptions: {
type: Object,
default () {
default() {
return {
aspectRatio: 1,
autoCropArea: 1,
@ -46,7 +42,7 @@ const ImageCropper = {
type: String
}
},
data () {
data() {
return {
cropper: undefined,
dataUrl: undefined,
@ -55,18 +51,21 @@ const ImageCropper = {
}
},
computed: {
saveText () {
saveText() {
return this.saveButtonLabel || this.$t('image_cropper.save')
},
saveWithoutCroppingText () {
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
saveWithoutCroppingText() {
return (
this.saveWithoutCroppingButtonlabel ||
this.$t('image_cropper.save_without_cropping')
)
},
cancelText () {
cancelText() {
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
}
},
methods: {
destroy () {
destroy() {
if (this.cropper) {
this.cropper.destroy()
}
@ -74,7 +73,7 @@ const ImageCropper = {
this.dataUrl = undefined
this.$emit('close')
},
submit (cropping = true) {
submit(cropping = true) {
this.submitting = true
this.submitHandler(cropping && this.cropper, this.file)
.then(() => this.destroy())
@ -82,16 +81,18 @@ const ImageCropper = {
this.submitting = false
})
},
pickImage () {
pickImage() {
this.$refs.input.click()
},
createCropper () {
createCropper() {
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
},
getTriggerDOM () {
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
getTriggerDOM() {
return typeof this.trigger === 'object'
? this.trigger
: document.querySelector(this.trigger)
},
readFile () {
readFile() {
const fileInput = this.$refs.input
if (fileInput.files != null && fileInput.files[0] != null) {
this.file = fileInput.files[0]
@ -105,7 +106,7 @@ const ImageCropper = {
}
}
},
mounted () {
mounted() {
// listen for click event on trigger
const trigger = this.getTriggerDOM()
if (!trigger) {

View File

@ -7,7 +7,7 @@
:src="dataUrl"
alt=""
@load.stop="createCropper"
>
/>
</div>
<div class="image-cropper-buttons-wrapper">
<button
@ -43,7 +43,7 @@
type="file"
class="image-cropper-img-input"
:accept="mimes"
>
/>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More