resolve merge conflicts

This commit is contained in:
Puniko 2022-11-20 08:37:26 +01:00
commit b6280ae97d
83 changed files with 3622 additions and 3523 deletions

View File

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

View File

@ -3,7 +3,7 @@ pipeline:
when:
event:
- pull_request
image: node:16
image: node:18
commands:
- yarn
- yarn lint
@ -13,7 +13,7 @@ pipeline:
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

24
CODE_OF_CONDUCT.md Normal file
View File

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

View File

@ -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$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""
"""""""""""""""""""""""""""""""""""""""""""""""""""""
```

View File

@ -28,18 +28,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) {

View File

@ -2,8 +2,8 @@ var path = require('path')
var config = require('../config')
var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
var { VueLoaderPlugin } = require("vue-loader");
const WorkboxPlugin = require('workbox-webpack-plugin');
var { VueLoaderPlugin } = require('vue-loader')
var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
@ -19,6 +19,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'
@ -33,6 +34,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'),
@ -115,9 +119,10 @@ module.exports = {
]
},
plugins: [
new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js'
new WorkboxPlugin.InjectManifest({
swSrc: path.join(__dirname, '..', 'src/sw.js'),
swDest: 'sw-pleroma.js',
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
}),
new VueLoaderPlugin()
]

View File

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

View File

@ -2,7 +2,7 @@ var path = require('path')
var config = require('../config')
var utils = require('./utils')
var webpack = require('webpack')
var merge = require('webpack-merge')
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 +19,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: {
@ -62,7 +62,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

View File

@ -1,4 +1,4 @@
var merge = require('webpack-merge')
var { merge } = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {

View File

@ -1,4 +1,4 @@
var merge = require('webpack-merge')
var { merge } = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {

View File

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

View File

@ -3,7 +3,7 @@
<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">
@ -13,7 +13,7 @@
<link rel="icon" type="image/png" href="/favicon.png">
</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 -->

View File

@ -33,13 +33,13 @@
"escape-html": "1.0.3",
"js-cookie": "^3.0.1",
"localforage": "1.10.0",
"marked": "^4.0.17",
"marked": "^4.2.2",
"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 +48,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 +59,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 +95,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",
@ -106,27 +105,27 @@
"ora": "0.4.1",
"postcss-loader": "3.0.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"
"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"
}
}

View File

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

View File

@ -61,6 +61,7 @@
<EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal />
<ModModal />
<GlobalNoticeList />
</div>
</template>

View File

@ -150,6 +150,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('showWiderShortcuts')
copyInstanceOption('showNavShortcuts')
copyInstanceOption('showPanelNavShortcuts')
copyInstanceOption('stopGifs')
copyInstanceOption('logo')
store.dispatch('setInstanceOption', {
@ -171,10 +172,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')
@ -398,6 +397,7 @@ 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 })

View File

@ -225,7 +225,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)
}
}
}

View File

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

View File

@ -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 {
@ -98,6 +100,9 @@ export default {
privateMode () { return this.$store.state.instance.private },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
showBubbleTimeline () {
return this.$store.state.instance.localBubbleInstances.length > 0
}
},
methods: {
@ -109,6 +114,9 @@ export default {
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
},
openModModal () {
this.$store.dispatch('openModModal')
}
}
}

View File

@ -55,7 +55,7 @@
/>
</router-link>
<router-link
v-if="currentUser"
v-if="currentUser && showBubbleTimeline"
:to="{ name: 'bubble-timeline' }"
class="nav-icon"
>
@ -151,6 +151,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"

View File

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

View File

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

View File

@ -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 []
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -148,7 +148,7 @@ const PostStatusForm = {
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 || {},
@ -181,9 +181,6 @@ const PostStatusForm = {
userDefaultScope () {
return this.$store.state.users.currentUser.default_scope
},
showAllScopes () {
return !this.mergedConfig.minimalScopesMode
},
emojiUserSuggestor () {
return suggestor({
emoji: [
@ -225,9 +222,6 @@ const PostStatusForm = {
isOverLengthLimit () {
return this.hasStatusLengthLimit && (this.charactersLeft < 0)
},
minimalScopesMode () {
return this.$store.state.instance.minimalScopesMode
},
alwaysShowSubject () {
return this.mergedConfig.alwaysShowSubjectInput
},
@ -419,7 +413,7 @@ const PostStatusForm = {
this.$emit('resize')
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 })
@ -500,7 +494,7 @@ const PostStatusForm = {
})
},
onSubjectInput (e) {
if (this.newStatus.sensitiveIfSubject) {
if (this.$store.getters.mergedConfig.sensitiveIfSubject) {
this.newStatus.nsfw = true
}
},
@ -649,10 +643,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

View File

@ -188,7 +188,6 @@
>
<scope-selector
v-if="!disableVisibilitySelector"
:show-all="showAllScopes"
:user-default="userDefaultScope"
:original-scope="copyMessageScope"
:initial-scope="newStatus.visibility"

View File

@ -123,51 +123,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('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot', '"')
.replaceAll('&#39;', "'")
)
})
}
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 +260,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 -> ...

View File

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

View File

@ -32,6 +32,7 @@ const SearchBar = {
this.$emit('toggled', this.hidden)
this.$nextTick(() => {
if (!this.hidden) {
this.searchTerm = undefined
this.$refs.searchInput.focus()
}
})

View File

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

View File

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

View File

@ -105,8 +105,12 @@ const GeneralTab = {
return this.$store.getters.mergedConfig.profileVersion
},
translationLanguages () {
const langs = this.$store.state.instance.translationLanguages || []
return (langs || []).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 },

View File

@ -567,18 +567,12 @@
{{ $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"
/>
</label>
</li>
<li>
<BooleanSetting path="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
@ -589,14 +583,6 @@
{{ $t('settings.sensitive_if_subject') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="scopeCopy"
expert="1"
>
{{ $t('settings.scope_copy') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowSubjectInput"
@ -624,14 +610,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"

View File

@ -151,7 +151,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]

View File

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

View File

@ -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')
}
}
}

View File

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

View File

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

View File

@ -177,6 +177,7 @@
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"
:src="status.user.favicon"
:title="faviconAlt(status)"
>
</div>

View File

@ -1,7 +1,7 @@
<template>
<div
class="StatusBody"
:class="{ '-compact': compact }"
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
>
<div class="body">
<div

View File

@ -82,9 +82,16 @@
}
}
&.mfm-disabled {
span {
font-size: 100% !important;
}
.mfm {
animation: none !important;
}
.emoji {
width: 32px !important;
height: 32px !important;
}
}
}

View File

@ -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 () {

View File

@ -126,7 +126,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 +154,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 +188,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,7 +226,7 @@ const Timeline = {
this.fetchOlderStatuses()
}
},
handleScroll: throttle(function (e) {
handleScroll: throttle( function (e) {
this.determineVisibleStatuses()
this.scrollLoad(e)
}, 200),

View File

@ -71,7 +71,6 @@ export default {
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
},
loggedIn () {
console.log({ ...this.$store.state.users.currentUser })
return this.$store.state.users.currentUser
},
dailyAvg () {

View File

@ -235,7 +235,7 @@
line-height: 22px;
flex-wrap: wrap;
.following {
.following, .requested_by {
flex: 1 0 auto;
margin: 0;
margin-bottom: .25em;

View File

@ -122,6 +122,12 @@
>
{{ $t('user_card.follows_you') }}
</div>
<div
v-if="relationship.requested_by && loggedIn && isOtherUser"
class="requested_by"
>
{{ $t('user_card.requested_by') }}
</div>
<div
v-if="isOtherUser && (loggedIn || !switcher)"
class="highlighter"

View File

@ -164,6 +164,74 @@
"load_older": "Carrega interaccions més antigues",
"moves": "Migracions d'usuari"
},
"languages": {
"ar": "Àrab",
"az": "Azerbaidjanès",
"bg": "Búlgar",
"cs": "Txec",
"da": "Danès",
"de": "Alemany",
"el": "Grec",
"en": "Anglès",
"eo": "Esperanto",
"es": "Espanyol",
"fa": "Persa",
"fi": "Finès",
"fr": "Francès",
"ga": "Irlandès",
"he": "Hebreu",
"hi": "Indi",
"hu": "Hungarès",
"id": "Indonesi",
"it": "Italià",
"ja": "Japonès",
"ko": "Coreà",
"lt": "Lituà",
"lv": "Latvià",
"nl": "Holandès",
"pl": "Polonès",
"pt": "Portuguès",
"ru": "Rus",
"sk": "Eslovè",
"sv": "Suec",
"tr": "Turc",
"translated_from": {
"ar": "Traduït de @:languages.ar",
"az": "Traduït de @:languages.az",
"bg": "Traduït de @:languages.bg",
"cs": "Traduït de @:languages.cs",
"da": "Traduït de @:languages.da",
"de": "Traduït de @:languages.de",
"el": "Traduït de @:languages.el",
"en": "Traduït de @:languages.en",
"eo": "Traduït de @:languages.eo",
"es": "Traduït de @:languages.es",
"fa": "Traduït de @:languages.fa",
"fi": "Traduït de @:languages.en",
"fr": "Traduït de @:languages.fr",
"ga": "Traduït de @:languages.ga",
"he": "Traduït de @:languages.he",
"hi": "Traduït de @:languages.hi",
"hu": "Traduït de @:languages.hu",
"id": "Traduït de @:languages.id",
"it": "Traduït de @:languages.it",
"ja": "Traduït de @:languages.ja",
"ko": "Traduït de @:languages.ko",
"lt": "Traduït de @:languages.lt",
"lv": "Traduït de @:languages.lv",
"nl": "Traduït de @:languages.nl",
"pl": "Traduït de @:languages.pl",
"pt": "Traduït de @:languages.pt",
"ru": "Traduït de @:languages.ru",
"sk": "Traduït de @:languages.sk",
"sv": "Traduït de @:languages.sv",
"tr": "Traduït de @:languages.fr",
"uk": "Traduït de @:languages.uk",
"zh": "Traduït de @:languages.zh"
},
"uk": "Ucraïnès",
"zh": "Xinès"
},
"lists": {
"create": "Crea",
"delete": "Esborra la llista",
@ -279,10 +347,13 @@
"text/plain": "Text pla",
"text/x.misskeymarkdown": "MFM"
},
"content_warning": "Assumpte (opcional)",
"content_warning": "Avís de contingut (opcional)",
"default": "Just ara he arribat a Catalunya",
"direct_warning_to_all": "Aquest apunt serà visible per a tots els usuaris mencionats.",
"direct_warning_to_first_only": "Aquest apunt només serà visible per als usuaris mencionats al principi del missatge.",
"edit_remote_warning": "Els canvis fets en aquest apunt poden no ser visibles en algunes instàncies!",
"edit_status": "Edita l'apunt",
"edit_unsupported_warning": "Les enquestes i les mencions no es canviaran al editar.",
"empty_status_error": "No es pot enviar un apunt buit i sense fitxers adjunts",
"media_description": "Descripció multimèdia",
"media_description_error": "Ha fallat la pujada del Mèdia, prova de nou",
@ -313,7 +384,7 @@
"email": "Adreça de Correu",
"email_language": "En quina llengua vols rebre els correus del servidor?",
"fullname": "Nom a mostrar",
"fullname_placeholder": "p. ex. Lain Iwakura",
"fullname_placeholder": "p. ex. Atsuko Kagari",
"new_captcha": "Clica a la imatge per a obtenir un nou captcha",
"password_confirm": "Confirma la contrasenya",
"reason": "Raó per a registrar-se",
@ -321,7 +392,7 @@
"register": "Registre",
"registration": "Registre",
"token": "Codi d'invitació",
"username_placeholder": "p. ex. lain",
"username_placeholder": "p. ex. akko",
"validations": {
"email_required": "no es pot deixar en blanc",
"fullname_required": "no es pot deixar en blanc",
@ -392,9 +463,19 @@
"changed_password": "S'ha canviat la contrasenya correctament!",
"chatMessageRadius": "Missatge de xat",
"checkboxRadius": "Caselles",
"collapse_subject": "Replega els apunts amb assumpte",
"collapse_subject": "Replega els apunts amb avís de contingut",
"columns": "Columnes",
"composing": "Composant",
"confirm_dialogs": "Requereix confirmació per:",
"confirm_dialogs_approve_follow": "Acceptant peticions de seguiment",
"confirm_dialogs_block": "Bloquejant algú",
"confirm_dialogs_delete": "Esborrant un apunt",
"confirm_dialogs_deny_follow": "Rebutjant una sol·licitud de seguiment",
"confirm_dialogs_mute": "Silenciant algú",
"confirm_dialogs_repeat": "Repetint un apunt",
"confirm_dialogs_unfollow": "Deixant de seguir a algú",
"confirm_new_password": "Confirma la nova contrasenya",
"confirmation_dialogs": "Opcions de confirmació",
"conversation_display": "Estil de visualització de la conversa",
"conversation_display_linear": "Estil linear",
"conversation_display_tree": "Estil d'arbre",
@ -426,8 +507,8 @@
"backup_settings_theme": "Còpia de seguretat de la configuració i tema a un fitxer",
"errors": {
"file_slightly_new": "La versió menor del fitxer és diferent, alguns paràmetres podrien no carregar-se",
"file_too_new": "Versió important incompatible: {fileMajor}, aquest PleromaFE (configuració versió {feMajor}) és massa antiga per gestionar-lo",
"file_too_old": "Versió important incompatible: {fileMajor}, la versió del fitxer és massa antiga i no està suportada (min. set. ver. {feMajor})",
"file_too_new": "Versió major incompatible: {fileMajor}, aquest PleromaFE (configuració versió {feMajor}) és massa antiga per gestionar-lo",
"file_too_old": "Versió major incompatible: {fileMajor}, la versió del fitxer és massa antiga i no està suportada (min. set. ver. {feMajor})",
"invalid_file": "El fitxer seleccionat no és suportat per Akkoma com a còpia de seguretat de la configuració. No s'ha realitzat cap canvi."
},
"restore_settings": "Restaurar configuració des d'un fitxer"
@ -520,7 +601,7 @@
"more_settings": "Més opcions",
"move_account": "Mou el compte",
"move_account_error": "Error al moure el compte: {error}",
"move_account_notes": "Si vols moure el compte a un altre lloc has d'anar a aquest altre compte i afegir un àlies que apunti a aquest.",
"move_account_notes": "Si vols moure aquest compte a un altre lloc, has d'anar a aquest altre compte i afegir un àlies que apunti a aquest.",
"move_account_target": "Compte destí (p.ex. {example})",
"moved_account": "El compte s'ha mogut.",
"mute_bot_posts": "Silencia apunts de bots",
@ -604,7 +685,7 @@
"security": "Seguretat",
"security_tab": "Seguretat",
"sensitive_by_default": "Marca apunts com a sensibles per defecte",
"sensitive_if_subject": "Marca automàticament les imatges com a sensibles si s'ha especificat la línia assumpte",
"sensitive_if_subject": "Marca automàticament les imatges com a sensibles si s'ha especificat un avís de contingut",
"set_new_avatar": "Establir un nou avatar",
"set_new_mascot": "Establir una nova mascota",
"set_new_profile_background": "Canvia el fons del perfil",
@ -612,6 +693,19 @@
"setting_changed": "La configuració és diferent a la predeterminada",
"setting_server_side": "Aquest ajust està lligat al teu perfil i afectarà a totes les sessions i clients",
"settings": "Configuració",
"settings_profile": "Perfils de configuracions",
"settings_profile_creation": "Crea nou perfil",
"settings_profile_creation_new_name_label": "Nom",
"settings_profile_creation_submit": "Crea",
"settings_profile_currently": "Actualment usant {name} (versió: {version})",
"settings_profile_delete": "Esborra",
"settings_profile_delete_confirm": "Vols de debò esborrar aquest perfil?",
"settings_profile_force_sync": "Sincronitza",
"settings_profile_in_use": "En ús",
"settings_profile_use": "Usa",
"settings_profiles_refresh": "Recarrega els perfils de configuracions",
"settings_profiles_show": "Mostra tots els perfils de configuracions",
"settings_profiles_unshow": "Amaga tots els perfils de configuracions",
"show_admin_badge": "Mostra l'insígnia \"Administrador\" en el meu perfil",
"show_moderator_badge": "Mostra l'insígnia \"Moderador\" en el meu perfil",
"show_nav_shortcuts": "Mostra els accessos directes addicionals en el panell superior",
@ -767,9 +861,9 @@
"use_source": "Nova versió"
}
},
"subject_input_always_show": "Sempre mostrar el camp del assumpte",
"subject_line_behavior": "Copiar l'assumpte en les respostes",
"subject_line_email": "Com a l'email: \"re: assumpte\"",
"subject_input_always_show": "Sempre mostra el camp d'avís de contingut",
"subject_line_behavior": "Copiar l'avís de contingut en les respostes",
"subject_line_email": "Com en el correu: \"re: avís\"",
"subject_line_mastodon": "Com a mastodon: copiar com és",
"subject_line_noop": "No copiar",
"text": "Text",
@ -783,7 +877,8 @@
"third_column_mode_postform": "Formulari de publicació principal i navegació",
"token": "Token",
"tooltipRadius": "Globus/alertes",
"tree_advanced": "Permet una navegació més flexible en la vista d'arbre",
"translation_language": "Traducció Automàtica de la Llengua",
"tree_advanced": "Mostra els botons extra per a obrir i tancar les cadenes en els fils",
"tree_fade_ancestors": "Mostra els avantpassats del apunt actual en text dèbil",
"type_domains_to_mute": "Buscar dominis per a silenciar",
"upload_a_photo": "Pujar una foto",
@ -793,6 +888,7 @@
"use_contain_fit": "No retallar els adjunts en miniatures",
"use_one_click_nsfw": "Obre els adjunts NSFW amb només un clic",
"user_mutes": "Usuaris",
"user_profile_default_tab": "Pestanya per defecte en el Perfil d'Usuari",
"user_profiles": "Perfils d'usuari",
"user_settings": "Configuració d'usuari",
"valid_until": "Vàlid fins",
@ -809,6 +905,12 @@
"word_filter": "Filtre de paraules",
"wordfilter": "Filtre de paraules"
},
"settings_profile": {
"creating": "Creant nou perfil de configuracions \"{profile}\"...",
"synchronization_error": "No s'han pogut sincronitzar les configuracions: {err}",
"synchronized": "Configuracions sincronitzades!",
"synchronizing": "Sincronitzant el perfil de configuració \"{profile}\"..."
},
"status": {
"ancestor_follow": "Veure {numReplies} altres respostes sota aquest apunt | Veure {numReplies} altres respostes sota aquest apunt",
"ancestor_follow_with_icon": "{icon} {text}",
@ -818,12 +920,19 @@
"copy_link": "Copia l'enllaç a l'apunt",
"delete": "Esborra l'apunt",
"delete_confirm": "Segur que vols esborrar aquest apunt?",
"delete_confirm_accept_button": "Sí, esborra'l",
"delete_confirm_cancel_button": "No, desa'l",
"delete_confirm_title": "Confirma esborrat",
"edit": "Edita",
"edit_history": "Historial d'edició",
"edit_history_modal_title": "Editat {historyCount} vegada | Editat {historyCount} vegades",
"edited_at": "Editat {time}",
"expand": "Expandeix",
"external_source": "Font externa",
"favorites": "Favorits",
"hide_attachment": "Amaga l'adjunt",
"hide_content": "Amaga el contingut",
"hide_full_subject": "Amaga tot l'assumpte",
"hide_full_subject": "Amaga tot l'avís de contingut",
"many_attachments": "L'apunt té {number} adjunt | L'apunt té {number} adjunts",
"mentions": "Menciona",
"move_down": "Mou l'adjunt a la dreta",
@ -831,32 +940,44 @@
"mute_conversation": "Silencia la conversa",
"nsfw": "No segur per a entorns laborals",
"open_gallery": "Obre la galeria",
"override_translation_source_language": "Anul·la la llengua d'origen",
"pin": "Destaca al perfil",
"pinned": "Destacat",
"plus_more": "+{number} més",
"redraft": "Esborra i torna a escriure",
"redraft_confirm": "De debò vols esborrar i tornar a escriure aquest apunt? Les interaccions del apunt original no es mantindran.",
"redraft_confirm_accept_button": "Sí, esborra i redacta de nou",
"redraft_confirm_cancel_button": "No, manté l'original",
"redraft_confirm_title": "Confirma esborra i re escriu",
"remove_attachment": "Elimina l'adjunt",
"repeat_confirm": "Vols de debò repetir aquest apunt?",
"repeat_confirm_accept_button": "Sí, repeteix-lo",
"repeat_confirm_cancel_button": "No, no el repeteixis",
"repeat_confirm_title": "Confirma repetició",
"repeats": "Repeticions",
"replies_list": "Respostes:",
"replies_list_with_others": "Respostes (+{numReplies} altre): | Respostes (+{numReplies} altres):",
"replies_list_with_others": "Veure (+{numReplies} resposta més): | Veure (+{numReplies} respostes més):",
"reply_to": "Respon a",
"show_all_attachments": "Mostra tots els adjunts",
"show_all_conversation": "Mostra la conversa sencera ({numStatus} altre apunt) | Mostra la conversa sencera ({numStatus} altres apunts)",
"show_all_conversation_with_icon": "{icon} {text}",
"show_attachment_description": "Descripció prèvia (obre l'adjunt per a descripció sencera)",
"show_attachment_in_modal": "Mostra en el modal de Mèdia",
"show_attachment_in_modal": "Mostra l'adjunt en un finestra",
"show_content": "Mostra el contingut",
"show_full_subject": "Mostra tot l'assumpte",
"show_full_subject": "Mostra tot l'avís de contingut",
"show_only_conversation_under_this": "Només mostra respostes a aquest apunt",
"status_deleted": "Aquest apunt ha estat esborrat",
"status_unavailable": "Apunt no disponible",
"thread_follow": "Mira la part restant d'aquest fil ({numStatus} apunt en total) | Mira la part restant d'aquest fil ({numStatus} apunts en total)",
"thread_follow": "Veure {numStatus} resposta més | Veure {numStatus} respostes més",
"thread_follow_with_icon": "{icon} {text}",
"thread_hide": "Amaga aquest fil",
"thread_muted": "Fil silenciat",
"thread_muted_and_words": ", té les paraules:",
"thread_show": "Mostra aquest fil",
"thread_show_full": "Mostra-ho tot sota aquest fil ({numStatus} apunt en total, màx. profunditat {depth}) | Mostra-ho tot sota aquest fil ({numStatus} apunts en total, màx. profunditat {depth})",
"thread_show_full": "Mostra {numStatus} resposta | Mostra totes {numStatus} respostes",
"thread_show_full_with_icon": "{icon} {text}",
"translate": "Tradueix",
"translated_from": "Tradueix des de {language}",
"unbookmark": "Desmarca",
"unmute_conversation": "Deixa de silenciar la conversa",
"unpin": "Deixa de destacar al perfil",
@ -899,6 +1020,9 @@
"socket_reconnected": "Connexió a temps real establerta",
"up_to_date": "Actualitzat"
},
"toast": {
"no_translation_target_set": "No s'ha configurat una llengua objectiu -això podria fallar. Si us plau configura una llengua objectiu en la teva configuració."
},
"tool_tip": {
"accept_follow_request": "Accepta la sol·licitud de seguiment",
"add_reaction": "Reacciona",
@ -947,12 +1071,24 @@
"strip_media": "Esborra els Mèdia dels apunts"
},
"approve": "Aprova",
"approve_confirm": "Estàs segur que vols deixar que et segueixi aquest usuari?",
"approve_confirm_accept_button": "Sí, accepta",
"approve_confirm_cancel_button": "No, cancel·la",
"approve_confirm_title": "Aprova la sol·licitud de seguiment",
"block": "Bloqueja",
"block_confirm": "Estàs segur que vols bloquejar a {user}?",
"block_confirm_accept_button": "Sí, bloqueja'l",
"block_confirm_cancel_button": "No, no el bloquegis",
"block_confirm_title": "Bloqueja l'usuari",
"block_progress": "Bloquejant…",
"blocked": "Bloquejat!",
"bot": "Bot",
"deactivated": "Desactivat",
"deny": "Denega",
"deny_confirm": "Estàs segur que vols denegar la sol·licitud de seguiment d'aquest usuari?",
"deny_confirm_accept_button": "Sí, denega",
"deny_confirm_cancel_button": "No, cancel·la",
"deny_confirm_title": "Denega sol·licitud de seguiment",
"domain_muted": "Desbloqueja el domini",
"edit_profile": "Edita el perfil",
"favorites": "Favorits",
@ -978,18 +1114,28 @@
"mention": "Menció",
"message": "Missatge",
"mute": "Silencia",
"mute_confirm": "Estàs segur que vols silenciar a {user}?",
"mute_confirm_accept_button": "Sí, silencia'l",
"mute_confirm_cancel_button": "No, no el silenciïs",
"mute_confirm_title": "Silencia usuari",
"mute_domain": "Bloqueja el domini",
"mute_progress": "Silenciant…",
"muted": "Silenciat",
"note": "Nota privada",
"per_day": "per dia",
"remote_follow": "Seguiment remot",
"remove_follower": "Esborra seguidor",
"replies": "Amb respostes",
"report": "Informa",
"show_repeats": "Mostra les repeticions",
"statuses": "Apunts",
"subscribe": "Subscriu-te",
"unblock": "Desbloqueja",
"unblock_progress": "Desbloquejant…",
"unfollow_confirm": "Estàs segur que vols deixar de seguir a {user}?",
"unfollow_confirm_accept_button": "Sí, deixa'l de seguir",
"unfollow_confirm_cancel_button": "No, no el deixis de seguir",
"unfollow_confirm_title": "Deixa de seguir l'usuari",
"unmute": "Deixa de silenciar",
"unmute_progress": "Deixant de silenciar…",
"unsubscribe": "Anul·la la subscripció"

View File

@ -258,7 +258,11 @@
"placeholder": "myusername",
"recovery_code": "Recovery code",
"register": "Register",
"username": "Username"
"username": "Username",
"logout_confirm_cancel_button": "Cancel",
"logout_confirm_accept_button": "Log out",
"logout_confirm": "Are you sure you want to log out?",
"logout_confirm_title": "Log out"
},
"media_modal": {
"counter": "{current} / {total}",
@ -266,6 +270,32 @@
"next": "Next",
"previous": "Previous"
},
"moderation": {
"moderation": "Moderation",
"reports": {
"no_reports": "No reports to show",
"add_note": "Add note",
"close": "Close",
"delete_note": "Delete",
"delete_note_accept": "Yes, delete it",
"delete_note_cancel": "No, keep it",
"delete_note_confirm": "Are you sure you want to delete this note?",
"delete_note_title": "Confirm deletion",
"no_content": "No description given",
"note_placeholder": "Leave a note...",
"notes": "{ count } note | { count } notes",
"reopen": "Reopen",
"report": "Report on",
"reports": "Reports",
"resolve": "Resolve",
"show_closed": "Show closed",
"statuses": "{ count } status | { count } statuses",
"tag_policy_notice": "Enable the TagPolicy MRF to set post restrictions",
"tags": "Set post restrictions"
},
"statuses": "Statuses",
"users": "Users"
},
"nav": {
"about": "About",
"administration": "Administration",
@ -282,6 +312,7 @@
"interactions": "Interactions",
"lists": "Lists",
"mentions": "Mentions",
"moderation": "Moderation",
"preferences": "Preferences",
"public_timeline_description": "Public posts from this instance",
"public_tl": "Public timeline",
@ -1101,6 +1132,7 @@
"followers": "Followers",
"following": "Following!",
"follows_you": "Follows you!",
"requested_by": "Has requested to follow you",
"hidden": "Hidden",
"hide_repeats": "Hide repeats",
"highlight": {

View File

@ -164,9 +164,78 @@
"load_older": "Cargar interacciones más antiguas",
"moves": "Usuario migrado"
},
"languages": {
"ar": "Árabe",
"az": "Azerbaiyán",
"bg": "Búlgaro",
"cs": "Checo",
"da": "Danés",
"de": "Alemán",
"el": "Griego",
"en": "Inglés",
"eo": "Esperanto",
"es": "Español",
"fa": "Persa",
"fi": "Finlandés",
"fr": "Francés",
"ga": "Irlandés",
"he": "Hebreo",
"hi": "Hindi",
"hu": "Húngaro",
"id": "Indonesio",
"it": "Italiano",
"ja": "Japonés",
"ko": "Coreano",
"lt": "Lituano",
"lv": "Letón",
"nl": "Alemán",
"pl": "Polaco",
"pt": "Portugués",
"ru": "Ruso",
"sk": "Eslovaco",
"sv": "Sueco",
"tr": "Turco",
"translated_from": {
"ar": "Traducido del @:languages.ar",
"az": "Traducido del @:languages.az",
"bg": "Traducido del @:languages.bg",
"cs": "Traducido del @:languages.cs",
"da": "Traducido del @:languages.da",
"de": "Traducido del @:languages.de",
"el": "Traducido del @:languages.el",
"en": "Traducido del @:languages.en",
"eo": "Traducido del @:languages.eo",
"es": "Traducido del @:languages.es",
"fa": "Traducido del @:languages.fa",
"fi": "Traducido del @:languages.fi",
"fr": "Traducido del @:languages.fr",
"ga": "Traducido del @:languages.ga",
"he": "Traducido del @:languages.he",
"hi": "Traducido del @:languages.hi",
"hu": "Traducido del @:languages.hu",
"id": "Traducido del @:languages.id",
"it": "Traducido del @:languages.it",
"ja": "Traducido del @:languages.ja",
"ko": "Traducido del @:languages.ko",
"lt": "Traducido del @:languages.it",
"lv": "Traducido del @:languages.lv",
"nl": "Traducido del @:languages.nl",
"pl": "Traducido del @:languages.pl",
"pt": "Traducido del @:languages.pt",
"ru": "Traducido del @:languages.ru",
"sk": "Traducido del @:languages.sk",
"sv": "Traducido del @:languages.sv",
"tr": "Traducido del @:languages.tr",
"uk": "Traducido del @:languages.uk",
"zh": "Traducido del @:languages.zh"
},
"uk": "Ucraniano",
"zh": "Chino"
},
"lists": {
"create": "Crear",
"delete": "Eliminar lista",
"following_only": "Limitar a seguidores",
"lists": "Listas",
"new": "Nueva Lista",
"save": "Guardar cambios",
@ -186,28 +255,59 @@
"login": "Identificarse",
"logout": "Cerrar sesión",
"password": "Contraseña",
"placeholder": "p.ej. lain",
"placeholder": "miusuario",
"recovery_code": "Código de recuperación",
"register": "Registrarse",
"username": "Usuario"
},
"media_modal": {
"counter": "{current} / {total}",
"hide": "Cerrar visor de medios",
"next": "Siguiente",
"previous": "Anterior"
},
"moderation": {
"moderation": "Moderación",
"reports": {
"add_note": "Añadir nota",
"close": "Cerrar",
"delete_note": "Eliminar",
"delete_note_accept": "Sí, eliminarlo",
"delete_note_cancel": "No, mantenerlo",
"delete_note_confirm": "¿Estás seguro que quieres eliminar esta nota?",
"delete_note_title": "Confirma la eliminación",
"no_content": "Sin descripción dada",
"note_placeholder": "Dejar una nota...",
"notes": "notas",
"reopen": "Reabrir",
"report": "Reportar",
"reports": "Reportes",
"resolve": "Resolver",
"show_closed": "Mostrar cerrados",
"statuses": "estados",
"tag_policy_notice": "Habilitar TagPolicy MRF para establecer restricciones de publicación",
"tags": "Establecer restricciones de publicación"
},
"statuses": "Estados",
"users": "Usuarios"
},
"nav": {
"about": "Acerca de",
"administration": "Administración",
"announcements": "Anuncios",
"back": "Volver",
"bookmarks": "Marcadores",
"bubble_timeline": "Linea temporal burbuja",
"bubble_timeline_description": "Publicaciones de instancias cercanas a la tuya, recomendadas por los/las administradores/as",
"chats": "Chats",
"dms": "Mensajes directos",
"friend_requests": "Solicitudes de seguimiento",
"home_timeline": "Línea temporal personal",
"home_timeline_description": "Publicaciones de personas que sigues",
"interactions": "Interacciones",
"lists": "Listas",
"mentions": "Menciones",
"moderation": "Moderación",
"preferences": "Preferencias",
"public_timeline_description": "Publicaciones públicas de esta instancia",
"public_tl": "Línea temporal pública",
@ -220,18 +320,19 @@
"who_to_follow": "A quién seguir"
},
"notifications": {
"broken_favorite": "Estado desconocido, buscándolo…",
"broken_favorite": "Publicación desconocida, buscándola…",
"error": "Error obteniendo notificaciones:{0}",
"favorited_you": "le gusta tu estado",
"favorited_you": "le gusta tu publicación",
"follow_request": "quiere seguirte",
"followed_you": "empezó a seguirte",
"load_older": "Cargar notificaciones antiguas",
"migrated_to": "migrado a",
"no_more_notifications": "No hay más notificaciones",
"notifications": "Notificaciones",
"poll_ended": "La encuesta ha terminado",
"reacted_with": "reaccionó con {0}",
"read": "¡Leído!",
"repeated_you": "repitió tu estado"
"repeated_you": "repitió tu publicación"
},
"password_reset": {
"check_email": "Revise su correo electrónico para obtener un enlace para restablecer su contraseña.",
@ -269,27 +370,34 @@
"text/bbcode": "BBCode",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/plain": "Texto Plano"
"text/plain": "Texto Plano",
"text/x.misskeymarkdown": "MFM"
},
"content_warning": "Tema (opcional)",
"content_warning": "Advertencia de contenido (opcional)",
"default": "Acabo de aterrizar en L.A.",
"direct_warning_to_all": "Esta publicación será visible para todos los usuarios mencionados.",
"direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.",
"empty_status_error": "No se puede publicar un estado vacío y sin archivos adjuntos",
"edit_remote_warning": "¡Los cambios realizados en la publicación pueden no ser visibles en algunas instancias!",
"edit_status": "Editar estado",
"edit_unsupported_warning": "Las encuestas y menciones no se modificarán al editar.",
"empty_status_error": "No se puede enviar una publicación sin contenido ni archivos adjuntos",
"media_description": "Descripción multimedia",
"media_description_error": "Error al actualizar el archivo, inténtalo de nuevo",
"new_status": "Publicar un nuevo estado",
"media_not_sensitive_warning": "¡Tiene una advertencia de contenido, pero los archivos adjuntos no están marcados como contenido sensible!",
"new_status": "Nueva publicación",
"post": "Publicar",
"posting": "Publicando",
"preview": "Vista previa",
"preview_empty": "Vacío",
"scope": {
"direct": "Directo - solo para los usuarios mencionados",
"local": "Local- no federar esta publicación",
"private": "Solo-seguidores - solo tus seguidores leerán la publicación",
"public": "Público - publicaciones visibles en las líneas temporales públicas",
"unlisted": "Sin listar -publicaciones no visibles en las líneas temporales públicas"
},
"scope_notice": {
"local": "Esta publicación no será visible en otras instancias",
"private": "Esta publicación solo será visible para tus seguidores",
"public": "Esta publicación será visible para todo el mundo",
"unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida"
@ -297,11 +405,12 @@
},
"registration": {
"bio": "Biografía",
"bio_placeholder": "e.g.\nHola, soy un ejemplo.\nAquí puedes poner algo representativo tuyo... o no.",
"bio_placeholder": "p. ej.\nHola, soy un ejemplo.\nAquí puedes poner algo representativo tuyo... o no.",
"captcha": "CAPTCHA",
"email": "Correo electrónico",
"email_language": "¿En qué idioma desea recibir correos electrónicos del servidor?",
"fullname": "Nombre a mostrar",
"fullname_placeholder": "p.ej. Lain Iwakura",
"fullname_placeholder": "p. ej. Atsuko Kagari",
"new_captcha": "Haz click en la imagen para obtener un nuevo captcha",
"password_confirm": "Confirmar contraseña",
"reason": "Razón para registrarse",
@ -309,7 +418,7 @@
"register": "Registrarse",
"registration": "Registro",
"token": "Token de invitación",
"username_placeholder": "p.ej. lain",
"username_placeholder": "p. ej. akko",
"validations": {
"email_required": "no puede estar vacío",
"fullname_required": "no puede estar vacío",
@ -336,6 +445,16 @@
},
"settings": {
"accent": "Acento",
"account_alias_table_head": "Alias",
"account_backup": "Copia de seguridad de la cuenta",
"account_backup_description": "Esto le permite descargar un archivo con la información de su cuenta y sus publicaciones, pero aún no se pueden importar a una cuenta de Pleroma.",
"account_backup_table_head": "Copia de seguridad",
"account_privacy": "Privacidad",
"add_alias_error": "Error añadiendo el alias: {error}",
"add_backup": "Crear una nueva copia de seguridad",
"add_backup_error": "Error al agregar una nueva copia de seguridad: {error}",
"added_alias": "El alias ha sido añadido.",
"added_backup": "La copia de seguridad ha sido agregada.",
"allow_following_move": "Permitir el seguimiento automático, cuando la cuenta que sigues se traslada a otra instancia",
"always_show_post_button": "Muestra siempre el botón flotante de Nueva Plubicación",
"app_name": "Nombre de la aplicación",
@ -347,6 +466,7 @@
"avatarRadius": "Avatares",
"avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.",
"background": "Fondo",
"backup_not_ready": "Esta copia de seguridad aún no está lista.",
"bio": "Biografía",
"block_export": "Exportar usuarios bloqueados",
"block_export_button": "Exporta la lista de tus usuarios bloqueados a un archivo csv",
@ -368,10 +488,18 @@
"changed_password": "¡Contraseña cambiada correctamente!",
"chatMessageRadius": "Mensaje de chat",
"checkboxRadius": "Casillas de verificación",
"collapse_subject": "Colapsar publicaciones con tema",
"collapse_subject": "Ocultar publicaciones con advertencias de contenido",
"columns": "Columnas",
"composing": "Redactando",
"confirm_dialogs": "Requiere confirmación de:",
"confirm_new_password": "Confirmar la nueva contraseña",
"conversation_display_linear": "Lineal",
"conversation_display_tree": "Ramificado",
"conversation_other_replies_button": "Mostrar el botón \"otras respuestas\"",
"conversation_other_replies_button_below": "Debajo de las publicaciones",
"conversation_other_replies_button_inside": "Dentro de las publicaciones",
"current_avatar": "Tu avatar actual",
"current_mascot": "Tu mascota actual",
"current_password": "Contraseña actual",
"data_import_export_tab": "Importar / Exportar datos",
"default_vis": "Alcance de visibilidad por defecto",
@ -379,11 +507,15 @@
"delete_account_description": "Eliminar para siempre los datos y desactivar la cuenta.",
"delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el/la administrador/a de tu instancia.",
"delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.",
"disable_sticky_headers": "No insertar encabezados de columna en la parte superior de la pantalla",
"discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios",
"domain_mutes": "Dominios",
"download_backup": "Descargar",
"email_language": "Idioma para recibir correos electrónicos del servidor",
"emoji_reactions_on_timeline": "Mostrar las reacciones de emoji en la línea de tiempo",
"enable_web_push_notifications": "Habilitar las notificiaciones en el navegador",
"enter_current_password_to_confirm": "Introduce la contraseña actual para confirmar tu identidad",
"expert_mode": "Mostrar avanzados",
"export_theme": "Exportar tema",
"file_export_import": {
"backup_restore": "Copia de seguridad de la configuración",
@ -398,7 +530,7 @@
"restore_settings": "Restaurar ajustes desde archivo"
},
"filtering": "Filtrado",
"filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea",
"filtering_explanation": "Todos las publicaciones que contengan estas palabras serán silenciadas. Una por línea",
"follow_export": "Exportar personas que tú sigues",
"follow_export_button": "Exporta tus seguidores a un fichero csv",
"follow_import": "Importar personas que tú sigues",
@ -411,18 +543,25 @@
"hide_all_muted_posts": "Ocultar las publicaciones silenciadas",
"hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones",
"hide_attachments_in_tl": "Ocultar adjuntos en la línea temporal",
"hide_favorites_description": "No mostrar la lista de mis favoritos (las personas aún serán notificadas)",
"hide_filtered_statuses": "Ocultar estados filtrados",
"hide_followers_count_description": "No mostrar el número de cuentas que me siguen",
"hide_followers_description": "No mostrar quién me sigue",
"hide_follows_count_description": "No mostrar el número de cuentas que sigo",
"hide_follows_description": "No mostrar a quién sigo",
"hide_isp": "Ocultar el panel específico de la instancia",
"hide_list_aliases_error_action": "Cerrar",
"hide_media_previews": "Ocultar la vista previa multimedia",
"hide_muted_posts": "Ocultar las publicaciones de los usuarios silenciados",
"hide_muted_threads": "Ocultar conversaciones silenciadas",
"hide_post_stats": "Ocultar las estadísticas de las publicaciones (p.ej. el número de favoritos)",
"hide_shoutbox": "Ocultar cuadro de diálogo de la instancia",
"hide_site_favicon": "Ocultar \"favicon\" de la instancia en el \"top panel\"",
"hide_site_name": "Ocultar el nombre de la instancia en el \"top panel\"",
"hide_threads_with_blocked_users": "Ocultar conversaciones que mencionen usuarios bloqueados",
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
"hide_wallpaper": "Ocultar el fondo de pantalla de la instancia",
"hide_wordfiltered_statuses": "Ocultar publicaciones filtradas por palabras",
"import_blocks_from_a_csv_file": "Importar lista de usuarios bloqueados dese un archivo csv",
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
"import_mutes_from_a_csv_file": "Importar silenciados desde un archivo csv",
@ -435,10 +574,21 @@
"invalid_theme_imported": "El archivo importado no es un tema válido de Pleroma. No se han realizado cambios.",
"limited_availability": "No disponible en tu navegador",
"links": "Enlaces",
"list_aliases_error": "Error al obtener alias: {error}",
"list_backups_error": "Error al obtener la lista de copias de seguridad: {error}",
"lock_account_description": "Restringir el acceso a tu cuenta solo a seguidores admitidos",
"loop_video": "Vídeos en bucle",
"loop_video_silent_only": "Bucle solo en vídeos sin sonido (p.ej. \"gifs\" de Mastodon)",
"mascot": "Mascota de Mastodon FE",
"max_depth_in_thread": "Número máximo de niveles a mostrar en la conversación por defecto",
"max_thumbnails": "Cantidad máxima de miniaturas por publicación",
"mention_link_bolden_you": "Resaltar la mención de ti cuando te mencionen",
"mention_link_display": "Mostrar enlaces de mención",
"mention_link_display_full": "siempre como nombre completo (p. ej. {'@'}foo{'@'}example.org)",
"mention_link_display_full_for_remote": "como nombre completo solo para usuarios remotos (p. ej. {'@'}foo{'@'}example.org)",
"mention_link_display_short": "siempre como nombres cortos (p. ej. {'@'}foo)",
"mention_link_show_avatar": "Mostrar el avatar del usuario detrás del enlace",
"mention_link_show_tooltip": "Mostrar el nombre completo de los usuarios remotos como información emergente",
"mfa": {
"authentication_methods": "Métodos de autentificación",
"confirm_and_enable": "Confirmar y habilitar OTP",
@ -462,6 +612,12 @@
},
"minimal_scopes_mode": "Minimizar las opciones de publicación",
"more_settings": "Más opciones",
"move_account": "Trasladar la cuenta",
"move_account_error": "Error trasladando la cuenta: {error}",
"move_account_notes": "Si desea trasladar esta cuenta a otro lugar, debe ir a su cuenta de destino y agregar un alias que apunte aquí.",
"move_account_target": "Cuenta de destino (p. ej. {example})",
"moved_account": "La cuenta ha sido trasladada.",
"mute_bot_posts": "Silenciar publicaciones de \"bots\"",
"mute_export": "Exportar silenciados",
"mute_export_button": "Exportar los silenciados a un archivo csv",
"mute_import": "Importar silenciados",
@ -471,6 +627,7 @@
"mutes_tab": "Silenciados",
"name": "Nombre",
"name_bio": "Nombre y biografía",
"new_alias_target": "Añadir un nuevo alias (p. ej. {example})",
"new_email": "Nuevo correo electrónico",
"new_password": "Nueva contraseña",
"no_blocks": "No hay usuarios bloqueados",

View File

@ -273,23 +273,23 @@
"back": "Retour",
"bookmarks": "Marques-Pages",
"bubble_timeline": "Flux de cette bulle",
"bubble_timeline_description": "Les status des instances proches de celle-ci, choisies par l'administration",
"bubble_timeline_description": "Les statuts des instances proches de celle-ci, choisies par l'administration",
"chats": "Chats",
"dms": "Messages directs",
"friend_requests": "Demandes de suivi",
"home_timeline": "Flux personnel",
"home_timeline_description": "Les status de vos abonnements",
"home_timeline_description": "Les statuts de vos abonnements",
"interactions": "Interactions",
"lists": "Listes",
"mentions": "Mentions",
"preferences": "Préférences",
"public_timeline_description": "Tous les status publics de cette instance",
"public_timeline_description": "Tous les statuts publics de cette instance",
"public_tl": "Flux publique",
"search": "Recherche",
"timeline": "Flux personnel",
"timelines": "Flux",
"twkn": "Réseau connu",
"twkn_timeline_description": "Les status du réseau entier",
"twkn_timeline_description": "Les statuts du réseau entier",
"user_search": "Recherche de comptes",
"who_to_follow": "Suggestion de suivi"
},
@ -337,7 +337,7 @@
"votes_count": "{count} vote | {count} votes"
},
"post_status": {
"account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.",
"account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre et voir vos statuts réservés aux abonné·es.",
"account_not_locked_warning_link": "verrouillé",
"attachments_sensitive": "Marquer les pièce-jointes comme sensible",
"content_type": {
@ -693,6 +693,19 @@
"setting_changed": "Préférence modifiée",
"setting_server_side": "Modifier cette préférence répercute sur tous vos clients",
"settings": "Paramètres",
"settings_profile": "Profils de paramètres",
"settings_profile_creation": "Créer un profil",
"settings_profile_creation_new_name_label": "Nom",
"settings_profile_creation_submit": "Créer",
"settings_profile_currently": "Profil actuel : {name} (version {version})",
"settings_profile_delete": "Supprimer",
"settings_profile_delete_confirm": "Voulez-vous vraiment supprimer ce profil ?",
"settings_profile_force_sync": "Synchroniser",
"settings_profile_in_use": "Actuel",
"settings_profile_use": "Utiliser",
"settings_profiles_refresh": "Recharger",
"settings_profiles_show": "Afficher touts les profils",
"settings_profiles_unshow": "Cacher les profils",
"show_admin_badge": "Afficher le badge d'Admin sur mon profil",
"show_moderator_badge": "Afficher le badge de Modo' sur mon profil",
"show_nav_shortcuts": "Afficher plus de raccourcis de navigations dans le panneau supérieur",
@ -892,6 +905,12 @@
"word_filter": "Filtrage par mots",
"wordfilter": "Filtrage par mot-clé"
},
"settings_profile": {
"creating": "Création du profil « {profil} » …",
"synchronization_error": "Impossible de synchroniser les paramètres : {err}",
"synchronized": "Paramètres synchronisés !",
"synchronizing": "Synchronisation du profil « {profile} » …"
},
"status": {
"ancestor_follow": "Voir {numReplies} autre réponse en dessous de ce status | Voir {numReplies} autres réponses en dessous de ce status",
"ancestor_follow_with_icon": "{icon} {text}",
@ -925,6 +944,11 @@
"pin": "Agrafer sur le profil",
"pinned": "Agraffé",
"plus_more": "+{number} autre | +{number} autres",
"redraft": "Supprimer et réécrire",
"redraft_confirm": "Supprimer et réécrire ce status ? Les interactions avec l'original seront perdues.",
"redraft_confirm_accept_button": "Oui : supprimer et réécrire",
"redraft_confirm_cancel_button": "Non : garder l'original",
"redraft_confirm_title": "Confirmer la suppression et réécriture",
"remove_attachment": "Supprimer la pièce jointe",
"repeat_confirm": "Partager ce statut ?",
"repeat_confirm_accept_button": "Partager",
@ -1047,12 +1071,24 @@
"strip_media": "Supprimer les medias des statuts"
},
"approve": "Accepter",
"approve_confirm": "Voulez-vous vraiment approuver cet abonnement ?",
"approve_confirm_accept_button": "Oui : accepter",
"approve_confirm_cancel_button": "Non : rejeter",
"approve_confirm_title": "Approuver une demande d'abonnement",
"block": "Bloquer",
"block_confirm": "Êtes-vous sûr de vouloir bloquer {user} ?",
"block_confirm_accept_button": "Oui",
"block_confirm_cancel_button": "Non",
"block_confirm_title": "Bloquer l'utilisateur",
"block_progress": "Blocage…",
"blocked": "Bloqué !",
"bot": "Robot",
"deactivated": "Désactivé",
"deny": "Rejeter",
"deny_confirm": "Êtes-vous sûr de vouloir rejeter cette demande d'abonnement ?",
"deny_confirm_accept_button": "Oui : rejeter",
"deny_confirm_cancel_button": "Non : annuler",
"deny_confirm_title": "Rejeter une demande d'abonnement",
"domain_muted": "Débloquer la domaine",
"edit_profile": "Éditer le profil",
"favorites": "Favoris",
@ -1061,8 +1097,8 @@
"follow_progress": "Demande en cours…",
"follow_sent": "Demande envoyée !",
"follow_unfollow": "Désabonner",
"followees": "Suivis",
"followers": "Vous suivent",
"followees": "Abonnements",
"followers": "Abonné·es",
"following": "Suivi !",
"follows_you": "Vous suit !",
"hidden": "Caché",
@ -1078,18 +1114,28 @@
"mention": "Mention",
"message": "Message",
"mute": "Masquer",
"mute_confirm": "Êtes-vous sûr de vouloir masquer {user} ?",
"mute_confirm_accept_button": "Oui",
"mute_confirm_cancel_button": "Non",
"mute_confirm_title": "Masquer",
"mute_domain": "Bloquer la domaine",
"mute_progress": "Masquage…",
"muted": "Masqué",
"note": "Note privée",
"per_day": "par jour",
"remote_follow": "Suivre d'une autre instance",
"remove_follower": "Désabonner",
"replies": "Statuts et réponses",
"report": "Signalement",
"show_repeats": "Montrer les partages",
"statuses": "Statuts",
"subscribe": "Abonner",
"unblock": "Débloquer",
"unblock_progress": "Déblocage…",
"unfollow_confirm": "Êtes-vous sûr de vouloir vous désabonner de {user} ?",
"unfollow_confirm_accept_button": "Oui : me désabonner",
"unfollow_confirm_cancel_button": "Non : garder l'abonnement",
"unfollow_confirm_title": "Désabonner",
"unmute": "Démasquer",
"unmute_progress": "Démasquage…",
"unsubscribe": "Désabonner"

View File

@ -254,6 +254,10 @@
"hint": "会話に加わるには、ログインしてください",
"login": "ログイン",
"logout": "ログアウト",
"logout_confirm": "ログアウトしますか?",
"logout_confirm_accept_button": "ログアウト",
"logout_confirm_cancel_button": "キャンセル",
"logout_confirm_title": "ログアウト",
"password": "パスワード",
"placeholder": "例: user",
"recovery_code": "リカバリーコード",
@ -266,6 +270,27 @@
"next": "次",
"previous": "前"
},
"moderation": {
"moderation": "管理",
"reports": {
"add_note": "OK",
"close": "閉じる",
"delete_note": "削除",
"delete_note_accept": "削除",
"delete_note_cancel": "キャンセル",
"delete_note_confirm": "削除しますか?",
"delete_note_title": "確認してください",
"no_content": "説明なし",
"no_reports": "通報なし",
"note_placeholder": "メモ",
"notes": "メモ",
"reopen": "再開",
"report": "通報:",
"reports": "通報",
"resolve": "完了",
"show_closed": "完了した通報を表示"
}
},
"nav": {
"about": "このインスタンスについて",
"administration": "管理",
@ -282,6 +307,7 @@
"interactions": "インタラクション",
"lists": "リスト",
"mentions": "通知",
"moderation": "管理",
"preferences": "設定",
"public_timeline_description": "このインスタンスからの公開投稿",
"public_tl": "公開タイムライン",
@ -1124,8 +1150,10 @@
"note": "私的なメモ",
"per_day": "/日",
"remote_follow": "リモートフォロー",
"remove_follower": "フォロワーやめさせる",
"replies": "投稿と返信",
"report": "通報",
"requested_by": "あなたにフォローリクエストを送りました",
"show_repeats": "リピートを見る",
"statuses": "投稿",
"subscribe": "購読",

View File

@ -62,7 +62,7 @@ export default function createPersistedState ({
}
loaded = true
} catch (e) {
console.log("Couldn't load state")
console.error("Couldn't load state")
console.error(e)
loaded = true
}
@ -83,8 +83,8 @@ export default function createPersistedState ({
})
}
} catch (e) {
console.log("Couldn't persist state:")
console.log(e)
console.error("Couldn't persist state:")
console.error(e)
}
})
}

View File

@ -163,6 +163,7 @@ const api = {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('startFetchingAnnouncements')
dispatch('startFetchingReports')
dispatch('pushGlobalNotice', {
level: 'error',
messageKey: 'timeline.socket_broke',
@ -280,6 +281,19 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'announcements', fetcher })
},
// Reports
startFetchingReports (store) {
if (store.state.fetchers['reports']) return
const fetcher = store.state.backendInteractor.startFetchingReports({ store })
store.commit('addFetcher', { fetcherName: 'reports', fetcher })
},
stopFetchingReports (store) {
const fetcher = store.state.fetchers.reports
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'reports', fetcher })
},
getSupportedTranslationlanguages (store) {
store.state.backendInteractor.getSupportedTranslationlanguages({ store })
.then((data) => {

View File

@ -59,7 +59,7 @@ export const defaultState = {
alwaysShowNewPostButton: false,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: true,
stopGifs: undefined,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
notificationVisibility: {
@ -80,11 +80,9 @@ export const defaultState = {
hideScopeNotice: false,
useStreamingApi: false,
sidebarRight: undefined, // instance default
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
postContentType: undefined, // instance default
minimalScopesMode: undefined, // instance default
// This hides statuses filtered via a word filter
hideFilteredStatuses: undefined, // instance default
modalOnRepeat: undefined, // instance default

View File

@ -53,12 +53,10 @@ const defaultState = {
logoMargin: '.2em',
logoMask: true,
logoLeft: false,
minimalScopesMode: false,
nsfwCensorImage: undefined,
postContentType: 'text/plain',
redirectRootLogin: '/main/friends',
redirectRootNoLogin: '/main/all',
scopeCopy: true,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
showNavShortcuts: true,

View File

@ -2,6 +2,9 @@ const defaultState = {
settingsModalState: 'hidden',
settingsModalLoaded: false,
settingsModalTargetTab: null,
modModalState: 'hidden',
modModalLoaded: false,
modModalTargetTab: null,
settings: {
currentSaveStateNotice: null,
noticeClearTimeout: null,
@ -63,6 +66,30 @@ const interfaceMod = {
setSettingsModalTargetTab (state, value) {
state.settingsModalTargetTab = value
},
closeModModal (state) {
state.modModalState = 'hidden'
},
togglePeekModModal (state) {
switch (state.modModalState) {
case 'minimized':
state.modModalState = 'visible'
return
case 'visible':
state.modModalState = 'minimized'
return
default:
throw new Error('Illegal minimization state of mod modal')
}
},
openModModal (state) {
state.modModalState = 'visible'
if (!state.modModalLoaded) {
state.modModalLoaded = true
}
},
setModModalTargetTab (state, value) {
state.modModalTargetTab = value
},
pushGlobalNotice (state, notice) {
state.globalNotices.push(notice)
},
@ -105,6 +132,18 @@ const interfaceMod = {
commit('setSettingsModalTargetTab', value)
commit('openSettingsModal')
},
closeModModal ({ commit }) {
commit('closeModModal')
},
openModModal ({ commit }) {
commit('openModModal')
},
togglePeekModModal ({ commit }) {
commit('togglePeekModModal')
},
clearModModalTargetTab ({ commit }) {
commit('setModModalTargetTab', null)
},
pushGlobalNotice (
{ commit, dispatch, state },
{

View File

@ -1,11 +1,17 @@
import filter from 'lodash/filter'
import { filter, find, forEach, remove } from 'lodash'
const getReport = (state, id) => find(state.reports, { id })
const updateReport = (state, { report, param, value }) => {
getReport(state, report.id)[param] = value
}
const reports = {
state: {
userId: null,
statuses: [],
preTickedIds: [],
modalActivated: false
modalActivated: false,
reports: []
},
mutations: {
openUserReportingModal (state, { userId, statuses, preTickedIds }) {
@ -16,6 +22,38 @@ const reports = {
},
closeUserReportingModal (state) {
state.modalActivated = false
},
setReport (state, { report }) {
let existing = getReport(state, report.id)
if (existing) {
existing = report
} else {
state.reports.push(report)
}
},
updateReportStates (state, { reports }) {
forEach(reports, (report) => {
updateReport(state, { report, param: 'state', value: report.state })
})
},
addNoteToReport (state, { id, note, user }) {
// akkoma doesn't return the note from this API endpoint, and there's no
// good way to get it. the note data is spoofed in the frontend until
// reload.
// definitely worth adding this to the backend at some point
const report = getReport(state, id)
const date = new Date()
report.notes.push({
content: note,
user,
created_at: date.toISOString(),
id: date.getTime()
})
},
deleteNoteFromReport (state, { id, note }) {
const report = getReport(state, id)
remove(report.notes, { id: note })
}
},
actions: {
@ -31,6 +69,22 @@ const reports = {
},
closeUserReportingModal ({ commit }) {
commit('closeUserReportingModal')
},
updateReportStates ({ rootState, commit }, { reports }) {
commit('updateReportStates', { reports })
return rootState.api.backendInteractor.updateReportStates({ reports })
},
getReport ({ rootState, commit }, { id }) {
return rootState.api.backendInteractor.getReport({ id })
.then(report => commit('setReport', { report }))
},
addNoteToReport ({ rootState, commit }, { id, note }) {
commit('addNoteToReport', { id, note, user: rootState.users.currentUser })
return rootState.api.backendInteractor.addNoteToReport({ id, note })
},
deleteNoteFromReport ({ rootState, commit }, { id, note }) {
commit('deleteNoteFromReport', { id, note })
return rootState.api.backendInteractor.deleteNoteFromReport({ id, note })
}
}
}

View File

@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
@ -19,6 +19,9 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
const ADMIN_REPORTS_URL = '/api/v1/pleroma/admin/reports'
const ADMIN_REPORT_NOTES_URL = id => `/api/v1/pleroma/admin/reports/${id}/notes`
const ADMIN_REPORT_NOTE_URL = (report, note) => `/api/v1/pleroma/admin/reports/${report}/notes/${note}`
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
@ -342,7 +345,7 @@ const fetchUserRelationship = ({ id, credentials }) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url }, response))
return reject(new StatusCodeError(400, json, { url }, response))
}
return resolve(json)
}))
@ -635,6 +638,57 @@ const deleteUser = ({ credentials, user }) => {
})
}
const getReports = ({ state, limit, page, pageSize, credentials }) => {
let url = ADMIN_REPORTS_URL
const args = [
state && `state=${state}`,
limit && `limit=${limit}`,
page && `page=${page}`,
pageSize && `page_size=${pageSize}`
].filter(_ => _).join('&')
url = url + (args ? '?' + args : '')
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.reports.map(parseReport))
}
const updateReportStates = ({ credentials, reports }) => {
// reports syntax: [{ id: int, state: string }...]
const updates = {
reports: reports.map(report => {
return {
id: report.id.toString(),
state: report.state
}
})
}
return promisedRequest({
url: ADMIN_REPORTS_URL,
method: 'PATCH',
payload: updates,
credentials
})
}
const addNoteToReport = ({ id, note, credentials }) => {
return promisedRequest({
url: ADMIN_REPORT_NOTES_URL(id),
method: 'POST',
payload: { content: note },
credentials
})
}
const deleteNoteFromReport = ({ report, note, credentials }) => {
return promisedRequest({
url: ADMIN_REPORT_NOTE_URL(report, note),
method: 'DELETE',
credentials
})
}
const fetchTimeline = ({
timeline,
credentials,
@ -1726,7 +1780,11 @@ const apiService = {
getSettingsProfile,
saveSettingsProfile,
listSettingsProfiles,
deleteSettingsProfile
deleteSettingsProfile,
getReports,
updateReportStates,
addNoteToReport,
deleteNoteFromReport
}
export default apiService

View File

@ -5,6 +5,7 @@ import followRequestFetcher from '../../services/follow_request_fetcher/follow_r
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
import configFetcher from '../config_fetcher/config_fetcher.service.js'
import reportsFetcher from '../reports_fetcher/reports_fetcher.service.js'
const backendInteractorService = credentials => ({
startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) {
@ -39,6 +40,10 @@ const backendInteractorService = credentials => ({
return announcementsFetcher.startFetching({ store, credentials })
},
startFetchingReports ({ store, state, limit, page, pageSize }) {
return reportsFetcher.startFetching({ store, credentials, state, limit, page, pageSize })
},
startUserSocket ({ store }) {
const serv = store.rootState.instance.server.replace('http', 'ws')
const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })

View File

@ -1,7 +1,6 @@
import { promiseInterval } from '../promise_interval/promise_interval.js'
const startFetching = ({ credentials, store }) => {
console.log('startFetching: Config')
const boundFetchAndUpdate = () => store.dispatch('getSettingsProfile')
boundFetchAndUpdate()
return promiseInterval(boundFetchAndUpdate, 10 * 60000)

View File

@ -88,6 +88,9 @@ export const parseUser = (data) => {
output.friends_count = data.following_count
output.bot = data.bot
if (data.akkoma) {
output.instance = data.akkoma.instance
}
if (data.pleroma) {
const relationship = data.pleroma.relationship
@ -429,6 +432,24 @@ export const parseNotification = (data) => {
return output
}
export const parseReport = (data) => {
const report = {}
report.account = parseUser(data.account)
report.actor = parseUser(data.actor)
report.statuses = data.statuses.map(parseStatus)
report.notes = data.notes.map(note => {
note.user = parseUser(note.user)
return note
})
report.state = data.state
report.content = data.content
report.created_at = data.created_at
report.id = data.id
return report
}
const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)

View File

@ -1,5 +1,3 @@
import runtime from 'serviceworker-webpack-plugin/lib/runtime'
function urlBase64ToUint8Array (base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding)
@ -15,7 +13,7 @@ function isPushSupported () {
}
function getOrCreateServiceWorker () {
return runtime.register()
return navigator.serviceWorker.register('/sw-pleroma.js')
.catch((err) => console.error('Unable to get or create a service worker.', err))
}

View File

@ -0,0 +1,20 @@
import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.js'
import { forEach } from 'lodash'
const fetchAndUpdate = ({ store, credentials, state, limit, page, pageSize }) => {
return apiService.getReports({ credentials, state, limit, page, pageSize })
.then(reports => forEach(reports, report => store.commit('setReport', { report })))
}
const startFetching = ({ store, credentials, state, limit, page, pageSize }) => {
const boundFetchAndUpdate = () => fetchAndUpdate({ store, credentials, state, limit, page, pageSize })
boundFetchAndUpdate()
return promiseInterval(boundFetchAndUpdate, 60000)
}
const reportsFetcher = {
startFetching
}
export default reportsFetcher

View File

@ -5,7 +5,9 @@ import { parseNotification } from './services/entity_normalizer/entity_normalize
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
import { createI18n } from 'vue-i18n'
import messages from './i18n/service_worker_messages.js'
import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute';
precacheAndRoute(self.__WB_MANIFEST);
const i18n = createI18n({
// By default, use the browser locale, we will update it if neccessary
locale: 'en',

View File

@ -13,12 +13,10 @@
"logoMargin": ".1em",
"logoMask": true,
"logoLeft": false,
"minimalScopesMode": false,
"nsfwCensorImage": "",
"postContentType": "text/plain",
"redirectRootLogin": "/main/friends",
"redirectRootNoLogin": "/main/all",
"scopeCopy": true,
"showFeaturesPanel": true,
"showInstanceSpecificPanel": false,
"sidebarRight": false,

View File

@ -4,7 +4,7 @@
// https://github.com/webpack/karma-webpack
// var path = require('path')
var merge = require('webpack-merge')
var { merge } = require('webpack-merge')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var baseConfig = require('../../build/webpack.base.conf')
var utils = require('../../build/utils')
@ -16,7 +16,7 @@ var webpackConfig = merge(baseConfig, {
module: {
rules: utils.styleLoaders()
},
devtool: '#inline-source-map',
devtool: 'inline-source-map',
// vue: {
// loaders: {
// js: 'isparta'

View File

@ -194,8 +194,9 @@ describe('API Entities normalizer', () => {
expect(parsedPost).to.have.property('type', 'status')
expect(parsedRepeat).to.have.property('type', 'retweet')
console.log(parsedRepeat)
expect(parsedRepeat).to.have.property('retweeted_status')
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
expect(parsedRepeat).to.have.nested.property('retweeted_status.id', 'deadbeef')
})
it('sets nsfw for statuses with the #nsfw tag', () => {
@ -229,7 +230,7 @@ describe('API Entities normalizer', () => {
expect(parsedPost).to.have.property('type', 'status')
expect(parsedRepeat).to.have.property('type', 'retweet')
expect(parsedRepeat).to.have.property('retweeted_status')
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
expect(parsedRepeat).to.have.nested.property('retweeted_status.id', 'deadbeef')
})
})
})
@ -284,9 +285,9 @@ describe('API Entities normalizer', () => {
})
expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('seen', false)
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
expect(parseNotification(notif)).to.have.nested.property('status.id', '444')
expect(parseNotification(notif)).to.have.nested.property('action.id', '444')
expect(parseNotification(notif)).to.have.nested.property('from_profile.id', 'spurdo')
})
it('correctly normalizes favorite notifications', () => {
@ -303,9 +304,9 @@ describe('API Entities normalizer', () => {
expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('type', 'like')
expect(parseNotification(notif)).to.have.property('seen', true)
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
expect(parseNotification(notif)).to.have.nested.property('status.id', '4412')
expect(parseNotification(notif)).to.have.nested.property('action.id', '444')
expect(parseNotification(notif)).to.have.nested.property('from_profile.id', 'spurdo')
})
})

5129
yarn.lock

File diff suppressed because it is too large Load Diff