develop update #3

Merged
desea merged 39 commits from AkkomaGang/akkoma-fe:develop into develop 2022-11-12 22:02:17 +00:00
53 changed files with 1595 additions and 94 deletions

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

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

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

@ -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 || {},
@ -418,7 +418,7 @@ const PostStatusForm = {
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
if (this.newStatus.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
this.newStatus.nsfw = true
}
this.$emit('resize', { delayed: true })
@ -498,7 +498,7 @@ const PostStatusForm = {
})
},
onSubjectInput (e) {
if (this.newStatus.sensitiveIfSubject) {
if (this.$store.getters.mergedConfig.sensitiveIfSubject) {
this.newStatus.nsfw = true
}
},

View file

@ -130,11 +130,11 @@ export default {
codeblocks.forEach((pre) => {
content = content.replace(pre,
pre.replaceAll('<br/>', '\n')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot', '"')
.replaceAll('&#39;', "'")
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot', '"')
.replaceAll('&#39;', "'")
)
})
}

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

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

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

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

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

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

@ -55,7 +55,7 @@ export const defaultState = {
alwaysShowNewPostButton: false,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: true,
stopGifs: undefined,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
notificationVisibility: {

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

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

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

@ -1092,12 +1092,12 @@
"@fortawesome/fontawesome-common-types@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893"
integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w==
"@fortawesome/fontawesome-svg-core@1.3.0":
version "1.3.0"
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz#343fac91fa87daa630d26420bfedfba560f85885"
integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.3.0"
@ -1141,9 +1141,9 @@
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@intlify/bundle-utils@next":
version "3.1.2"
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.1.2.tgz"
integrity sha512-amgSo0NN5OKWYdcgFmfJqo2tcUcZ6C66Bxm5ALQnB0m3MUQtS9aJzKoIo+EU9XQiOVmlBFxRtNoZm+psHa5FNA==
version "3.2.1"
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.2.1.tgz"
integrity sha512-rf4cLBOnbqmpXVcCdcYHilZpMt1m82syh3WLBJlZvGxN2KkH9HeHVH4+bnibF/SDXCHNh6lM6wTpS/qw+PkcMg==
dependencies:
"@intlify/message-compiler" next
"@intlify/shared" next
@ -1168,7 +1168,7 @@
dependencies:
"@intlify/shared" "9.2.2"
"@intlify/message-compiler@9.2.2":
"@intlify/message-compiler@9.2.2", "@intlify/message-compiler@next":
version "9.2.2"
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz"
integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==
@ -1176,24 +1176,11 @@
"@intlify/shared" "9.2.2"
source-map "0.6.1"
"@intlify/message-compiler@next":
version "9.3.0-beta.3"
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.3.0-beta.3.tgz"
integrity sha512-j8OwToBQgs01RBMX4GCDNQfcnmw3AiDG3moKIONTrfXcf+1yt/rWznLTYH/DXbKcFMAFijFpCzMYjUmH1jVFYA==
dependencies:
"@intlify/shared" "9.3.0-beta.3"
source-map "0.6.1"
"@intlify/shared@9.2.2", "@intlify/shared@next":
version "9.2.2"
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz"
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
"@intlify/shared@9.3.0-beta.3":
version "9.3.0-beta.3"
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.3.0-beta.3.tgz"
integrity sha512-Z/0TU4GhFKRxKh+0RbwJExik9zz57gXYgxSYaPn7YQdkQ/pabSioCY/SXnYxQHL6HzULF5tmqarFm6glbGqKhw==
"@intlify/vue-devtools@9.2.2":
version "9.2.2"
resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz"
@ -8095,9 +8082,9 @@ mute-stream@0.0.7:
integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==
nan@^2.12.1:
version "2.16.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^3.3.4:
version "3.3.4"