forked from AkkomaGang/akkoma-fe
Compare commits
119 Commits
aedd0794a4
...
b009428814
Author | SHA1 | Date |
---|---|---|
floatingghost | b009428814 | |
floatingghost | 7bec96a1bf | |
floatingghost | 0b5793c1e0 | |
floatingghost | 72ef2e7454 | |
FloatingGhost | c39332c1bf | |
FloatingGhost | 8c6cf86de3 | |
FloatingGhost | 909271c764 | |
anna | 413acbc7dd | |
Sol Fisher Romanoff | 6e1ba218df | |
Sol Fisher Romanoff | 830e8fdb45 | |
FloatingGhost | 9bf310d509 | |
FloatingGhost | e3e8b19df3 | |
Weblate | e86c7abb39 | |
floatingghost | 8a0da8861d | |
Sol Fisher Romanoff | 6c7e691aea | |
Weblate | b33d15a739 | |
floatingghost | 40e86998e6 | |
Weblate | 177f344033 | |
floatingghost | 9079ac4afa | |
Weblate | dfc4e0a026 | |
Weblate | 3d732d1d28 | |
Weblate | e8ee31afed | |
Weblate | d9d6b1e80b | |
Weblate | 1dd7a89544 | |
floatingghost | d3280c4ab3 | |
FloatingGhost | abc75c360b | |
FloatingGhost | a8e119b0f1 | |
FloatingGhost | 17e574b173 | |
floatingghost | 71d2e0b0ce | |
FloatingGhost | b68e968bf9 | |
floatingghost | eb49295422 | |
astra akari | 337a30fe01 | |
floatingghost | 105ecd3836 | |
FloatingGhost | a3e490edcd | |
FloatingGhost | f8f5e1c89b | |
FloatingGhost | e132814478 | |
FloatingGhost | 6af1df8bef | |
floatingghost | b86f12cede | |
Karl Prieb | c669701762 | |
floatingghost | 0900a9d87b | |
FloatingGhost | 0a01a2bdf0 | |
darkkirb | 7860c885c4 | |
floatingghost | 1c3bd60af2 | |
Sean Meininger | b8faee5d6d | |
floatingghost | c01c62f149 | |
floatingghost | 105b934f90 | |
FloatingGhost | b1f41add0e | |
floatingghost | e4e8ed812b | |
Beefox | 684894aee3 | |
floatingghost | f8a796b234 | |
floatingghost | 70ea9e772c | |
Mergan | efe0f53736 | |
Beefox | fcbbbad8d4 | |
FloatingGhost | 39b6b0b49f | |
FloatingGhost | 867a86d887 | |
FloatingGhost | 7538369fa1 | |
Weblate | 2d4b2f2e20 | |
astra akari | 862c93706c | |
David | e06348ee33 | |
FloatingGhost | 169282ea42 | |
floatingghost | db46879a8f | |
FloatingGhost | 0770981a20 | |
floatingghost | c2a5a8c91f | |
Sol Fisher Romanoff | a1c0642bb5 | |
Weblate | 1157396ed5 | |
Weblate | 5a76dd5f90 | |
FloatingGhost | c7200b2234 | |
FloatingGhost | 98074ed90d | |
floatingghost | d51308a56b | |
FloatingGhost | 4b5536ae68 | |
FloatingGhost | e44462b1d5 | |
Ngô Ngọc Đức Huy | af32f901ac | |
floatingghost | 0810c57c8b | |
FloatingGhost | e77931d68c | |
floatingghost | f3962e3be7 | |
FloatingGhost | 1e8fc5bcc4 | |
Sol Fisher Romanoff | 642fe3dc10 | |
FloatingGhost | 8713f1870f | |
floatingghost | 837c61569a | |
sn0w | a83c3a1fa1 | |
Weblate | 4d8f288bd9 | |
floatingghost | 3286641f3c | |
Weblate | 677f5ae071 | |
sfr | 15bac1e401 | |
Weblate | 22b4aed8f6 | |
floatingghost | 23b0b01829 | |
Weblate | 53c487535e | |
floatingghost | 278b2c25ad | |
Sol Fisher Romanoff | 6a045dbc58 | |
Sol Fisher Romanoff | cd9dc9d2b2 | |
Weblate | 3f2d54f057 | |
floatingghost | 251e440dad | |
FloatingGhost | ffac376b5a | |
Weblate | 8bd18643e4 | |
Weblate | 5c28865018 | |
floatingghost | 721e3b016d | |
FloatingGhost | 469063ff52 | |
FloatingGhost | d8643b5b4a | |
FloatingGhost | 04c744e764 | |
solidsanek | bda433b006 | |
FloatingGhost | 2e00c19074 | |
FloatingGhost | 7df49720de | |
Weblate | ddc40f5bb3 | |
Weblate | 90793281d5 | |
Weblate | d1dd043cfa | |
Weblate | 8745317c38 | |
Sean King | 80f58baa86 | |
Sean King | 2453a338be | |
floatingghost | 4f837f75ea | |
floatingghost | eaf2bd05a0 | |
Sol Fisher Romanoff | ca646822f6 | |
Mergan | 468eb12573 | |
floatingghost | 2ab223e791 | |
FloatingGhost | 87683052e8 | |
FloatingGhost | 42e48348ae | |
floatingghost | 38d50acaeb | |
floatingghost | e72548ae51 | |
Norm | bf1debdeb6 | |
Sol Fisher Romanoff | b936506f47 |
10
.eslintrc.js
10
.eslintrc.js
|
@ -1,17 +1,17 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
parser: '@babel/eslint-parser',
|
||||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
'vue',
|
||||
'import'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
|
@ -23,6 +23,8 @@ module.exports = {
|
|||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'vue/require-prop-types': 0,
|
||||
'vue/no-unused-vars': 0,
|
||||
'no-tabs': 0
|
||||
'no-tabs': 0,
|
||||
'vue/multi-word-component-names': 0,
|
||||
'vue/no-reserved-component-names': 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
name: "Bug report"
|
||||
about: "Something isn't working as expected"
|
||||
title: "[bug] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thanks for taking the time to file this bug report! Please try to be as specific and detailed as you can, so we can track down the issue and fix it as soon as possible."
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: "Version"
|
||||
description: "Which version of pleroma-fe are you running? If running develop, specify the commit hash."
|
||||
placeholder: "e.g. 2022.11, 40e86998e6"
|
||||
- type: textarea
|
||||
id: attempt
|
||||
attributes:
|
||||
label: "What were you trying to do?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expectation
|
||||
attributes:
|
||||
label: "What did you expect to happen?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reality
|
||||
attributes:
|
||||
label: "What actually happened?"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: "Severity"
|
||||
description: "Does this issue prevent you from using the software as normal?"
|
||||
options:
|
||||
- "I cannot use the software"
|
||||
- "I cannot use it as easily as I'd like"
|
||||
- "I can manage"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this issue?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev) or [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this issue mentioned anywhere."
|
|
@ -0,0 +1,29 @@
|
|||
name: "Feature request"
|
||||
about: "I'd like something to be added to pleroma-fe"
|
||||
title: "[feat] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thanks for taking the time to request a new feature! Please be as concise and clear as you can in your proposal, so we could understand what you're going for."
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: "The idea"
|
||||
description: "What do you think you should be able to do in pleroma-fe?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: "The reasoning"
|
||||
description: "Why would this be a worthwhile feature? Does it solve any problems? Have people talked about wanting it?"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this feature request?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev), [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues), or the one for [the backend](https://akkoma.dev/AkkomaGang/akkoma/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this feature request mentioned anywhere."
|
||||
- label: "This feature is related to the pleroma-fe Akkoma frontend specifically, and not the backend."
|
|
@ -9,3 +9,4 @@ selenium-debug.log
|
|||
config/local.json
|
||||
config/local.*.json
|
||||
docs/site/
|
||||
.vscode/
|
|
@ -1,19 +1,13 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-recommended-vue/scss",
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-standard"
|
||||
],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
true,
|
||||
{
|
||||
"component": "pascal-case",
|
||||
"variant": "^-[a-z]\\w+",
|
||||
"element": "^[a-z]\\w+"
|
||||
}
|
||||
]
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,17 @@ pipeline:
|
|||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:16
|
||||
image: node:18
|
||||
commands:
|
||||
- yarn
|
||||
- yarn lint
|
||||
- yarn stylelint
|
||||
#- yarn stylelint
|
||||
|
||||
test:
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:16
|
||||
image: node:18
|
||||
commands:
|
||||
- apt update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
|
@ -27,7 +27,7 @@ pipeline:
|
|||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:16
|
||||
image: node:18
|
||||
commands:
|
||||
- yarn
|
||||
- yarn build
|
||||
|
@ -39,7 +39,7 @@ pipeline:
|
|||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:16
|
||||
image: node:18
|
||||
secrets:
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
|
|
|
@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Implemented remote interaction with statuses
|
||||
|
||||
|
||||
## 2022.09 - 2022-09-10
|
||||
### Added
|
||||
- Automatic post translations. Must be configured on the backend in order to work.
|
||||
|
@ -62,7 +67,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Ability to rearrange order of attachments when uploading
|
||||
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
||||
- Added frontend ui for account migration
|
||||
- Implemented remote interaction with statuses
|
||||
|
||||
|
||||
## [2.4.2] - 2022-01-09
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Akkoma Code of Conduct
|
||||
|
||||
The Akkoma project aims to be **enjoyable** for anyone to participate in, regardless of their identity or level of expertise. To achieve this, the community must create an environment which is **safe** and **equitable**; the following guidelines have been created with these goals in mind.
|
||||
|
||||
1. **Treat individuals with respect.** Differing experiences and viewpoints deserve to be respected, and bigotry and harassment are not tolerated under any circumstances.
|
||||
- Individuals should at all times be treated as equals, regardless of their age, gender, sexuality, race, ethnicity, _or any other characteristic_, intrinsic or otherwise.
|
||||
- Behaviour that is harmful in nature should be addressed and corrected *regardless of intent*.
|
||||
- Respect personal boundaries and ask for clarification whenever they are unclear.
|
||||
- (Obviously, hate does not count as merely a "differing viewpoint", because it is harmful in nature.)
|
||||
|
||||
2. **Be understanding of differences in communication.** Not everyone is aware of unspoken social cues, and speech that is not intended to be offensive should not be treated as such simply due to an atypical manner of communication.
|
||||
- Somebody who speaks bluntly is not necessarily rude, and somebody who swears a lot is not necessarily volatile.
|
||||
- Try to confirm your interpretation of their intent rather than assuming bad faith.
|
||||
- Someone may not communicate as, or come across as a picture of "professionalism", but this should not be seen as a reason to dismiss them. This is a **casual** space, and communication styles can reflect that.
|
||||
|
||||
3. **"Uncomfortable" does not mean "unsafe".** In an ideal world, the community would be safe, equitable, enjoyable, *and* comfortable for all members at all times. Unfortunately, this is not always possible in reality.
|
||||
- Safety and equity will be prioritized over comfort whenever it is necessary to do so.
|
||||
- Weaponizing one's own discomfort to deflect accountability or censor an individual (e.g. "white fragility") is a form of discriminatory conduct.
|
||||
|
||||
4. **Let people grow from their mistakes.** Nobody is perfect; even the most well-meaning individual can do something hurtful. Everyone should be given a fair opportunity to explain themselves and correct their behaviour. Portraying someone as inherently malicious prevents improvement and shifts focus away from the *action* that was problematic.
|
||||
- Avoid bringing up past events that do not accurately reflect an individual's current actions or beliefs. (This is, of course, different from providing evidence of a recurring pattern of behaviour.)
|
||||
|
||||
---
|
||||
This document was adapted from one created by ~keith as part of punks default repository template, and is licensed under CC-BY-SA 4.0. The original template is here: <https://bytes.keithhacks.cyou/keith/default-template>
|
|
@ -1,49 +0,0 @@
|
|||
```
|
||||
o$$$$$$oo
|
||||
o$" "$oo
|
||||
$ o""""$o "$o
|
||||
"$ o "o "o $
|
||||
"$ $o $ $ o$
|
||||
"$ o$"$ o$
|
||||
"$ooooo$$ $ o$
|
||||
o$ """ $ " $$$ " $
|
||||
o$ $o $$" " "
|
||||
$$ $ " $ $$$o"$ o o$"
|
||||
$" o "" $ $" " o" $$
|
||||
$o " " $ o$" o" o$"
|
||||
"$o $$ $ o" o$$"
|
||||
""o$o"$" $oo" o$"
|
||||
o$$ $ $$$ o$$
|
||||
o" o oo"" "" "$o
|
||||
o$o" "" $
|
||||
$" " o" " " " "o
|
||||
$$ " " o$ o$o " $
|
||||
o$ $ $ o$$ " " ""
|
||||
o $ $" " "o o$
|
||||
$ o $o$oo$""
|
||||
$o $ o o o"$$
|
||||
$o o $ $ "$o
|
||||
$o $ o $ $ "o
|
||||
$ $ "o $ "o"$o
|
||||
$ " o $ o $$
|
||||
$o$o$o$o$$o$$$o$$o$o$$o$$o$$$o$o$o$o$o$o$o$o$o$ooo
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ " $$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$"
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ooooo$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""""
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
|
||||
"$o$o$o$o$o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
|
||||
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
|
||||
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
```
|
|
@ -29,18 +29,6 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
|||
})
|
||||
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler)
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
compiler.plugin('compilation', function (compilation) {
|
||||
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
// FIXME: This supposed to reload whole page when index.html is changed,
|
||||
// however now it reloads entire page on every breath, i suppose the order
|
||||
// of plugins changed or something. It's a minor thing and douesn't hurt
|
||||
// disabling it, constant reloads hurt much more
|
||||
|
||||
// hotMiddleware.publish({ action: 'reload' })
|
||||
// cb()
|
||||
})
|
||||
})
|
||||
|
||||
// proxy api requests
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
|
|
|
@ -2,8 +2,6 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||
var CopyPlugin = require('copy-webpack-plugin');
|
||||
var { VueLoaderPlugin } = require('vue-loader')
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
|
@ -20,6 +18,7 @@ module.exports = {
|
|||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
|
@ -34,6 +33,9 @@ module.exports = {
|
|||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
fallback: {
|
||||
"url": require.resolve("url/"),
|
||||
},
|
||||
alias: {
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
|
@ -116,23 +118,6 @@ module.exports = {
|
|||
]
|
||||
},
|
||||
plugins: [
|
||||
new ServiceWorkerWebpackPlugin({
|
||||
entry: path.join(__dirname, '..', 'src/sw.js'),
|
||||
filename: 'sw-pleroma.js'
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "node_modules/ruffle-mirror/*",
|
||||
to: "static/ruffle",
|
||||
flatten: true
|
||||
},
|
||||
],
|
||||
options: {
|
||||
concurrency: 100,
|
||||
},
|
||||
})
|
||||
new VueLoaderPlugin()
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
var config = require('../config')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
var { merge } = require('webpack-merge')
|
||||
var utils = require('./utils')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
@ -16,7 +16,7 @@ module.exports = merge(baseWebpackConfig, {
|
|||
},
|
||||
mode: 'development',
|
||||
// eval-source-map is faster for development
|
||||
devtool: '#eval-source-map',
|
||||
devtool: 'eval-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
|
|
|
@ -2,7 +2,8 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
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')
|
||||
|
@ -19,7 +20,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
minimize: true,
|
||||
splitChunks: {
|
||||
|
@ -32,6 +33,11 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||
swDest: 'sw-pleroma.js',
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
||||
}),
|
||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env,
|
||||
|
@ -62,7 +68,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'dependency'
|
||||
chunksSortMode: 'auto'
|
||||
}),
|
||||
// split vendor js into its own file
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var merge = require('webpack-merge')
|
||||
var { merge } = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
|
|
|
@ -38,6 +38,11 @@ module.exports = {
|
|||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/manifest.json': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/api': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var merge = require('webpack-merge')
|
||||
var { merge } = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
|
|
|
@ -70,9 +70,6 @@ Default post formatting option (markdown/bbcode/plaintext/etc...)
|
|||
### `redirectRootNoLogin`, `redirectRootLogin`
|
||||
These two settings should point to where FE should redirect visitor when they login/open up website root
|
||||
|
||||
### `scopeCopy`
|
||||
Copy post scope (visibility) when replying to a post. Instance-default.
|
||||
|
||||
### `sidebarRight`
|
||||
Change alignment of sidebar and panels to the right. Defaults to `false`.
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ You have several timelines to browse trough
|
|||
- **Bookmarks** all the posts you've bookmarked. You can bookmark a post by clicking the three dots on the bottom right of the post and choose Bookmark.
|
||||
- **Direct Messages** all posts with `direct` scope addressed to you or mentioning you.
|
||||
- **Public Timelines** all public posts made by users on the instance you're on
|
||||
- **Bubble Timeline** all public posts from instances recommended by your admin(s) in the instance settings. This won't appear if they haven't set anything up for it.
|
||||
- **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all public posts known by your instance. Due to nature of the network your instance may not know *all* the posts on the network, so only posts known by your instance are shown there.
|
||||
|
||||
Note that by default you will see all posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can change said behavior in the [settings](settings.md#filtering).
|
||||
|
|
|
@ -3,17 +3,19 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<title>Pleroma</title>
|
||||
<title>Akkoma</title>
|
||||
<link rel="stylesheet" href="/static/font/css/fontello.css">
|
||||
<link rel="stylesheet" href="/static/font/css/animation.css">
|
||||
<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">
|
||||
<!--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 Pleroma, please enable JavaScript.</noscript>
|
||||
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
|
|
89
package.json
89
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.2.0",
|
||||
"version": "3.5.0",
|
||||
"description": "A frontend for Akkoma instances",
|
||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||
"private": true,
|
||||
|
@ -11,7 +11,7 @@
|
|||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"stylelint": "npx stylelint src/components/status/status.scss",
|
||||
"stylelint": "stylelint src/**/*.scss",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
|
@ -20,11 +20,11 @@
|
|||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@vuelidate/core": "2.0.0-alpha.42",
|
||||
"@vuelidate/validators": "2.0.0-alpha.30",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"body-scroll-lock": "2.7.1",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
|
@ -33,13 +33,11 @@
|
|||
"escape-html": "1.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"localforage": "1.10.0",
|
||||
"marked": "^4.0.17",
|
||||
"marked-mfm": "^0.5.0",
|
||||
"parse-link-header": "1.0.1",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"phoenix": "1.6.2",
|
||||
"punycode.js": "2.1.0",
|
||||
"qrcode": "1",
|
||||
"ruffle-mirror": "2021.12.31",
|
||||
"url": "^0.11.0",
|
||||
"vue": "^3.2.31",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "4.0.14",
|
||||
|
@ -48,6 +46,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/plugin-transform-runtime": "7.17.0",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/register": "7.17.7",
|
||||
|
@ -58,31 +57,29 @@
|
|||
"@vue/compiler-sfc": "^3.1.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"autoprefixer": "6.7.7",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-loader": "8.2.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "3.5.0",
|
||||
"chai": "^4.3.7",
|
||||
"chalk": "1.1.3",
|
||||
"chromedriver": "87.0.7",
|
||||
"connect-history-api-fallback": "1.6.0",
|
||||
"copy-webpack-plugin": "6.4.1",
|
||||
"cross-spawn": "4.0.2",
|
||||
"css-loader": "0.28.11",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-standard": "12.0.0",
|
||||
"eslint-friendly-formatter": "2.0.7",
|
||||
"eslint-loader": "2.2.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-node": "7.0.1",
|
||||
"eslint-plugin-promise": "4.3.1",
|
||||
"eslint-plugin-standard": "4.1.0",
|
||||
"eslint-plugin-vue": "5.2.3",
|
||||
"chromedriver": "^107.0.3",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"css-loader": "^6.7.2",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^7.32.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-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.17.3",
|
||||
"file-loader": "3.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"function-bind": "1.1.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"http-proxy-middleware": "0.21.0",
|
||||
"inject-loader": "2.0.1",
|
||||
"iso-639-1": "2.1.15",
|
||||
|
@ -96,7 +93,7 @@
|
|||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-webpack": "4.0.2",
|
||||
"karma-webpack": "^5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"lolex": "1.6.0",
|
||||
"mini-css-extract-plugin": "0.12.0",
|
||||
|
@ -104,29 +101,33 @@
|
|||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.4.1",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-sass": "^0.5.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "1.53.0",
|
||||
"sass-loader": "7.3.1",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "5.7.1",
|
||||
"serviceworker-webpack-plugin": "1.0.1",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
"sinon-chai": "2.14.0",
|
||||
"stylelint": "13.6.1",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
"stylelint-rscss": "0.4.0",
|
||||
"url-loader": "1.1.2",
|
||||
"vue-loader": "^16.0.0",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"webpack": "4.46.0",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-hot-middleware": "2.25.1",
|
||||
"webpack-merge": "0.20.0"
|
||||
"stylelint": "^14.15.0",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-config-standard-scss": "^6.1.0",
|
||||
"stylelint-rscss": "^0.4.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-dev-middleware": "^5.3.3",
|
||||
"webpack-hot-middleware": "^2.25.1",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"node": ">= 16.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import FeaturesPanel from './components/features_panel/features_panel.vue'
|
|||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import ModModal from './components/mod_modal/mod_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
|
@ -33,6 +34,7 @@ export default {
|
|||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal,
|
||||
ModModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<ModModal />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -148,7 +148,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('showWiderShortcuts')
|
||||
copyInstanceOption('showNavShortcuts')
|
||||
copyInstanceOption('showPanelNavShortcuts')
|
||||
copyInstanceOption('stopGifs')
|
||||
copyInstanceOption('logo')
|
||||
copyInstanceOption('conversationDisplay')
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
|
@ -169,10 +171,8 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('minimalScopesMode')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
|
@ -396,9 +396,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
store.dispatch('startFetchingAnnouncements')
|
||||
store.dispatch('startFetchingReports')
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
store.dispatch('getSupportedTranslationlanguages')
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
|
|
|
@ -22,6 +22,8 @@ import Lists from 'components/lists/lists.vue'
|
|||
import ListTimeline from 'components/list_timeline/list_timeline.vue'
|
||||
import ListEdit from 'components/list_edit/list_edit.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import RegistrationRequestSent from 'components/registration_request_sent/registration_request_sent.vue'
|
||||
import AwaitingEmailConfirmation from 'components/awaiting_email_confirmation/awaiting_email_confirmation.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
|
@ -62,6 +64,8 @@ export default (store) => {
|
|||
{ 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 },
|
||||
|
|
|
@ -20,6 +20,9 @@ const About = {
|
|||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
},
|
||||
showLocalBubblePanel () {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||
<staff-panel />
|
||||
<terms-of-service-panel />
|
||||
<LocalBubblePanel />
|
||||
<LocalBubblePanel v-if="showLocalBubblePanel" />
|
||||
<MRFTransparencyPanel />
|
||||
<features-panel v-if="showFeaturesPanel" />
|
||||
</div>
|
||||
|
|
|
@ -26,6 +26,9 @@ const AccountActions = {
|
|||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
refetchRelationship () {
|
||||
return this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
|
@ -52,8 +55,19 @@ const AccountActions = {
|
|||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
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])
|
||||
.then(() => this.refetchRelationship())
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -28,6 +28,13 @@
|
|||
class="dropdown-divider"
|
||||
/>
|
||||
</template>
|
||||
<button
|
||||
v-if="relationship.followed_by"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="removeUserFromFollowers"
|
||||
>
|
||||
{{ $t('user_card.remove_follower') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
|
@ -48,6 +55,20 @@
|
|||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.domain_blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="unmuteDomain"
|
||||
>
|
||||
{{ $t('user_card.domain_muted') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!user.is_local"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="muteDomain"
|
||||
>
|
||||
{{ $t('user_card.mute_domain') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
|
|
|
@ -227,7 +227,7 @@ const Attachment = {
|
|||
this.$emit('resize', newHeight)
|
||||
},
|
||||
postStatus (event) {
|
||||
console.log(this.statusForm.postStatus(event, this.statusForm.newStatus))
|
||||
this.statusForm.postStatus(event, this.statusForm.newStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
display: flex;
|
||||
padding-top: 0.5em;
|
||||
z-index: 1;
|
||||
max-height: 50%;
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
|
@ -36,7 +37,7 @@
|
|||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
&.-static {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
computed: {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{{ $t('registration.awaiting_email_confirmation_title') }}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>{{ $t('registration.awaiting_email_confirmation') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./awaiting_email_confirmation.js"></script>
|
|
@ -16,7 +16,8 @@ import {
|
|||
faUsers,
|
||||
faCommentMedical,
|
||||
faBookmark,
|
||||
faInfoCircle
|
||||
faInfoCircle,
|
||||
faUserTie
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
|
@ -34,7 +35,8 @@ library.add(
|
|||
faUsers,
|
||||
faCommentMedical,
|
||||
faBookmark,
|
||||
faInfoCircle
|
||||
faInfoCircle,
|
||||
faUserTie
|
||||
)
|
||||
|
||||
export default {
|
||||
|
@ -96,8 +98,15 @@ export default {
|
|||
logoLeft () { return this.$store.state.instance.logoLeft },
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
federating () { return this.$store.state.instance.federating },
|
||||
shouldConfirmLogout () {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
showBubbleTimeline () {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
},
|
||||
restrictedTimelines () {
|
||||
return this.$store.state.instance.restrict_unauthenticated.timelines
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -109,6 +118,9 @@ export default {
|
|||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
openModModal () {
|
||||
this.$store.dispatch('openModModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="currentUser || !(privateMode || restrictedTimelines.public)"
|
||||
:to="{ name: 'public-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -55,7 +56,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="currentUser"
|
||||
v-if="currentUser && showBubbleTimeline"
|
||||
:to="{ name: 'bubble-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -67,6 +68,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="federating && (currentUser || !(privateMode || restrictedTimelines.federated))"
|
||||
:to="{ name: 'public-external-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -151,6 +153,18 @@
|
|||
:title="$t('nav.preferences')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||
class="button-unstyled nav-icon"
|
||||
@click.stop="openModModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="user-tie"
|
||||
:title="$t('nav.moderation')"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
href="/pleroma/admin/#/login-pleroma"
|
||||
|
|
|
@ -178,7 +178,7 @@ const EmojiInput = {
|
|||
textAtCaret: async function (newWord) {
|
||||
const firstchar = newWord.charAt(0)
|
||||
this.suggestions = []
|
||||
if (newWord === firstchar) return
|
||||
if (newWord === firstchar && firstchar !== '$') return
|
||||
const matchedSuggestions = await this.suggest(newWord)
|
||||
// Async: cancel if textAtCaret has changed during wait
|
||||
if (this.textAtCaret !== newWord) return
|
||||
|
@ -277,7 +277,6 @@ const EmojiInput = {
|
|||
},
|
||||
replaceText (e, suggestion) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (this.textAtCaret.length === 1) { return }
|
||||
if (len > 0 || suggestion) {
|
||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||
const replacement = chosenSuggestion.replacement
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span class="image">
|
||||
<span v-if="!suggestion.mfm" class="image">
|
||||
<img
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
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
|
||||
* data: object providing source information for specific types of suggestions:
|
||||
|
@ -21,6 +24,10 @@ export default data => {
|
|||
if (firstChar === '@' && usersCurry) {
|
||||
return usersCurry(input)
|
||||
}
|
||||
if (firstChar === '$') {
|
||||
return MFM_TAGS
|
||||
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,10 @@ const EmojiPicker = {
|
|||
this.scrolledGroup(target)
|
||||
this.triggerLoadMore(target)
|
||||
},
|
||||
onWheel (e) {
|
||||
e.preventDefault()
|
||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||
},
|
||||
highlight (key) {
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = key
|
||||
|
@ -138,7 +142,7 @@ const EmojiPicker = {
|
|||
if (this.keyword === '') return list
|
||||
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
|
||||
return list.filter(emoji => {
|
||||
return regex.test(emoji.displayText)
|
||||
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.Notification {
|
||||
.emoji-picker {
|
||||
min-width: 160%;
|
||||
width: 150%;
|
||||
overflow: hidden;
|
||||
left: -70%;
|
||||
max-width: 100%;
|
||||
@media (min-width: 800px) and (max-width: 1300px) {
|
||||
left: -50%;
|
||||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
left: -10%;
|
||||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.emoji-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<div class="emoji-picker panel panel-default panel-body">
|
||||
<div class="heading">
|
||||
<span class="emoji-tabs">
|
||||
<span
|
||||
class="emoji-tabs"
|
||||
@wheel="onWheel"
|
||||
ref="emoji-tabs"
|
||||
>
|
||||
<span
|
||||
v-for="group in emojis"
|
||||
:key="group.id"
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.picked-reaction {
|
||||
.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);
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
faThumbtack,
|
||||
faShareAlt,
|
||||
faExternalLinkAlt,
|
||||
faHistory
|
||||
faHistory,
|
||||
faFilePen
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faBookmark as faBookmarkReg,
|
||||
|
@ -24,7 +25,8 @@ library.add(
|
|||
faShareAlt,
|
||||
faExternalLinkAlt,
|
||||
faFlag,
|
||||
faHistory
|
||||
faHistory,
|
||||
faFilePen
|
||||
)
|
||||
|
||||
const ExtraButtons = {
|
||||
|
@ -36,7 +38,8 @@ const ExtraButtons = {
|
|||
data () {
|
||||
return {
|
||||
expanded: false,
|
||||
showingDeleteDialog: false
|
||||
showingDeleteDialog: false,
|
||||
showingRedraftDialog: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -122,6 +125,34 @@ const ExtraButtons = {
|
|||
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 () {
|
||||
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,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
this.doDeleteStatus()
|
||||
},
|
||||
showRedraftStatusConfirmDialog () {
|
||||
this.showingRedraftDialog = true
|
||||
},
|
||||
hideRedraftStatusConfirmDialog () {
|
||||
this.showingRedraftDialog = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -95,6 +95,17 @@
|
|||
icon="history"
|
||||
/><span>{{ $t("status.edit_history") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="redraftStatus"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="file-pen"
|
||||
/><span>{{ $t("status.redraft") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -179,6 +190,16 @@
|
|||
>
|
||||
{{ $t('status.delete_confirm') }}
|
||||
</ConfirmModal>
|
||||
<ConfirmModal
|
||||
v-if="showingRedraftDialog"
|
||||
:title="$t('status.redraft_confirm_title')"
|
||||
:cancel-text="$t('status.redraft_confirm_cancel_button')"
|
||||
:confirm-text="$t('status.redraft_confirm_accept_button')"
|
||||
@cancelled="hideRedraftStatusConfirmDialog"
|
||||
@accepted="doRedraftStatus"
|
||||
>
|
||||
{{ $t('status.redraft_confirm') }}
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</template>
|
||||
</Popover>
|
||||
|
|
|
@ -4,7 +4,6 @@ const FeaturesPanel = {
|
|||
computed: {
|
||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
||||
textlimit: function () { return this.$store.state.instance.textlimit },
|
||||
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import FollowButton from '../follow_button/follow_button.vue'
|
||||
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||
|
||||
const FollowCard = {
|
||||
props: [
|
||||
|
@ -10,7 +11,8 @@ const FollowCard = {
|
|||
components: {
|
||||
BasicUserCard,
|
||||
RemoteFollow,
|
||||
FollowButton
|
||||
FollowButton,
|
||||
RemoveFollowerButton
|
||||
},
|
||||
computed: {
|
||||
isMe () {
|
||||
|
|
|
@ -22,6 +22,11 @@
|
|||
class="follow-card-follow-button"
|
||||
:user="user"
|
||||
/>
|
||||
<RemoveFollowerButton
|
||||
v-if="noFollowsYou && relationship.followed_by"
|
||||
:relationship="relationship"
|
||||
class="follow-card-button"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</basic-user-card>
|
||||
|
@ -40,6 +45,12 @@
|
|||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
&-button {
|
||||
margin-top: 0.5em;
|
||||
padding: 0 1.5em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
&-follow-button {
|
||||
margin-top: 0.5em;
|
||||
margin-left: auto;
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import Modal from 'src/components/modal/modal.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
faChevronDown
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faWindowMinimize
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
faWindowMinimize,
|
||||
faChevronDown
|
||||
)
|
||||
|
||||
const ModModal = {
|
||||
components: {
|
||||
Modal,
|
||||
ModModalContent: getResettableAsyncComponent(
|
||||
() => import('./mod_modal_content.vue'),
|
||||
{
|
||||
loadingComponent: PanelLoading,
|
||||
errorComponent: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeModModal')
|
||||
},
|
||||
peekModal () {
|
||||
this.$store.dispatch('togglePeekModModal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
moderator () {
|
||||
return this.$store.state.users.currentUser &&
|
||||
(this.$store.state.users.currentUser.role === 'admin' ||
|
||||
this.$store.state.users.currentUser.role === 'moderator')
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.interface.modModalState !== 'hidden'
|
||||
},
|
||||
modalOpenedOnce () {
|
||||
return this.$store.state.interface.modModalLoaded
|
||||
},
|
||||
modalPeeked () {
|
||||
return this.$store.state.interface.modModalState === 'minimized'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ModModal
|
|
@ -0,0 +1,44 @@
|
|||
@import 'src/_variables.scss';
|
||||
.mod-modal {
|
||||
overflow: hidden;
|
||||
|
||||
&.peek {
|
||||
.mod-modal-panel {
|
||||
/* Explanation:
|
||||
* Modal is positioned vertically centered.
|
||||
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||
* bottom of the screen
|
||||
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||
*/
|
||||
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
/* For mobile, the modal takes 100% of the available screen.
|
||||
This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
|
||||
*/
|
||||
transform: translateY(calc(100% - 50px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mod-modal-panel {
|
||||
overflow: hidden;
|
||||
transition: transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 300ms;
|
||||
width: 1000px;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
max-width: 100vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<Modal
|
||||
v-if="moderator"
|
||||
:is-open="modalActivated"
|
||||
class="mod-modal"
|
||||
:class="{ peek: modalPeeked }"
|
||||
:no-background="modalPeeked"
|
||||
>
|
||||
<div class="mod-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t('moderation.moderation') }}
|
||||
</span>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('general.peek')"
|
||||
@click="peekModal"
|
||||
>
|
||||
<FAIcon
|
||||
:icon="['far', 'window-minimize']"
|
||||
fixed-width
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('general.close')"
|
||||
@click="closeModal"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
fixed-width
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ModModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./mod_modal.js"></script>
|
||||
<style src="./mod_modal.scss" lang="scss"></style>
|
|
@ -0,0 +1,63 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
|
||||
import ReportsTab from './tabs/reports_tab/reports_tab.vue'
|
||||
// import StatusesTab from './tabs/statuses_tab.vue'
|
||||
// import UsersTab from './tabs/users_tab.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faFlag,
|
||||
faMessage,
|
||||
faUsers
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faFlag,
|
||||
faMessage,
|
||||
faUsers
|
||||
)
|
||||
|
||||
const ModModalContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
|
||||
ReportsTab
|
||||
// StatusesTab,
|
||||
// UsersTab
|
||||
},
|
||||
computed: {
|
||||
open () {
|
||||
return this.$store.state.interface.modModalState !== 'hidden'
|
||||
},
|
||||
bodyLock () {
|
||||
return this.$store.state.interface.modModalState === 'visible'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen () {
|
||||
const targetTab = this.$store.state.interface.modModalTargetTab
|
||||
// We're being told to open in specific tab
|
||||
if (targetTab) {
|
||||
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
|
||||
return elm.props && elm.props['data-tab-name'] === targetTab
|
||||
})
|
||||
if (tabIndex >= 0) {
|
||||
this.$refs.tabSwitcher.setTab(tabIndex)
|
||||
}
|
||||
}
|
||||
// Clear the state of target tab, so that next time moderation is opened
|
||||
// it doesn't force it.
|
||||
this.$store.dispatch('clearModModalTargetTab')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.onOpen()
|
||||
},
|
||||
watch: {
|
||||
open: function (value) {
|
||||
if (value) this.onOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ModModalContent
|
|
@ -0,0 +1,21 @@
|
|||
@import 'src/_variables.scss';
|
||||
.mod_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
.content {
|
||||
margin: 1em 1em 1.4em;
|
||||
|
||||
> div {
|
||||
margin-bottom: .5em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="mod_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
:body-scroll-lock="bodyLock"
|
||||
>
|
||||
<div
|
||||
:label="$t('moderation.reports.reports')"
|
||||
icon="flag"
|
||||
data-tab-name="reports"
|
||||
>
|
||||
<ReportsTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./mod_modal_content.js"></script>
|
||||
<style src="./mod_modal_content.scss" lang="scss"></style>
|
|
@ -0,0 +1,124 @@
|
|||
import Popover from 'src/components/popover/popover.vue'
|
||||
import Status from 'src/components/status/status.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import ReportNote from './report_note.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronUp
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faChevronDown,
|
||||
faChevronUp
|
||||
)
|
||||
|
||||
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||
const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
|
||||
const SANDBOX = 'mrf_tag:sandbox'
|
||||
|
||||
const ReportCard = {
|
||||
data () {
|
||||
return {
|
||||
hidden: true,
|
||||
statusesHidden: true,
|
||||
notesHidden: true,
|
||||
note: null,
|
||||
tags: {
|
||||
FORCE_NSFW,
|
||||
STRIP_MEDIA,
|
||||
FORCE_UNLISTED,
|
||||
SANDBOX
|
||||
}
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'account',
|
||||
'actor',
|
||||
'content',
|
||||
'id',
|
||||
'notes',
|
||||
'state',
|
||||
'statuses'
|
||||
],
|
||||
components: {
|
||||
ReportNote,
|
||||
Popover,
|
||||
Status,
|
||||
UserAvatar
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchUser', this.account.id)
|
||||
},
|
||||
computed: {
|
||||
isOpen () {
|
||||
return this.state === 'open'
|
||||
},
|
||||
tagPolicyEnabled () {
|
||||
return this.$store.state.instance.federationPolicy.mrf_policies.includes('TagPolicy')
|
||||
},
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.account.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
},
|
||||
decode (content) {
|
||||
content = content.replaceAll('<br/>', '\n')
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.innerHTML = content
|
||||
return textarea.value
|
||||
},
|
||||
updateReportState (state) {
|
||||
this.$store.dispatch('updateReportStates', { reports: [{ id: this.id, state }] })
|
||||
},
|
||||
toggleNotes () {
|
||||
this.notesHidden = !this.notesHidden
|
||||
},
|
||||
addNoteToReport () {
|
||||
if (this.note.length > 0) {
|
||||
this.$store.dispatch('addNoteToReport', { id: this.id, note: this.note })
|
||||
this.note = null
|
||||
}
|
||||
},
|
||||
toggleStatuses () {
|
||||
this.statusesHidden = !this.statusesHidden
|
||||
},
|
||||
hasTag (tag) {
|
||||
return this.user.tags.includes(tag)
|
||||
},
|
||||
toggleTag (tag) {
|
||||
if (this.hasTag(tag)) {
|
||||
this.$store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
this.$store.commit('untagUser', { user: this.user, tag })
|
||||
})
|
||||
} else {
|
||||
this.$store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
this.$store.commit('tagUser', { user: this.user, tag })
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleActivationStatus () {
|
||||
this.$store.dispatch('toggleActivationStatus', { user: this.user })
|
||||
},
|
||||
deleteUser () {
|
||||
this.$store.state.backendInteractor.deleteUser({ user: this.user })
|
||||
.then(e => {
|
||||
this.$store.dispatch('markStatusesAsDeleted', status => this.user.id === status.user.id)
|
||||
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
|
||||
const isTargetUser = this.$route.params.name === this.user.name || this.$route.params.id === this.user.id
|
||||
if (isProfile && isTargetUser) {
|
||||
window.history.back()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportCard
|
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<div class="report-card panel">
|
||||
<div
|
||||
class="panel-heading"
|
||||
@click="toggleHidden"
|
||||
>
|
||||
<h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
|
||||
<button
|
||||
v-if="isOpen"
|
||||
class="button-default"
|
||||
@click.stop="updateReportState('closed')"
|
||||
>
|
||||
{{ $t('moderation.reports.close') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="isOpen"
|
||||
class="button-default"
|
||||
@click.stop="updateReportState('resolved')"
|
||||
>
|
||||
{{ $t('moderation.reports.resolve') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="button-default"
|
||||
@click.stop="updateReportState('open')"
|
||||
>
|
||||
{{ $t('moderation.reports.reopen') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
class="panel-body report-body"
|
||||
>
|
||||
<div class="report-content">
|
||||
<div v-if="content">
|
||||
{{ decode(content) }}
|
||||
</div>
|
||||
<i v-else class="faint">
|
||||
{{ $t('moderation.reports.no_content') }}
|
||||
</i>
|
||||
<div class="report-author">
|
||||
<UserAvatar
|
||||
class="small-avatar"
|
||||
:user="actor"
|
||||
/>
|
||||
{{ this.actor.screen_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown"
|
||||
v-if="!hidden && this.statuses.length > 0"
|
||||
>
|
||||
<button
|
||||
class="button button-unstyled dropdown-header"
|
||||
@click="toggleStatuses"
|
||||
>
|
||||
{{ $tc('moderation.reports.statuses', statuses.length - 1, { count: statuses.length }) }}
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="statusesHidden ? 'chevron-down' : 'chevron-up'"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="!statusesHidden">
|
||||
<Status
|
||||
v-for="status in statuses"
|
||||
:key="status.id"
|
||||
:collapsable="false"
|
||||
:expandable="false"
|
||||
:compact="false"
|
||||
:statusoid="status"
|
||||
:no-heading="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown"
|
||||
v-if="!hidden && this.notes.length > 0"
|
||||
>
|
||||
<button
|
||||
class="button button-unstyled dropdown-header"
|
||||
@click="toggleNotes"
|
||||
>
|
||||
{{ $tc('moderation.reports.notes', notes.length - 1, { count: notes.length }) }}
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="notesHidden ? 'chevron-down' : 'chevron-up'"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="!notesHidden">
|
||||
<ReportNote
|
||||
v-for="note in notes"
|
||||
:key="note.id"
|
||||
:report_id="id"
|
||||
v-bind="note"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-add-note">
|
||||
<textarea
|
||||
rows="1"
|
||||
cols="1"
|
||||
v-model.trim="note"
|
||||
:placeholder="$t('moderation.reports.note_placeholder')"
|
||||
/>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="addNoteToReport"
|
||||
>
|
||||
{{ $t('moderation.reports.add_note') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
class="panel-footer"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="toggleActivationStatus"
|
||||
>
|
||||
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="deleteUser"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.delete_account') }}
|
||||
</button>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="!tagPolicyEnabled"
|
||||
:title="tagPolicyEnabled ? '' : $t('moderation.reports.account.tag_policy_notice')"
|
||||
>
|
||||
<span>{{ $t("moderation.reports.tags") }}</span>
|
||||
{{ ' ' }}
|
||||
<FAIcon
|
||||
icon="chevron-down"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template v-slot:content="{close}">
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
:disabled="!tagPolicyEnabled"
|
||||
>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.FORCE_NSFW)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.force_nsfw') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.STRIP_MEDIA)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.strip_media') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.FORCE_UNLISTED)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.force_unlisted') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.SANDBOX)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.sandbox') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./report_card.js"></script>
|
|
@ -0,0 +1,37 @@
|
|||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
import Timeago from 'src/components/timeago/timeago.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
const ReportNote = {
|
||||
data () {
|
||||
return {
|
||||
showingDeleteDialog: false
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'content',
|
||||
'created_at',
|
||||
'user',
|
||||
'report_id',
|
||||
'id'
|
||||
],
|
||||
components: {
|
||||
ConfirmModal,
|
||||
Timeago,
|
||||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
deleteNoteFromReport () {
|
||||
this.$store.dispatch('deleteNoteFromReport', { id: this.report_id, note: this.id })
|
||||
this.showingDeleteDialog = false
|
||||
},
|
||||
showDeleteDialog () {
|
||||
this.showingDeleteDialog = true
|
||||
},
|
||||
hideDeleteDialog () {
|
||||
this.showingDeleteDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportNote
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="report-note">
|
||||
<div class="note-header">
|
||||
<div class="note-author">
|
||||
<UserAvatar
|
||||
class="small-avatar"
|
||||
:user="user"
|
||||
/>
|
||||
{{ this.user.screen_name }}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Timeago
|
||||
class="faint"
|
||||
:time="created_at"
|
||||
:auto-update="60"
|
||||
:long-format="true"
|
||||
:with-direction="true"
|
||||
/>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="showDeleteDialog"
|
||||
>
|
||||
{{ $t('moderation.reports.delete_note') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
<confirm-modal
|
||||
v-if="showingDeleteDialog"
|
||||
:title="$t('moderation.reports.delete_note_title')"
|
||||
:confirm-text="$t('moderation.reports.delete_note_accept')"
|
||||
:cancel-text="$t('moderation.reports.delete_note_cancel')"
|
||||
@accepted="deleteNoteFromReport"
|
||||
@cancelled="hideDeleteDialog"
|
||||
>
|
||||
{{ $t('moderation.reports.delete_note_confirm') }}
|
||||
</confirm-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./report_note.js"></script>
|
|
@ -0,0 +1,26 @@
|
|||
import { filter } from 'lodash'
|
||||
|
||||
import ReportCard from './report_card.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const ReportsTab = {
|
||||
data () {
|
||||
return {
|
||||
showClosed: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
ReportCard
|
||||
},
|
||||
computed: {
|
||||
reports () {
|
||||
return this.$store.state.reports.reports
|
||||
},
|
||||
openReports () {
|
||||
return filter(this.reports, { state: 'open' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportsTab
|
|
@ -0,0 +1,83 @@
|
|||
@import '../../../../_variables.scss';
|
||||
.report-card {
|
||||
.report-body {
|
||||
& > * {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
& > :not(:last-child) {
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: var(--border, #222);
|
||||
}
|
||||
|
||||
.report-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.report-author {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
.small-avatar {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
padding-right: 0.4em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
.dropdown-header {
|
||||
padding: 1em;
|
||||
color: var(--link, $fallback--link);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: var(--selectedMenuText, $fallback--link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-note {
|
||||
padding: 1em;
|
||||
|
||||
.note-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.report-add-note {
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 2em;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
display: flex;
|
||||
& > * {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reports-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div :label="$t('moderation.reports.reports')">
|
||||
<div class="content">
|
||||
<div class="reports-header">
|
||||
<h2>{{ $t('moderation.reports.reports') }}</h2>
|
||||
<Checkbox v-model="showClosed">
|
||||
{{ $t('moderation.reports.show_closed') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="reports">
|
||||
<div v-if="(openReports.length === 0 && !showClosed) || reports.length === 0">
|
||||
<p>{{ $t('moderation.reports.no_reports') }}</p>
|
||||
</div>
|
||||
<ReportCard
|
||||
v-for="report in (showClosed ? reports : openReports)"
|
||||
:key="report.id"
|
||||
v-bind="report"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./reports_tab.js"></script>
|
||||
<style src="./reports_tab.scss" lang="scss"></style>
|
|
@ -86,7 +86,8 @@ const PostStatusForm = {
|
|||
'fileLimit',
|
||||
'submitOnEnter',
|
||||
'emojiPickerPlacement',
|
||||
'optimisticPosting'
|
||||
'optimisticPosting',
|
||||
'isRedraft'
|
||||
],
|
||||
emits: [
|
||||
'posted',
|
||||
|
@ -141,13 +142,13 @@ const PostStatusForm = {
|
|||
contentType
|
||||
}
|
||||
|
||||
if (this.statusId) {
|
||||
if (this.statusId || this.isRedraft) {
|
||||
const statusContentType = this.statusContentType || contentType
|
||||
statusParams = {
|
||||
spoilerText: this.subject || '',
|
||||
status: this.statusText || '',
|
||||
sensitiveIfSubject,
|
||||
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
||||
nsfw: this.statusIsSensitive || (sensitiveIfSubject && this.subject) || !!sensitiveByDefault,
|
||||
files: this.statusFiles || [],
|
||||
poll: this.statusPoll || {},
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
|
@ -180,9 +181,6 @@ const PostStatusForm = {
|
|||
userDefaultScope () {
|
||||
return this.$store.state.users.currentUser.default_scope
|
||||
},
|
||||
showAllScopes () {
|
||||
return !this.mergedConfig.minimalScopesMode
|
||||
},
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
|
@ -224,9 +222,6 @@ const PostStatusForm = {
|
|||
isOverLengthLimit () {
|
||||
return this.hasStatusLengthLimit && (this.charactersLeft < 0)
|
||||
},
|
||||
minimalScopesMode () {
|
||||
return this.$store.state.instance.minimalScopesMode
|
||||
},
|
||||
alwaysShowSubject () {
|
||||
return this.mergedConfig.alwaysShowSubjectInput
|
||||
},
|
||||
|
@ -259,7 +254,7 @@ const PostStatusForm = {
|
|||
return this.newStatus.files.length >= this.fileLimit
|
||||
},
|
||||
isEdit () {
|
||||
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
|
||||
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' && !this.isRedraft
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
|
@ -417,7 +412,7 @@ const PostStatusForm = {
|
|||
addMediaFile (fileInfo) {
|
||||
this.newStatus.files.push(fileInfo)
|
||||
|
||||
if (this.newStatus.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
|
||||
this.newStatus.nsfw = true
|
||||
}
|
||||
this.$emit('resize', { delayed: true })
|
||||
|
@ -497,7 +492,7 @@ const PostStatusForm = {
|
|||
})
|
||||
},
|
||||
onSubjectInput (e) {
|
||||
if (this.newStatus.sensitiveIfSubject) {
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject) {
|
||||
this.newStatus.nsfw = true
|
||||
}
|
||||
},
|
||||
|
@ -646,10 +641,8 @@ const PostStatusForm = {
|
|||
if (this.copyMessageScope === 'direct') {
|
||||
return this.copyMessageScope
|
||||
}
|
||||
if (this.$store.getters.mergedConfig.scopeCopy) {
|
||||
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
|
||||
return this.copyMessageScope
|
||||
}
|
||||
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
|
||||
return this.copyMessageScope
|
||||
}
|
||||
}
|
||||
return this.$store.state.users.currentUser.default_scope
|
||||
|
|
|
@ -188,7 +188,6 @@
|
|||
>
|
||||
<scope-selector
|
||||
v-if="!disableVisibilitySelector"
|
||||
:show-all="showAllScopes"
|
||||
:user-default="userDefaultScope"
|
||||
:original-scope="copyMessageScope"
|
||||
:initial-scope="newStatus.visibility"
|
||||
|
|
|
@ -28,7 +28,8 @@ const PostStatusModal = {
|
|||
},
|
||||
watch: {
|
||||
params (newVal, oldVal) {
|
||||
if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) {
|
||||
if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id') ||
|
||||
get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||
this.resettingForm = true
|
||||
this.$nextTick(() => {
|
||||
this.resettingForm = false
|
||||
|
|
|
@ -79,8 +79,16 @@ const registration = {
|
|||
|
||||
if (!this.v$.$invalid) {
|
||||
try {
|
||||
await this.signUp(this.user)
|
||||
this.$router.push({ name: 'friends' })
|
||||
const data = await this.signUp(this.user)
|
||||
if (data.me) {
|
||||
this.$router.push({ name: 'friends' })
|
||||
} else if (data.identifier === 'awaiting_approval') {
|
||||
this.$router.push({ name: 'registration-request-sent' })
|
||||
} else if (data.identifier === 'missing_confirmed_email') {
|
||||
this.$router.push({ name: 'awaiting-email-confirmation' })
|
||||
} else {
|
||||
console.warn('Unknown response from sign up', data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Registration failed: ', error)
|
||||
this.setCaptcha()
|
||||
|
|
|
@ -177,6 +177,7 @@
|
|||
<div
|
||||
v-if="accountApprovalRequired"
|
||||
class="form-group"
|
||||
:class="{ 'form-group--error': v$.user.reason.$error }"
|
||||
>
|
||||
<label
|
||||
class="form--label"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
computed: {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{{ $t('registration.request_sent_title') }}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>{{ $t('registration.request_sent') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./registration_request_sent.js"></script>
|
|
@ -0,0 +1,25 @@
|
|||
export default {
|
||||
props: ['relationship'],
|
||||
data () {
|
||||
return {
|
||||
inProgress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
label () {
|
||||
if (this.inProgress) {
|
||||
return this.$t('user_card.follow_progress')
|
||||
} else {
|
||||
return this.$t('user_card.remove_follower')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.inProgress = true
|
||||
this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => {
|
||||
this.inProgress = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<button
|
||||
class="btn button-default follow-button"
|
||||
:class="{ toggled: inProgress }"
|
||||
:disabled="inProgress"
|
||||
:title="$t('user_card.remove_follower')"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ label }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script src="./remove_follower_button.js"></script>
|
|
@ -2,8 +2,6 @@ import { unescape, flattenDeep } from 'lodash'
|
|||
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
|
||||
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||
import { marked } from 'marked'
|
||||
import markedMfm from 'marked-mfm'
|
||||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
|
||||
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
|
||||
|
@ -123,51 +121,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
const renderMisskeyMarkdown = (content) => {
|
||||
// Untangle code blocks from <br> tags and other html encodings
|
||||
const codeblocks = content.match(/(<br\/>)?(~~~|```)\w*<br\/>.+?<br\/>\2\1?/g)
|
||||
if (codeblocks) {
|
||||
codeblocks.forEach((pre) => {
|
||||
content = content.replace(pre,
|
||||
pre.replaceAll('<br/>', '\n')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
marked.use(markedMfm, {
|
||||
mangle: false,
|
||||
gfm: false,
|
||||
breaks: true
|
||||
})
|
||||
const mfmHtml = document.createElement('template')
|
||||
mfmHtml.innerHTML = marked.parse(content)
|
||||
|
||||
// Add options with set values to CSS
|
||||
if (mfmHtml.content.firstChild) {
|
||||
Array.from(mfmHtml.content.firstChild.getElementsByClassName('mfm')).map((el) => {
|
||||
if (el.dataset.speed) {
|
||||
el.style.animationDuration = el.dataset.speed
|
||||
}
|
||||
if (el.dataset.deg) {
|
||||
el.style.transform = `rotate(${el.dataset.deg}deg)`
|
||||
}
|
||||
if (Array.from(el.classList).includes('_mfm_font_')) {
|
||||
const font = Object.keys(el.dataset)[0]
|
||||
if (['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'].includes(font)) {
|
||||
el.style.fontFamily = font
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return mfmHtml.innerHTML
|
||||
}
|
||||
|
||||
// Processor to use with html_tree_converter
|
||||
const processItem = (item, index, array, what) => {
|
||||
// Handle text nodes - just add emoji
|
||||
|
@ -305,7 +258,7 @@ export default {
|
|||
return item
|
||||
}
|
||||
|
||||
const pass1 = convertHtmlToTree(this.mfm ? renderMisskeyMarkdown(html) : html).map(processItem)
|
||||
const pass1 = convertHtmlToTree(html).map(processItem)
|
||||
const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
|
||||
// DO NOT USE SLOTS they cause a re-render feedback loop here.
|
||||
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
|
||||
|
|
|
@ -13,6 +13,14 @@ library.add(
|
|||
faLockOpen
|
||||
)
|
||||
|
||||
const SCOPE_LEVELS = {
|
||||
'direct': 0,
|
||||
'private': 1,
|
||||
'local': 2,
|
||||
'unlisted': 2,
|
||||
'public': 3
|
||||
}
|
||||
|
||||
const ScopeSelector = {
|
||||
props: [
|
||||
'showAll',
|
||||
|
@ -57,11 +65,15 @@ const ScopeSelector = {
|
|||
},
|
||||
methods: {
|
||||
shouldShow (scope) {
|
||||
return this.showAll ||
|
||||
this.currentScope === scope ||
|
||||
this.originalScope === scope ||
|
||||
this.userDefault === scope ||
|
||||
scope === 'direct'
|
||||
if (!this.originalScope) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.originalScope === 'local') {
|
||||
return scope === 'direct' || scope === 'local'
|
||||
}
|
||||
|
||||
return SCOPE_LEVELS[scope] <= SCOPE_LEVELS[this.originalScope]
|
||||
},
|
||||
changeVis (scope) {
|
||||
this.currentScope = scope
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
class="fa-scale-110 fa-old-padding"
|
||||
/>
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="showPrivate"
|
||||
class="button-unstyled scope"
|
||||
|
@ -30,7 +29,6 @@
|
|||
class="fa-scale-110 fa-old-padding"
|
||||
/>
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="showUnlisted"
|
||||
class="button-unstyled scope"
|
||||
|
@ -44,7 +42,6 @@
|
|||
class="fa-scale-110 fa-old-padding"
|
||||
/>
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="showPublic"
|
||||
class="button-unstyled scope"
|
||||
|
@ -87,6 +84,7 @@
|
|||
min-width: 1.3em;
|
||||
min-height: 1.3em;
|
||||
text-align: center;
|
||||
margin-right: 0.4em;
|
||||
|
||||
&.selected svg {
|
||||
color: $fallback--lightText;
|
||||
|
|
|
@ -32,6 +32,7 @@ const SearchBar = {
|
|||
this.$emit('toggled', this.hidden)
|
||||
this.$nextTick(() => {
|
||||
if (!this.hidden) {
|
||||
this.searchTerm = undefined
|
||||
this.$refs.searchInput.focus()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
|
||||
.search-bar-input {
|
||||
flex: 1 0 auto;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.cancel-search {
|
||||
|
|
|
@ -12,7 +12,8 @@ export default {
|
|||
'path',
|
||||
'disabled',
|
||||
'options',
|
||||
'expert'
|
||||
'expert',
|
||||
'hideDefaultLabel'
|
||||
],
|
||||
computed: {
|
||||
pathDefault () {
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
|
||||
<template
|
||||
v-if="hideDefaultLabel !== true"
|
||||
>
|
||||
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
|
||||
</template>
|
||||
</option>
|
||||
</Select>
|
||||
<ModifiedIndicator :changed="isChanged" />
|
||||
|
|
|
@ -19,7 +19,7 @@ const SharedComputedObject = () => ({
|
|||
.map(key => [key, {
|
||||
get () { return this.$store.getters.mergedConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: key, value })
|
||||
this.$store.dispatch('setOption', { name: key, value, manual: true })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
|
@ -27,7 +27,7 @@ const SharedComputedObject = () => ({
|
|||
.map(key => ['serverSide_' + key, {
|
||||
get () { return this.$store.state.serverSideConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setServerSideOption', { name: key, value })
|
||||
this.$store.dispatch('setServerSideOption', { name: key, value, manual: true })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
|
|
|
@ -175,7 +175,6 @@ const SettingsModal = {
|
|||
return this.$store.state.config.expertLevel > 0
|
||||
},
|
||||
set (value) {
|
||||
console.log(value)
|
||||
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,10 @@
|
|||
position: absolute;
|
||||
right: 20px;
|
||||
padding-right: 10px;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,10 @@
|
|||
<div class="panel-body">
|
||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
<span
|
||||
id="unscrolled-content"
|
||||
class="extra-content"
|
||||
/>
|
||||
<div class="panel-footer settings-footer">
|
||||
<Popover
|
||||
class="export"
|
||||
|
@ -53,7 +57,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('general.close')"
|
||||
|
@ -65,7 +69,7 @@
|
|||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template v-slot:content="{close}">
|
||||
<template #content="{close}">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -103,14 +107,11 @@
|
|||
|
||||
<Checkbox
|
||||
:model-value="!!expertLevel"
|
||||
class="expertMode"
|
||||
@update:modelValue="expertLevel = Number($event)"
|
||||
>
|
||||
{{ $t("settings.expert_mode") }}
|
||||
</Checkbox>
|
||||
<span
|
||||
id="unscrolled-content"
|
||||
class="extra-content"
|
||||
/>
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="button-default logout-button"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import { filter, trim, debounce } from 'lodash'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
|
@ -27,13 +27,13 @@ const FilteringTab = {
|
|||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
set: debounce(function (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
|
|
|
@ -8,11 +8,12 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
|
|||
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
faGlobe, faSync
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faGlobe
|
||||
faGlobe,
|
||||
faSync
|
||||
)
|
||||
|
||||
const GeneralTab = {
|
||||
|
@ -48,6 +49,8 @@ const GeneralTab = {
|
|||
value: tab,
|
||||
label: this.$t(`user_card.${tab}`)
|
||||
})),
|
||||
profilesExpanded: false,
|
||||
newProfileName: '',
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
|
@ -88,8 +91,26 @@ const GeneralTab = {
|
|||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
}
|
||||
},
|
||||
settingsProfiles () {
|
||||
return (this.$store.state.instance.settingsProfiles || [])
|
||||
},
|
||||
settingsProfile: {
|
||||
get: function () { return this.$store.getters.mergedConfig.profile },
|
||||
set: function (val) {
|
||||
this.$store.dispatch('setOption', { name: 'profile', value: val })
|
||||
this.$store.dispatch('getSettingsProfile')
|
||||
}
|
||||
},
|
||||
settingsVersion () {
|
||||
return this.$store.getters.mergedConfig.profileVersion
|
||||
},
|
||||
translationLanguages () {
|
||||
return (this.$store.getters.mergedConfig.supportedTranslationLanguages.target || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
|
||||
const langs = this.$store.state.instance.supportedTranslationLanguages
|
||||
if (langs && langs.source) {
|
||||
return langs.source.map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
translationLanguage: {
|
||||
get: function () { return this.$store.getters.mergedConfig.translationLanguage },
|
||||
|
@ -105,6 +126,30 @@ const GeneralTab = {
|
|||
},
|
||||
setTranslationLanguage (value) {
|
||||
this.$store.dispatch('setOption', { name: 'translationLanguage', value })
|
||||
},
|
||||
toggleExpandedSettings () {
|
||||
this.profilesExpanded = !this.profilesExpanded
|
||||
},
|
||||
loadSettingsProfile (name) {
|
||||
this.$store.commit('setOption', { name: 'profile', value: name })
|
||||
this.$store.dispatch('getSettingsProfile', true)
|
||||
},
|
||||
createSettingsProfile () {
|
||||
this.$store.dispatch('setOption', { name: 'profile', value: this.newProfileName })
|
||||
this.$store.dispatch('setOption', { name: 'profileVersion', value: 1 })
|
||||
this.$store.dispatch('syncSettings')
|
||||
this.newProfileName = ''
|
||||
},
|
||||
forceSync () {
|
||||
this.$store.dispatch('getSettingsProfile')
|
||||
},
|
||||
refreshProfiles () {
|
||||
this.$store.dispatch('listSettingsProfiles')
|
||||
},
|
||||
deleteSettingsProfile (name) {
|
||||
if (confirm(this.$t('settings.settings_profile_delete_confirm'))) {
|
||||
this.$store.dispatch('deleteSettingsProfile', name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div :label="$t('settings.general')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher
|
||||
|
@ -10,6 +9,94 @@
|
|||
:set-language="val => language = val"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
v-if="user && (settingsProfiles.length > 0)"
|
||||
>
|
||||
<h2>{{ $t('settings.settings_profile') }}</h2>
|
||||
<p>
|
||||
{{ $t('settings.settings_profile_currently', { name: settingsProfile, version: settingsVersion }) }}
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="forceSync()"
|
||||
>
|
||||
{{ $t('settings.settings_profile_force_sync') }}
|
||||
</button>
|
||||
|
||||
</p>
|
||||
<div
|
||||
@click="toggleExpandedSettings"
|
||||
>
|
||||
<template
|
||||
v-if="profilesExpanded"
|
||||
>
|
||||
<button class="btn button-default">
|
||||
{{ $t('settings.settings_profiles_unshow') }}
|
||||
</button>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<button class="btn button-default">
|
||||
{{ $t('settings.settings_profiles_show') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<br>
|
||||
<template
|
||||
v-if="profilesExpanded"
|
||||
>
|
||||
|
||||
<div
|
||||
v-for="profile in settingsProfiles"
|
||||
:key="profile.id"
|
||||
class="settings-profile"
|
||||
>
|
||||
<h4>{{ profile.name }} ({{ profile.version }})</h4>
|
||||
<template
|
||||
v-if="settingsProfile === profile.name"
|
||||
>
|
||||
{{ $t('settings.settings_profile_in_use') }}
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="loadSettingsProfile(profile.name)"
|
||||
>
|
||||
{{ $t('settings.settings_profile_use') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="deleteSettingsProfile(profile.name)"
|
||||
>
|
||||
{{ $t('settings.settings_profile_delete') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<button class="btn button-default" @click="refreshProfiles()">
|
||||
{{ $t('settings.settings_profiles_refresh') }}
|
||||
<FAIcon icon="sync" @click="refreshProfiles()" />
|
||||
</button>
|
||||
<h3>{{ $t('settings.settings_profile_creation') }}</h3>
|
||||
<label for="settings-profile-new-name">
|
||||
{{ $t('settings.settings_profile_creation_new_name_label') }}
|
||||
</label>
|
||||
<input v-model="newProfileName" id="settings-profile-new-name">
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="createSettingsProfile"
|
||||
>
|
||||
{{ $t('settings.settings_profile_creation_submit') }}
|
||||
</button>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<BooleanSetting path="hideISP">
|
||||
{{ $t('settings.hide_isp') }}
|
||||
|
@ -450,7 +537,6 @@
|
|||
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
|
||||
<ScopeSelector
|
||||
class="scope-selector"
|
||||
:show-all="true"
|
||||
:user-default="serverSide_defaultScope"
|
||||
:initial-scope="serverSide_defaultScope"
|
||||
:on-scope-change="changeDefaultScope"
|
||||
|
@ -458,12 +544,6 @@
|
|||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="minimalScopesMode">
|
||||
{{ $t('settings.minimal_scopes_mode') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
|
||||
<BooleanSetting path="sensitiveByDefault">
|
||||
{{ $t('settings.sensitive_by_default') }}
|
||||
</BooleanSetting>
|
||||
|
@ -473,14 +553,6 @@
|
|||
{{ $t('settings.sensitive_if_subject') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="scopeCopy"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.scope_copy') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="alwaysShowSubjectInput"
|
||||
|
@ -508,14 +580,6 @@
|
|||
{{ $t('settings.post_status_content_type') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="minimalScopesMode"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.minimal_scopes_mode') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="alwaysShowNewPostButton"
|
||||
|
@ -546,3 +610,13 @@
|
|||
</template>
|
||||
|
||||
<script src="./general_tab.js"></script>
|
||||
<style lang="scss">
|
||||
.settings-profile {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#settings-profile-new-name {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -43,7 +43,9 @@ const ProfileTab = {
|
|||
bannerPreview: null,
|
||||
background: null,
|
||||
backgroundPreview: null,
|
||||
emailLanguage: this.$store.state.users.currentUser.language || ''
|
||||
emailLanguage: this.$store.state.users.currentUser.language || '',
|
||||
newPostTTLDays: this.$store.state.users.currentUser.status_ttl_days,
|
||||
expirePosts: this.$store.state.users.currentUser.status_ttl_days !== null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -123,7 +125,8 @@ const ProfileTab = {
|
|||
display_name: this.newName,
|
||||
fields_attributes: this.newFields.filter(el => el != null),
|
||||
bot: this.bot,
|
||||
show_role: this.showRole
|
||||
show_role: this.showRole,
|
||||
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
|
||||
|
@ -151,7 +154,7 @@ const ProfileTab = {
|
|||
return false
|
||||
},
|
||||
deleteField (index, event) {
|
||||
this.$delete(this.newFields, index)
|
||||
this.newFields.splice(index, 1)
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.expire-posts-days {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
|
|
@ -89,6 +89,20 @@
|
|||
{{ $t('settings.bot') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="expirePosts">
|
||||
{{ $t('settings.expire_posts_enabled') }}
|
||||
</Checkbox>
|
||||
<input
|
||||
v-model="newPostTTLDays"
|
||||
:disabled="!expirePosts"
|
||||
type="number"
|
||||
min="1"
|
||||
max="730"
|
||||
class="expire-posts-days"
|
||||
:placeholder="$t('settings.expire_posts_input_placeholder')"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<interface-language-switcher
|
||||
:prompt-text="$t('settings.email_language')"
|
||||
|
|
|
@ -753,7 +753,6 @@ export default {
|
|||
selected () {
|
||||
this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => {
|
||||
if (Array.isArray(s)) {
|
||||
console.log(s[0] === this.selected, this.selected)
|
||||
return s[0] === this.selected
|
||||
} else {
|
||||
return s.name === this.selected
|
||||
|
|
|
@ -284,7 +284,6 @@
|
|||
box-shadow: none;
|
||||
background: transparent;
|
||||
color: var(--faint, $fallback--faint);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.theme-color-cl,
|
||||
|
@ -318,11 +317,11 @@
|
|||
|
||||
.extra-content {
|
||||
.apply-container {
|
||||
padding-left: 15vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
justify-content: space-evenly;
|
||||
flex-grow: 1;
|
||||
|
||||
.btn {
|
||||
flex-grow: 1;
|
||||
min-height: 2em;
|
||||
|
|
|
@ -958,20 +958,22 @@
|
|||
v-if="isActive"
|
||||
to="#unscrolled-content"
|
||||
>
|
||||
<div class="apply-container">
|
||||
<button
|
||||
class="btn button-default submit"
|
||||
:disabled="!themeValid"
|
||||
@click="setCustomTheme"
|
||||
>
|
||||
{{ $t('general.apply') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearAll"
|
||||
>
|
||||
{{ $t('settings.style.switcher.reset') }}
|
||||
</button>
|
||||
<div class="panel-body settings-footer">
|
||||
<div class="apply-container">
|
||||
<button
|
||||
class="btn button-default submit"
|
||||
:disabled="!themeValid"
|
||||
@click="setCustomTheme"
|
||||
>
|
||||
{{ $t('general.apply') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearAll"
|
||||
>
|
||||
{{ $t('settings.style.switcher.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,8 @@ import {
|
|||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle,
|
||||
faList
|
||||
faList,
|
||||
faUserTie
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
|
@ -30,7 +31,8 @@ library.add(
|
|||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle,
|
||||
faList
|
||||
faList,
|
||||
faUserTie
|
||||
)
|
||||
|
||||
const SideDrawer = {
|
||||
|
@ -102,6 +104,9 @@ const SideDrawer = {
|
|||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
openModModal () {
|
||||
this.$store.dispatch('openModModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,6 +143,21 @@
|
|||
/> {{ $t("nav.about") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled -link -fullwidth"
|
||||
@click="openModModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="user-tie"
|
||||
/> {{ $t("nav.moderation") }}
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
@click="toggleDrawer"
|
||||
|
|
|
@ -460,6 +460,16 @@ const Status = {
|
|||
return 'globe'
|
||||
}
|
||||
},
|
||||
faviconAlt (status) {
|
||||
if (!status.user.instance) {
|
||||
return ''
|
||||
}
|
||||
const software = ((status.user.instance) && (status.user.instance.nodeinfo) && (status.user.instance.nodeinfo.software)) || {}
|
||||
if (software.name) {
|
||||
return `${status.user.instance.name} (${software.name || ''} ${software.version || ''})`
|
||||
}
|
||||
return ''
|
||||
},
|
||||
showError (error) {
|
||||
this.error = error
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../_variables.scss';
|
||||
@import "../../_variables.scss";
|
||||
|
||||
.Status {
|
||||
min-width: 0;
|
||||
|
@ -42,6 +42,10 @@
|
|||
display: flex;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
@ -130,6 +134,15 @@
|
|||
.heading-left {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
img {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.heading-right {
|
||||
|
@ -139,6 +152,7 @@
|
|||
.button-unstyled {
|
||||
padding: 5px;
|
||||
margin: -5px;
|
||||
height: min-content;
|
||||
|
||||
&:hover svg {
|
||||
color: $fallback--lightText;
|
||||
|
@ -185,7 +199,7 @@
|
|||
|
||||
.reply-to-popover {
|
||||
.reply-to:hover::before {
|
||||
content: '';
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
@ -195,13 +209,12 @@
|
|||
}
|
||||
|
||||
.faint-link:hover {
|
||||
// override default
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.-strikethrough {
|
||||
.reply-to::after {
|
||||
content: '';
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
@ -293,10 +306,12 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: left;
|
||||
margin-top: var(--status-margin, $status-margin);
|
||||
|
||||
> * {
|
||||
max-width: 4em;
|
||||
min-width: fit-content;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
@ -340,7 +355,7 @@
|
|||
margin-left: 0.2em;
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -387,7 +402,7 @@
|
|||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
|
|
|
@ -166,18 +166,21 @@
|
|||
>
|
||||
{{ status.user.name }}
|
||||
</h4>
|
||||
<router-link
|
||||
class="account-name"
|
||||
:title="status.user.screen_name_ui"
|
||||
:to="userProfileLink"
|
||||
>
|
||||
{{ status.user.screen_name_ui }}
|
||||
</router-link>
|
||||
<img
|
||||
v-if="!!(status.user && status.user.favicon)"
|
||||
class="status-favicon"
|
||||
:src="status.user.favicon"
|
||||
>
|
||||
<span class="nowrap">
|
||||
<router-link
|
||||
class="account-name"
|
||||
:title="status.user.screen_name_ui"
|
||||
:to="userProfileLink"
|
||||
>
|
||||
@{{ status.user.screen_name_ui }}
|
||||
</router-link>
|
||||
<img
|
||||
v-if="!!(status.user && status.user.favicon)"
|
||||
class="status-favicon"
|
||||
:src="status.user.favicon"
|
||||
:title="faviconAlt(status)"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="heading-right">
|
||||
|
@ -349,22 +352,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
ref="content"
|
||||
:status="status"
|
||||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
:focused="isFocused"
|
||||
:controlled-showing-tall="controlledShowingTall"
|
||||
:controlled-expanding-subject="controlledExpandingSubject"
|
||||
:controlled-showing-long-subject="controlledShowingLongSubject"
|
||||
:controlled-toggle-showing-tall="controlledToggleShowingTall"
|
||||
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
|
||||
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
||||
@mediaplay="addMediaPlaying($event)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
@parseReady="setHeadTailLinks"
|
||||
/>
|
||||
<div class="content">
|
||||
<StatusContent
|
||||
ref="content"
|
||||
class="status-content"
|
||||
:status="status"
|
||||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
:focused="isFocused"
|
||||
:controlled-showing-tall="controlledShowingTall"
|
||||
:controlled-expanding-subject="controlledExpandingSubject"
|
||||
:controlled-showing-long-subject="controlledShowingLongSubject"
|
||||
:controlled-toggle-showing-tall="controlledToggleShowingTall"
|
||||
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
|
||||
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
||||
@mediaplay="addMediaPlaying($event)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
@parseReady="setHeadTailLinks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="inConversation && !isPreview && replies && replies.length"
|
||||
|
@ -531,6 +537,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./status.js" ></script>
|
||||
<script src="./status.js"></script>
|
||||
|
||||
<style src="./status.scss" lang="scss"></style>
|
||||
|
|
|
@ -83,7 +83,7 @@ const StatusContent = {
|
|||
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
},
|
||||
translationLanguages () {
|
||||
return (this.$store.getters.mergedConfig.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
|
||||
return (this.$store.state.instance.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="StatusBody"
|
||||
:class="{ '-compact': compact }"
|
||||
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
|
||||
>
|
||||
<div class="body">
|
||||
<div
|
||||
|
|
|
@ -82,9 +82,16 @@
|
|||
}
|
||||
}
|
||||
&.mfm-disabled {
|
||||
span {
|
||||
font-size: 100% !important;
|
||||
}
|
||||
.mfm {
|
||||
animation: none !important;
|
||||
}
|
||||
.emoji {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,13 @@ const StillImage = {
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
stopGifs: this.$store.getters.mergedConfig.stopGifs
|
||||
stopGifs: this.$store.getters.mergedConfig.stopGifs,
|
||||
isAnimated: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
animated () {
|
||||
return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif'))
|
||||
return this.stopGifs && this.isAnimated
|
||||
},
|
||||
style () {
|
||||
const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str
|
||||
|
@ -31,17 +32,89 @@ const StillImage = {
|
|||
const image = this.$refs.src
|
||||
if (!image) return
|
||||
this.imageLoadHandler && this.imageLoadHandler(image)
|
||||
this.detectAnimation(image)
|
||||
this.drawThumbnail()
|
||||
},
|
||||
onError () {
|
||||
this.imageLoadError && this.imageLoadError()
|
||||
},
|
||||
detectAnimation (image) {
|
||||
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
|
||||
this.isAnimated = true
|
||||
return
|
||||
}
|
||||
// harmless CORS errors without-- clean console with
|
||||
if (!this.$store.state.instance.mediaProxyAvailable) return
|
||||
// Animated JPEGs?
|
||||
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return
|
||||
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
||||
fetch(image.src, {
|
||||
referrerPolicy: 'same-origin'
|
||||
})
|
||||
.then(data => {
|
||||
// We don't need to read the whole file so only call it once
|
||||
data.body.getReader().read()
|
||||
.then(reader => {
|
||||
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) {
|
||||
this.isAnimated = true
|
||||
return
|
||||
}
|
||||
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) {
|
||||
this.isAnimated = true
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
// this.imageLoadError && this.imageLoadError()
|
||||
})
|
||||
},
|
||||
isAnimatedWEBP (data) {
|
||||
/**
|
||||
* WEBP HEADER CHUNK
|
||||
* === START HEADER ===
|
||||
* 82 73 70 70 ("RIFF")
|
||||
* xx xx xx xx (SIZE)
|
||||
* 87 69 66 80 ("WEBP")
|
||||
* === END OF HEADER ===
|
||||
* 86 80 56 88 ("VP8X") ← Extended VP8X
|
||||
* xx xx xx xx (VP8X)
|
||||
* [++] ← RSVILEX(A)R (1 byte)
|
||||
* A → Animated bit
|
||||
*/
|
||||
// Relevant bytes
|
||||
const segment = data.slice(4 * 3, (4 * 5) + 1)
|
||||
// Check for VP8X string
|
||||
if (segment.join('').includes(['86805688'])) {
|
||||
// Check for Animation bit
|
||||
return !!((segment[8] >> 1) & 1)
|
||||
}
|
||||
// No VP8X = Not Animated (X is for Extended)
|
||||
return false
|
||||
},
|
||||
isAnimatedPNG (data) {
|
||||
// Find acTL before IDAT in PNG; if found it is animated
|
||||
const segment = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
segment.push(String.fromCharCode(data[i]))
|
||||
}
|
||||
const str = segment.join('')
|
||||
const idatPos = str.indexOf('IDAT')
|
||||
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
||||
},
|
||||
drawThumbnail () {
|
||||
const canvas = this.$refs.canvas
|
||||
if (!canvas) return
|
||||
if (!this.$refs.canvas) return
|
||||
const image = this.$refs.src
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
|
||||
},
|
||||
onError () {
|
||||
this.imageLoadError && this.imageLoadError()
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
// On computed animated change
|
||||
this.drawThumbnail()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,8 +64,12 @@ export default {
|
|||
settingsModalVisible () {
|
||||
return this.settingsModalState === 'visible'
|
||||
},
|
||||
modModalVisible () {
|
||||
return this.modModalState === 'visible'
|
||||
},
|
||||
...mapState({
|
||||
settingsModalState: state => state.interface.settingsModalState
|
||||
settingsModalState: state => state.interface.settingsModalState,
|
||||
modModalState: state => state.interface.modModalState
|
||||
})
|
||||
},
|
||||
beforeUpdate () {
|
||||
|
|
|
@ -6,11 +6,13 @@ import TimelineMenuTabs from '../timeline_menu_tabs/timeline_menu_tabs.vue'
|
|||
import TimelineQuickSettings from './timeline_quick_settings.vue'
|
||||
import { debounce, throttle, keyBy } from 'lodash'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCircleNotch, faCog, faPlus, faMinus } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch,
|
||||
faCog
|
||||
faCog,
|
||||
faPlus,
|
||||
faMinus
|
||||
)
|
||||
|
||||
const Timeline = {
|
||||
|
@ -90,6 +92,15 @@ const Timeline = {
|
|||
},
|
||||
showPanelNavShortcuts () {
|
||||
return this.$store.getters.mergedConfig.showPanelNavShortcuts
|
||||
},
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
tagData () {
|
||||
return this.$store.state.tags.tags[this.tag]
|
||||
},
|
||||
tagFollowed () {
|
||||
return this.$store.state.tags.tags[this.tag]?.following
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -118,6 +129,10 @@ const Timeline = {
|
|||
}
|
||||
window.addEventListener('keydown', this.handleShortKey)
|
||||
setTimeout(this.determineVisibleStatuses, 250)
|
||||
|
||||
if (this.tag) {
|
||||
this.$store.dispatch('getTag', this.tag)
|
||||
}
|
||||
},
|
||||
unmounted () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
|
@ -126,7 +141,7 @@ const Timeline = {
|
|||
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||
},
|
||||
methods: {
|
||||
stopBlockingClicks: debounce(function () {
|
||||
stopBlockingClicks: debounce( function() {
|
||||
this.blockingClicks = false
|
||||
}, 1000),
|
||||
blockClicksTemporarily () {
|
||||
|
@ -154,7 +169,7 @@ const Timeline = {
|
|||
window.scrollTo({ top: 0 })
|
||||
}
|
||||
},
|
||||
fetchOlderStatuses: throttle(function () {
|
||||
fetchOlderStatuses: throttle( function () {
|
||||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
store.commit('setLoading', { timeline: this.timelineName, value: true })
|
||||
|
@ -188,7 +203,7 @@ const Timeline = {
|
|||
|
||||
const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5)
|
||||
|
||||
// Start from approximating the index of some visible status by using the
|
||||
// Start from approximating the index of some visible status by using
|
||||
// the center of the screen on the timeline.
|
||||
let approxIndex = Math.floor(statuses.length * (centerOfScreen / height))
|
||||
let err = statuses[approxIndex].getBoundingClientRect().y
|
||||
|
@ -226,12 +241,18 @@ const Timeline = {
|
|||
this.fetchOlderStatuses()
|
||||
}
|
||||
},
|
||||
handleScroll: throttle(function (e) {
|
||||
handleScroll: throttle( function (e) {
|
||||
this.determineVisibleStatuses()
|
||||
this.scrollLoad(e)
|
||||
}, 200),
|
||||
handleVisibilityChange () {
|
||||
this.unfocused = document.hidden
|
||||
},
|
||||
followTag (tag) {
|
||||
return this.$store.dispatch('followTag', tag)
|
||||
},
|
||||
unfollowTag (tag) {
|
||||
return this.$store.dispatch('unfollowTag', tag)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -21,6 +21,36 @@
|
|||
{{ $t('timeline.up_to_date') }}
|
||||
</div>
|
||||
<TimelineQuickSettings v-if="!embedded" />
|
||||
<div
|
||||
v-if="currentUser && tag !== undefined && tagData && !tagFollowed"
|
||||
class="followTag"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
:title="$t('timeline.follow_tag')"
|
||||
@click="followTag(tag)"
|
||||
>
|
||||
<FAIcon
|
||||
size="sm"
|
||||
icon="plus"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentUser && tag !== undefined && tagData && tagFollowed"
|
||||
class="followTag"
|
||||
>
|
||||
<button
|
||||
class="button-default"
|
||||
:title="$t('timeline.unfollow_tag')"
|
||||
@click="unfollowTag(tag)"
|
||||
>
|
||||
<FAIcon
|
||||
size="sm"
|
||||
icon="minus"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="classes.body">
|
||||
<div
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue