Merge remote-tracking branch 'upstream/develop' into neckbeard

new-bloop
Your New SJW Waifu 4 years ago
commit 916cefc15e

@ -29,11 +29,11 @@
"portal-vue": "^2.1.4", "portal-vue": "^2.1.4",
"sanitize-html": "^1.13.0", "sanitize-html": "^1.13.0",
"v-click-outside": "^2.1.1", "v-click-outside": "^2.1.1",
"vue": "^2.5.13", "vue": "^2.6.11",
"vue-chat-scroll": "^1.2.1", "vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4", "vue-template-compiler": "^2.6.11",
"vuelidate": "^0.7.4", "vuelidate": "^0.7.4",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"whatwg-fetch": "^2.0.3" "whatwg-fetch": "^2.0.3"

@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ChatPanel from './components/chat_panel/chat_panel.vue' import ChatPanel from './components/chat_panel/chat_panel.vue'
import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue' import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue' import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
@ -30,6 +31,7 @@ export default {
SideDrawer, SideDrawer,
MobilePostStatusButton, MobilePostStatusButton,
MobileNav, MobileNav,
SettingsModal,
UserReportingModal, UserReportingModal,
Banner, Banner,
PostStatusModal PostStatusModal
@ -119,6 +121,9 @@ export default {
onSearchBarToggled (hidden) { onSearchBarToggled (hidden) {
this.searchBarHidden = hidden this.searchBarHidden = hidden
}, },
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
},
updateMobileState () { updateMobileState () {
const mobileLayout = windowWidth() <= 800 const mobileLayout = windowWidth() <= 800
const changed = mobileLayout !== this.isMobileLayout const changed = mobileLayout !== this.isMobileLayout

@ -576,7 +576,7 @@ main-router {
min-height: 0; min-height: 0;
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
margin-left: .25em; margin-left: .5em;
min-width: 1px; min-width: 1px;
align-self: stretch; align-self: stretch;
} }
@ -870,51 +870,6 @@ nav {
} }
} }
.setting-item {
border-bottom: 2px solid var(--fg, $fallback--fg);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div {
margin-bottom: .5em;
&:last-child {
margin-bottom: 0;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable i {
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
.btn {
min-height: 28px;
min-width: 10em;
padding: 0 2em;
}
.number-input {
max-width: 6em;
}
}
.select-multiple { .select-multiple {
display: flex; display: flex;
.option-list { .option-list {

@ -46,15 +46,16 @@
@toggled="onSearchBarToggled" @toggled="onSearchBarToggled"
@click.stop.native @click.stop.native
/> />
<router-link <a
href="#"
class="mobile-hidden" class="mobile-hidden"
:to="{ name: 'settings'}" @click.stop="openSettingsModal"
> >
<i <i
class="button-icon icon-cog nav-icon" class="button-icon icon-cog nav-icon"
:title="$t('nav.preferences')" :title="$t('nav.preferences')"
/> />
</router-link> </a>
<a <a
v-if="currentUser && currentUser.role === 'admin'" v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma" href="/pleroma/admin/#/login-pleroma"
@ -126,6 +127,7 @@
<MobilePostStatusButton /> <MobilePostStatusButton />
<UserReportingModal /> <UserReportingModal />
<PostStatusModal /> <PostStatusModal />
<SettingsModal />
<portal-target name="modal" /> <portal-target name="modal" />
</div> </div>
</template> </template>

@ -7,10 +7,8 @@ import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue' import DMs from 'components/dm_timeline/dm_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.vue' import UserProfile from 'components/user_profile/user_profile.vue'
import Search from 'components/search/search.vue' import Search from 'components/search/search.vue'
import Settings from 'components/settings/settings.vue'
import Registration from 'components/registration/registration.vue' import Registration from 'components/registration/registration.vue'
import PasswordReset from 'components/password_reset/password_reset.vue' import PasswordReset from 'components/password_reset/password_reset.vue'
import UserSettings from 'components/user_settings/user_settings.vue'
import FollowRequests from 'components/follow_requests/follow_requests.vue' import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
import Notifications from 'components/notifications/notifications.vue' import Notifications from 'components/notifications/notifications.vue'
@ -56,12 +54,10 @@ export default (store) => {
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile }, { name: 'external-user-profile', path: '/users/:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
{ name: 'login', path: '/login', component: AuthForm }, { name: 'login', path: '/login', component: AuthForm },
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) }, { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },

@ -0,0 +1,41 @@
<template>
<div class="async-component-error">
<div>
<h4>
{{ $t('general.generic_error') }}
</h4>
<p>
{{ $t('general.error_retry') }}
</p>
<button
class="btn"
@click="retry"
>
{{ $t('general.retry') }}
</button>
</div>
</div>
</template>
<script>
export default {
methods: {
retry () {
this.$emit('resetAsyncComponent')
}
}
}
</script>
<style lang="scss">
.async-component-error {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
.btn {
margin: .5em;
padding: .5em 2em;
}
}
</style>

@ -1,8 +1,9 @@
<template> <template>
<div <div
v-show="isOpen" v-show="isOpen"
v-body-scroll-lock="isOpen" v-body-scroll-lock="isOpen && !noBackground"
class="modal-view" class="modal-view"
:class="classes"
@click.self="$emit('backdropClicked')" @click.self="$emit('backdropClicked')"
> >
<slot /> <slot />
@ -15,6 +16,18 @@ export default {
isOpen: { isOpen: {
type: Boolean, type: Boolean,
default: true default: true
},
noBackground: {
type: Boolean,
default: false
}
},
computed: {
classes () {
return {
'modal-background': !this.noBackground,
'open': this.isOpen
}
} }
} }
} }
@ -32,12 +45,22 @@ export default {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overflow: auto; overflow: auto;
pointer-events: none;
animation-duration: 0.2s; animation-duration: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein; animation-name: modal-background-fadein;
body:not(.scroll-locked) & {
opacity: 0; opacity: 0;
> * {
pointer-events: initial;
}
&.modal-background {
pointer-events: initial;
background-color: rgba(0, 0, 0, 0.5);
}
&.open {
opacity: 1;
} }
} }

@ -0,0 +1,29 @@
<template>
<div class="panel-loading">
<span class="loading-text">
<i class="icon-spin4 animate-spin" />
{{ $t('general.loading') }}
</span>
</div>
</template>
<style lang="scss">
@import 'src/_variables.scss';
.panel-loading {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
font-size: 2em;
color: $fallback--text;
color: var(--text, $fallback--text);
.loading-text i {
font-size: 3em;
line-height: 0;
vertical-align: middle;
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style>

@ -13,9 +13,6 @@ const PostStatusModal = {
} }
}, },
computed: { computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
modalActivated () { modalActivated () {
return this.$store.state.postStatus.modalActivated return this.$store.state.postStatus.modalActivated
}, },

@ -1,6 +1,5 @@
<template> <template>
<Modal <Modal
v-if="isLoggedIn && !resettingForm"
:is-open="modalActivated" :is-open="modalActivated"
class="post-form-modal-view" class="post-form-modal-view"
@backdropClicked="closeModal" @backdropClicked="closeModal"

@ -1,132 +0,0 @@
/* eslint-env browser */
import { filter, trim } from 'lodash'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import { extractCommit } from '../../services/version/version.service'
import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
import Checkbox from '../checkbox/checkbox.vue'
const pleromaFeCommitUrl = 'https://git.pleroma.social/sjw/pleroma-fe/commit/'
const pleromaBeCommitUrl = 'https://git.pleroma.social/sjw/pleroma/commit/'
const multiChoiceProperties = [
'postContentType',
'subjectLineBehavior'
]
const settings = {
data () {
const instance = this.$store.state.instance
return {
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
backendVersion: instance.backendVersion,
frontendVersion: instance.frontendVersion,
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
}
},
components: {
TabSwitcher,
StyleSwitcher,
InterfaceLanguageSwitcher,
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
},
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
postFormats () {
return this.$store.state.instance.postFormats || []
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
frontendVersionLink () {
return pleromaFeCommitUrl + this.frontendVersion
},
backendVersionLink () {
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
},
// Getting localized values for instance-default properties
...instanceDefaultProperties
.filter(key => multiChoiceProperties.includes(key))
.map(key => [
key + 'DefaultValue',
function () {
return this.$store.getters.instanceDefaultConfig[key]
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
...instanceDefaultProperties
.filter(key => !multiChoiceProperties.includes(key))
.map(key => [
key + 'LocalizedValue',
function () {
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Generating computed values for vuex properties
...Object.keys(configDefaultState)
.map(key => [key, {
get () { return this.$store.getters.mergedConfig[key] },
set (value) {
this.$store.dispatch('setOption', { name: key, value })
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values or perform actions first)
muteWordsString: {
get () {
return this.muteWordsStringLocal
},
set (value) {
this.muteWordsStringLocal = value
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
},
useStreamingApi: {
get () { return this.$store.getters.mergedConfig.useStreamingApi },
set (value) {
const promise = value
? this.$store.dispatch('enableMastoSockets')
: this.$store.dispatch('disableMastoSockets')
promise.then(() => {
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
this.$store.dispatch('disableMastoSockets')
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
})
}
}
},
// Updating nested properties
watch: {
notificationVisibility: {
handler (value) {
this.$store.dispatch('setOption', {
name: 'notificationVisibility',
value: this.$store.getters.mergedConfig.notificationVisibility
})
},
deep: true
}
}
}
export default settings

@ -1,424 +0,0 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('settings.settings') }}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div
v-if="currentSaveStateNotice.error"
class="alert error"
@click.prevent
>
{{ $t('settings.saving_err') }}
</div>
<div
v-if="!currentSaveStateNotice.error"
class="alert transparent"
@click.prevent
>
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
</div>
<div class="panel-body">
<keep-alive>
<tab-switcher>
<div :label="$t('settings.general')">
<div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list">
<li>
<interface-language-switcher />
</li>
<li v-if="instanceSpecificPanelPresent">
<Checkbox v-model="hideISP">
{{ $t('settings.hide_isp') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('nav.timeline') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="hideMutedPosts">
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="streaming">
{{ $t('settings.streaming') }}
</Checkbox>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li>
<Checkbox
v-model="pauseOnUnfocused"
:disabled="!streaming"
>
{{ $t('settings.pause_on_unfocused') }}
</Checkbox>
</li>
</ul>
</li>
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
<br>
<small>
{{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
<li>
<Checkbox v-model="autoLoad">
{{ $t('settings.autoload') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="hoverPreview">
{{ $t('settings.reply_link_preview') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="emojiReactionsOnTimeline">
{{ $t('settings.emoji_reactions_on_timeline') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.composing') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="scopeCopy">
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="alwaysShowSubjectInput">
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
</Checkbox>
</li>
<li>
<div>
{{ $t('settings.subject_line_behavior') }}
<label
for="subjectLineBehavior"
class="select"
>
<select
id="subjectLineBehavior"
v-model="subjectLineBehavior"
>
<option value="email">
{{ $t('settings.subject_line_email') }}
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
</option>
<option value="masto">
{{ $t('settings.subject_line_mastodon') }}
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
</option>
<option value="noop">
{{ $t('settings.subject_line_noop') }}
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
<i class="icon-down-open" />
</label>
</div>
</li>
<li v-if="postFormats.length > 0">
<div>
{{ $t('settings.post_status_content_type') }}
<label
for="postContentType"
class="select"
>
<select
id="postContentType"
v-model="postContentType"
>
<option
v-for="postFormat in postFormats"
:key="postFormat"
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
<i class="icon-down-open" />
</label>
</div>
</li>
<li>
<Checkbox v-model="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="autohideFloatingPostButton">
{{ $t('settings.autohide_floating_post_button') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="padEmoji">
{{ $t('settings.pad_emoji') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.attachments') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="hideAttachments">
{{ $t('settings.hide_attachments_in_tl') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="hideAttachmentsInConv">
{{ $t('settings.hide_attachments_in_convo') }}
</Checkbox>
</li>
<li>
<label for="maxThumbnails">
{{ $t('settings.max_thumbnails') }}
</label>
<input
id="maxThumbnails"
v-model.number="maxThumbnails"
class="number-input"
type="number"
min="0"
step="1"
>
</li>
<li>
<Checkbox v-model="hideNsfw">
{{ $t('settings.nsfw_clickthrough') }}
</Checkbox>
</li>
<ul class="setting-list suboptions">
<li>
<Checkbox
v-model="preloadImage"
:disabled="!hideNsfw"
>
{{ $t('settings.preload_images') }}
</Checkbox>
</li>
<li>
<Checkbox
v-model="useOneClickNsfw"
:disabled="!hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</Checkbox>
</li>
</ul>
<li>
<Checkbox v-model="stopGifs">
{{ $t('settings.stop_gifs') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="loopVideo">
{{ $t('settings.loop_video') }}
</Checkbox>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li>
<Checkbox
v-model="loopVideoSilentOnly"
:disabled="!loopVideo || !loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</Checkbox>
<div
v-if="!loopSilentAvailable"
class="unavailable"
>
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
</div>
</li>
</ul>
</li>
<li>
<Checkbox v-model="playVideosInModal">
{{ $t('settings.play_videos_in_modal') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="useContainFit">
{{ $t('settings.use_contain_fit') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.notifications') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="webPushNotifications">
{{ $t('settings.enable_web_push_notifications') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.fun') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="greentext">
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
</Checkbox>
</li>
</ul>
</div>
</div>
<div :label="$t('settings.theme')">
<div class="setting-item">
<style-switcher />
</div>
</div>
<div :label="$t('settings.filtering')">
<div class="setting-item">
<div class="select-multiple">
<span class="label">{{ $t('settings.notification_visibility') }}</span>
<ul class="option-list">
<li>
<Checkbox v-model="notificationVisibility.likes">
{{ $t('settings.notification_visibility_likes') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.repeats">
{{ $t('settings.notification_visibility_repeats') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.follows">
{{ $t('settings.notification_visibility_follows') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_mentions') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_emoji_reactions') }}
</Checkbox>
</li>
</ul>
</div>
<div>
{{ $t('settings.replies_in_timeline') }}
<label
for="replyVisibility"
class="select"
>
<select
id="replyVisibility"
v-model="replyVisibility"
>
<option
value="all"
selected
>{{ $t('settings.reply_visibility_all') }}</option>
<option value="following">{{ $t('settings.reply_visibility_following') }}</option>
<option value="self">{{ $t('settings.reply_visibility_self') }}</option>
</select>
<i class="icon-down-open" />
</label>
</div>
<div>
<Checkbox v-model="hidePostStats">
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
</Checkbox>
</div>
<div>
<Checkbox v-model="hideUserStats">
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
</Checkbox>
</div>
</div>
<div class="setting-item">
<div>
<p>{{ $t('settings.filtering_explanation') }}</p>
<textarea
id="muteWords"
v-model="muteWordsString"
/>
</div>
<div>
<Checkbox v-model="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
</Checkbox>
</div>
</div>
</div>
<div :label="$t('settings.version.title')">
<div class="setting-item">
<ul class="setting-list">
<li>
<p>{{ $t('settings.version.backend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="backendVersionLink"
target="_blank"
>{{ backendVersion }}</a>
</li>
</ul>
</li>
<li>
<p>{{ $t('settings.version.frontend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="frontendVersionLink"
target="_blank"
>{{ frontendVersion }}</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</tab-switcher>
</keep-alive>
</div>
</div>
</template>
<script src="./settings.js">
</script>

@ -0,0 +1,58 @@
import {
instanceDefaultProperties,
multiChoiceProperties,
defaultState as configDefaultState
} from 'src/modules/config.js'
const SharedComputedObject = () => ({
user () {
return this.$store.state.users.currentUser
},
// Getting localized values for instance-default properties
...instanceDefaultProperties
.filter(key => multiChoiceProperties.includes(key))
.map(key => [
key + 'DefaultValue',
function () {
return this.$store.getters.instanceDefaultConfig[key]
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
...instanceDefaultProperties
.filter(key => !multiChoiceProperties.includes(key))
.map(key => [
key + 'LocalizedValue',
function () {
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Generating computed values for vuex properties
...Object.keys(configDefaultState)
.map(key => [key, {
get () { return this.$store.getters.mergedConfig[key] },
set (value) {
this.$store.dispatch('setOption', { name: key, value })
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values or perform actions first)
useStreamingApi: {
get () { return this.$store.getters.mergedConfig.useStreamingApi },
set (value) {
const promise = value
? this.$store.dispatch('enableMastoSockets')
: this.$store.dispatch('disableMastoSockets')
promise.then(() => {
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
this.$store.dispatch('disableMastoSockets')
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
})
}
}
})
export default SharedComputedObject

@ -0,0 +1,42 @@
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'
const SettingsModal = {
components: {
Modal,
SettingsModalContent: getResettableAsyncComponent(
() => import('./settings_modal_content.vue'),
{
loading: PanelLoading,
error: AsyncComponentError,
delay: 0
}
)
},
methods: {
closeModal () {
this.$store.dispatch('closeSettingsModal')
},
peekModal () {
this.$store.dispatch('togglePeekSettingsModal')
}
},
computed: {
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
modalOpenedOnce () {
return this.$store.state.interface.settingsModalLoaded
},
modalPeeked () {
return this.$store.state.interface.settingsModalState === 'minimized'
}
}
}
export default SettingsModal

@ -0,0 +1,44 @@
@import 'src/_variables.scss';
.settings-modal {
overflow: hidden;
&.peek {
.settings-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));
}
}
.settings-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: 100vh;
}
.panel-body {
height: 100%;
overflow-y: hidden;
.btn {
min-height: 28px;
min-width: 10em;
padding: 0 2em;
}
}
}
}

@ -0,0 +1,54 @@
<template>
<Modal
:is-open="modalActivated"
class="settings-modal"
:class="{ peek: modalPeeked }"
:no-background="modalPeeked"
>
<div class="settings-modal-panel panel">
<div class="panel-heading">
<span class="title">
{{ $t('settings.settings') }}
</span>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div
v-if="currentSaveStateNotice.error"
class="alert error"
@click.prevent
>
{{ $t('settings.saving_err') }}
</div>
<div
v-if="!currentSaveStateNotice.error"
class="alert transparent"
@click.prevent
>
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
<button
class="btn"
@click="peekModal"
>
{{ $t('general.peek') }}
</button>
<button
class="btn"
@click="closeModal"
>
{{ $t('general.close') }}
</button>
</div>
<div class="panel-body">
<SettingsModalContent v-if="modalOpenedOnce" />
</div>
</div>
</Modal>
</template>
<script src="./settings_modal.js"></script>
<style src="./settings_modal.scss" lang="scss"></style>

@ -0,0 +1,34 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
import DataImportExportTab from './tabs/data_import_export_tab.vue'
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
import NotificationsTab from './tabs/notifications_tab.vue'
import FilteringTab from './tabs/filtering_tab.vue'
import SecurityTab from './tabs/security_tab/security_tab.vue'
import ProfileTab from './tabs/profile_tab.vue'
import GeneralTab from './tabs/general_tab.vue'
import VersionTab from './tabs/version_tab.vue'
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
const SettingsModalContent = {
components: {
TabSwitcher,
DataImportExportTab,
MutesAndBlocksTab,
NotificationsTab,
FilteringTab,
SecurityTab,
ProfileTab,
GeneralTab,
VersionTab,
ThemeTab
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
}
}
}
export default SettingsModalContent

@ -0,0 +1,43 @@
@import 'src/_variables.scss';
.settings_tab-switcher {
height: 100%;
.setting-item {
border-bottom: 2px solid var(--fg, $fallback--fg);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div {
margin-bottom: .5em;
&:last-child {
margin-bottom: 0;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable i {
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
.number-input {
max-width: 6em;
}
}
}

@ -0,0 +1,73 @@
<template>
<tab-switcher
ref="tabSwitcher"
class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
>
<div
:label="$t('settings.general')"
icon="wrench"
>
<GeneralTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.profile_tab')"
icon="user"
>
<ProfileTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.security_tab')"
icon="lock"
>
<SecurityTab />
</div>
<div
:label="$t('settings.filtering')"
icon="filter"
>
<FilteringTab />
</div>
<div
:label="$t('settings.theme')"
icon="brush"
>
<ThemeTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.notifications')"
icon="bell-ringing-o"
>
<NotificationsTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.data_import_export_tab')"
icon="download"
>
<DataImportExportTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.mutes_and_blocks')"
:fullHeight="true"
icon="eye-off"
>
<MutesAndBlocksTab />
</div>
<div
:label="$t('settings.version.title')"
icon="info-circled"
>
<VersionTab />
</div>
</tab-switcher>
</template>
<script src="./settings_modal_content.js"></script>
<style src="./settings_modal_content.scss" lang="scss"></style>

@ -0,0 +1,65 @@
import Importer from 'src/components/importer/importer.vue'
import Exporter from 'src/components/exporter/exporter.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
const DataImportExportTab = {
data () {
return {
activeTab: 'profile',
newDomainToMute: ''
}
},
created () {
this.$store.dispatch('fetchTokens')
},
components: {
Importer,
Exporter,
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
}
},
methods: {
getFollowsContent () {
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
.then(this.generateExportableUsersContent)
},
getBlocksContent () {
return this.$store.state.api.backendInteractor.fetchBlocks()
.then(this.generateExportableUsersContent)
},
importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
generateExportableUsersContent (users) {
// Get addresses
return users.map((user) => {
// check is it's a local user
if (user && user.is_local) {
// append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname
}
return user.screen_name
}).join('\n')
}
}
}
export default DataImportExportTab

@ -0,0 +1,43 @@
<template>
<div
:label="$t('settings.data_import_export_tab')"
>
<div class="setting-item">
<h2>{{ $t('settings.follow_import') }}</h2>
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
<Importer
:submit-handler="importFollows"
:success-message="$t('settings.follows_imported')"
:error-message="$t('settings.follow_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.follow_export') }}</h2>
<Exporter
:get-content="getFollowsContent"
filename="friends.csv"
:export-button-label="$t('settings.follow_export_button')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.block_import') }}</h2>
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
<Importer
:submit-handler="importBlocks"
:success-message="$t('settings.blocks_imported')"
:error-message="$t('settings.block_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.block_export') }}</h2>
<Exporter
:get-content="getBlocksContent"
filename="blocks.csv"
:export-button-label="$t('settings.block_export_button')"
/>
</div>
</div>
</template>
<script src="./data_import_export_tab.js"></script>
<!-- <style lang="scss" src="./profile.scss"></style> -->

@ -0,0 +1,44 @@
import { filter, trim } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const FilteringTab = {
data () {
return {
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
}
},
components: {
Checkbox
},
computed: {
...SharedComputedObject(),
muteWordsString: {
get () {
return this.muteWordsStringLocal
},
set (value) {
this.muteWordsStringLocal = value
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
}
},
// Updating nested properties
watch: {
notificationVisibility: {
handler (value) {
this.$store.dispatch('setOption', {
name: 'notificationVisibility',
value: this.$store.getters.mergedConfig.notificationVisibility
})
},
deep: true
}
}
}
export default FilteringTab

@ -0,0 +1,86 @@
<template>
<div :label="$t('settings.filtering')">
<div class="setting-item">
<div class="select-multiple">
<span class="label">{{ $t('settings.notification_visibility') }}</span>
<ul class="option-list">
<li>
<Checkbox v-model="notificationVisibility.likes">
{{ $t('settings.notification_visibility_likes') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.repeats">
{{ $t('settings.notification_visibility_repeats') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.follows">
{{ $t('settings.notification_visibility_follows') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_mentions') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_emoji_reactions') }}
</Checkbox>
</li>
</ul>
</div>
<div>
{{ $t('settings.replies_in_timeline') }}
<label
for="replyVisibility"
class="select"
>
<select
id="replyVisibility"
v-model="replyVisibility"
>
<option
value="all"
selected
>{{ $t('settings.reply_visibility_all') }}</option>
<option value="following">{{ $t('settings.reply_visibility_following') }}</option>
<option value="self">{{ $t('settings.reply_visibility_self') }}</option>
</select>
<i class="icon-down-open" />
</label>
</div>
<div>
<Checkbox v-model="hidePostStats">
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
</Checkbox>
</div>
<div>
<Checkbox v-model="hideUserStats">
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
</Checkbox>
</div>
</div>
<div class="setting-item">
<div>
<p>{{ $t('settings.filtering_explanation') }}</p>
<textarea
id="muteWords"
v-model="muteWordsString"
/>
</div>
<div>
<Checkbox v-model="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
</Checkbox>
</div>
</div>
</div>
</template>
<script src="./filtering_tab.js"></script>

@ -0,0 +1,31 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const GeneralTab = {
data () {
return {
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
}
},
components: {
Checkbox,
InterfaceLanguageSwitcher
},
computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject()
}
}
export default GeneralTab

@ -0,0 +1,272 @@
<template>
<div :label="$t('settings.general')">
<div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list">
<li>
<interface-language-switcher />
</li>
<li v-if="instanceSpecificPanelPresent">
<Checkbox v-model="hideISP">
{{ $t('settings.hide_isp') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('nav.timeline') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="hideMutedPosts">
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="streaming">
{{ $t('settings.streaming') }}
</Checkbox>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li>
<Checkbox
v-model="pauseOnUnfocused"
:disabled="!streaming"
>
{{ $t('settings.pause_on_unfocused') }}
</Checkbox>
</li>
</ul>
</li>
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
<br>
<small>
{{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
<li>
<Checkbox v-model="autoLoad">
{{ $t('settings.autoload') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="hoverPreview">
{{ $t('settings.reply_link_preview') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="emojiReactionsOnTimeline">
{{ $t('settings.emoji_reactions_on_timeline') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.composing') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="scopeCopy">
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="alwaysShowSubjectInput">
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
</Checkbox>
</li>
<li>
<div>
{{ $t('settings.subject_line_behavior') }}
<label
for="subjectLineBehavior"
class="select"
>
<select
id="subjectLineBehavior"
v-model="subjectLineBehavior"
>
<option value="email">
{{ $t('settings.subject_line_email') }}
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
</option>
<option value="masto">
{{ $t('settings.subject_line_mastodon') }}
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
</option>
<option value="noop">
{{ $t('settings.subject_line_noop') }}
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
<i class="icon-down-open" />
</label>
</div>
</li>
<li v-if="postFormats.length > 0">
<div>
{{ $t('settings.post_status_content_type') }}
<label
for="postContentType"
class="select"
>
<select
id="postContentType"
v-model="postContentType"
>
<option
v-for="postFormat in postFormats"
:key="postFormat"
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
<i class="icon-down-open" />
</label>
</div>
</li>
<li>
<Checkbox v-model="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
</Checkbox>
</li>
<li>
<Checkbox v-model="autohideFloatingPostButton">
{{ $t('settings.autohide_floating_post_button') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="padEmoji">
{{ $t('settings.pad_emoji') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.attachments') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="hideAttachments">
{{ $t('settings.hide_attachments_in_tl') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="hideAttachmentsInConv">
{{ $t('settings.hide_attachments_in_convo') }}
</Checkbox>
</li>
<li>
<label for="maxThumbnails">
{{ $t('settings.max_thumbnails') }}
</label>
<input
id="maxThumbnails"
v-model.number="maxThumbnails"
class="number-input"
type="number"
min="0"
step="1"
>
</li>
<li>
<Checkbox v-model="hideNsfw">
{{ $t('settings.nsfw_clickthrough') }}
</Checkbox>
</li>
<ul class="setting-list suboptions">
<li>
<Checkbox
v-model="preloadImage"
:disabled="!hideNsfw"
>
{{ $t('settings.preload_images') }}
</Checkbox>
</li>
<li>
<Checkbox
v-model="useOneClickNsfw"
:disabled="!hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</Checkbox>
</li>
</ul>
<li>
<Checkbox v-model="stopGifs">
{{ $t('settings.stop_gifs') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="loopVideo">
{{ $t('settings.loop_video') }}
</Checkbox>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li>
<Checkbox
v-model="loopVideoSilentOnly"
:disabled="!loopVideo || !loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</Checkbox>
<div
v-if="!loopSilentAvailable"
class="unavailable"
>
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
</div>
</li>
</ul>
</li>
<li>
<Checkbox v-model="playVideosInModal">
{{ $t('settings.play_videos_in_modal') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="useContainFit">
{{ $t('settings.use_contain_fit') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.notifications') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="webPushNotifications">
{{ $t('settings.enable_web_push_notifications') }}
</Checkbox>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.fun') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="greentext">
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
</Checkbox>
</li>
</ul>
</div>
</div>
</template>
<script src="./general_tab.js"></script>

@ -0,0 +1,124 @@
import get from 'lodash/get'
import map from 'lodash/map'
import reject from 'lodash/reject'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
import BlockCard from 'src/components/block_card/block_card.vue'
import MuteCard from 'src/components/mute_card/mute_card.vue'
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
import Checkbox from 'src/components/checkbox/checkbox.vue'
const BlockList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'items'
})(SelectableList)
const MuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'items'
})(SelectableList)
const DomainMuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
childPropName: 'items'
})(SelectableList)
const MutesAndBlocks = {
data () {
return {
activeTab: 'profile',
newDomainToMute: ''
}
},
created () {
this.$store.dispatch('fetchTokens')
},
components: {
TabSwitcher,
BlockList,
MuteList,
DomainMuteList,
BlockCard,
MuteCard,
DomainMuteCard,
ProgressButton,
Autosuggest,
Checkbox
},
methods: {
importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
generateExportableUsersContent (users) {
// Get addresses
return users.map((user) => {
// check is it's a local user
if (user && user.is_local) {
// append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname
}
return user.screen_name
}).join('\n')
},
activateTab (tabName) {
this.activeTab = tabName
},
filterUnblockedUsers (userIds) {
return reject(userIds, (userId) => {
const relationship = this.$store.getters.relationship(this.userId)
return relationship.blocking || userId === this.$store.state.users.currentUser.id
})
},
filterUnMutedUsers (userIds) {
return reject(userIds, (userId) => {
const relationship = this.$store.getters.relationship(this.userId)
return relationship.muting || userId === this.$store.state.users.currentUser.id
})
},
queryUserIds (query) {
return this.$store.dispatch('searchUsers', { query })
.then((users) => map(users, 'id'))
},
blockUsers (ids) {
return this.$store.dispatch('blockUsers', ids)
},
unblockUsers (ids) {
return this.$store.dispatch('unblockUsers', ids)
},
muteUsers (ids) {
return this.$store.dispatch('muteUsers', ids)
},
unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
unmuteDomains (domains) {
return this.$store.dispatch('unmuteDomains', domains)
},
muteDomain () {
return this.$store.dispatch('muteDomain', this.newDomainToMute)
.then(() => { this.newDomainToMute = '' })
}
}
}
export default MutesAndBlocks

@ -0,0 +1,29 @@
.mutes-and-blocks-tab {
height: 100%;
.usersearch-wrapper {
padding: 1em;
}
.bulk-actions {
text-align: right;
padding: 0 1em;
min-height: 28px;
}
.bulk-action-button {
width: 10em
}
.domain-mute-form {
padding: 1em;
display: flex;
flex-direction: column
}
.domain-mute-button {
align-self: flex-end;
margin-top: 1em;
width: 10em
}
}

@ -0,0 +1,176 @@
<template>
<tab-switcher
:scrollable-tabs="true"
class="mutes-and-blocks-tab"
>
<div :label="$t('settings.blocks_tab')">
<div class="usersearch-wrapper">
<Autosuggest
:filter="filterUnblockedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_block')"
>
<BlockCard
slot-scope="row"
:user-id="row.item"
/>
</Autosuggest>
</div>
<BlockList
:refresh="true"
:get-key="i => i"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default bulk-action-button"
:click="() => blockUsers(selected)"
>
{{ $t('user_card.block') }}
<template slot="progress">
{{ $t('user_card.block_progress') }}
</template>
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unblockUsers(selected)"
>
{{ $t('user_card.unblock') }}
<template slot="progress">
{{ $t('user_card.unblock_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<BlockCard :user-id="item" />
</template>
<template slot="empty">
{{ $t('settings.no_blocks') }}
</template>
</BlockList>
</div>
<div :label="$t('settings.mutes_tab')">
<tab-switcher>
<div label="Users">
<div class="usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')"
>
<MuteCard
slot-scope="row"
:user-id="row.item"
/>
</Autosuggest>
</div>
<MuteList
:refresh="true"
:get-key="i => i"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => muteUsers(selected)"
>
{{ $t('user_card.mute') }}
<template slot="progress">
{{ $t('user_card.mute_progress') }}
</template>
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteUsers(selected)"
>
{{ $t('user_card.unmute') }}
<template slot="progress">
{{ $t('user_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<MuteCard :user-id="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</MuteList>
</div>
<div :label="$t('settings.domain_mutes')">
<div class="domain-mute-form">
<input
v-model="newDomainToMute"
:placeholder="$t('settings.type_domains_to_mute')"
type="text"
@keyup.enter="muteDomain"
>
<ProgressButton
class="btn btn-default domain-mute-button"
:click="muteDomain"
>
{{ $t('domain_mute_card.mute') }}
<template slot="progress">
{{ $t('domain_mute_card.mute_progress') }}
</template>
</ProgressButton>
</div>
<DomainMuteList
:refresh="true"
:get-key="i => i"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteDomains(selected)"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
{{ $t('domain_mute_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<DomainMuteCard :domain="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</DomainMuteList>
</div>
</tab-switcher>
</div>
</tab-switcher>
</template>
<script src="./mutes_and_blocks_tab.js"></script>
<style lang="scss" src="./mutes_and_blocks_tab.scss"></style>

@ -0,0 +1,27 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
const NotificationsTab = {
data () {
return {
activeTab: 'profile',
notificationSettings: this.$store.state.users.currentUser.notification_settings,
newDomainToMute: ''
}
},
components: {
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
}
},
methods: {
updateNotificationSettings () {
this.$store.state.api.backendInteractor
.updateNotificationSettings({ settings: this.notificationSettings })
}
}
}
export default NotificationsTab

@ -0,0 +1,54 @@
<template>
<div :label="$t('settings.notifications')">
<div class="setting-item">
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<div class="select-multiple">
<span class="label">{{ $t('settings.notification_setting') }}</span>
<ul class="option-list">
<li>
<Checkbox v-model="notificationSettings.follows">
{{ $t('settings.notification_setting_follows') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationSettings.followers">
{{ $t('settings.notification_setting_followers') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationSettings.non_follows">
{{ $t('settings.notification_setting_non_follows') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationSettings.non_followers">
{{ $t('settings.notification_setting_non_followers') }}
</Checkbox>
</li>
</ul>
</div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
<p>
<Checkbox v-model="notificationSettings.privacy_option">
{{ $t('settings.notification_setting_privacy_option') }}
</Checkbox>
</p>
</div>
<div class="setting-item">
<p>{{ $t('settings.notification_mutes') }}</p>
<p>{{ $t('settings.notification_blocks') }}</p>
<button
class="btn btn-default"
@click="updateNotificationSettings"
>
{{ $t('general.submit') }}
</button>
</div>
</div>
</template>
<script src="./notifications_tab.js"></script>
<!-- <style lang="scss" src="./profile.scss"></style> -->

@ -0,0 +1,179 @@
import unescape from 'lodash/unescape'
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
import suggestor from 'src/components/emoji_input/suggestor.js'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
const ProfileTab = {
data () {
return {
newName: this.$store.state.users.currentUser.name,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
newDefaultScope: this.$store.state.users.currentUser.default_scope,
hideFollows: this.$store.state.users.currentUser.hide_follows,
hideFollowers: this.$store.state.users.currentUser.hide_followers,
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
discoverable: this.$store.state.users.currentUser.discoverable,
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
banner: null,
bannerPreview: null,
background: null,
backgroundPreview: null,
bannerUploadError: null,
backgroundUploadError: null
}
},
components: {
ScopeSelector,
ImageCropper,
EmojiInput,
Autosuggest,
ProgressButton,
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
},
emojiUserSuggestor () {
return suggestor({
emoji: [
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
],
users: this.$store.state.users.users,
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
})
},
emojiSuggestor () {
return suggestor({ emoji: [
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
] })
}
},
methods: {
updateProfile () {
this.$store.state.api.backendInteractor
.updateProfile({
params: {
note: this.newBio,
locked: this.newLocked,
// Backend notation.
/* eslint-disable camelcase */
display_name: this.newName,
default_scope: this.newDefaultScope,
no_rich_text: this.newNoRichText,
hide_follows: this.hideFollows,
hide_followers: this.hideFollowers,
discoverable: this.discoverable,
allow_following_move: this.allowFollowingMove,
hide_follows_count: this.hideFollowsCount,
hide_followers_count: this.hideFollowersCount,
show_role: this.showRole
/* eslint-enable camelcase */
} }).then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
})
},
changeVis (visibility) {
this.newDefaultScope = visibility
},
uploadFile (slot, e) {
const file = e.target.files[0]
if (!file) { return }
if (file.size > this.$store.state.instance[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
this[slot + 'UploadError'] = [
this.$t('upload.error.base'),
this.$t(
'upload.error.file_too_big',
{
filesize: filesize.num,
filesizeunit: filesize.unit,
allowedsize: allowedsize.num,
allowedsizeunit: allowedsize.unit
}
)
].join(' ')
return
}
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({ target }) => {
const img = target.result
this[slot + 'Preview'] = img
this[slot] = file
}
reader.readAsDataURL(file)
},
submitAvatar (cropper, file) {
const that = this
return new Promise((resolve, reject) => {
function updateAvatar (avatar) {
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
.then((user) => {
that.$store.commit('addNewUsers', [user])
that.$store.commit('setCurrentUser', user)
resolve()
})
.catch((err) => {
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
})
}
if (cropper) {
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
} else {
updateAvatar(file)
}
})
},
submitBanner () {
if (!this.bannerPreview) { return }
this.bannerUploading = true
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
.then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
this.bannerPreview = null
})
.catch((err) => {
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
})
.then(() => { this.bannerUploading = false })
},
submitBg () {
if (!this.backgroundPreview) { return }
let background = this.background
this.backgroundUploading = true
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
if (!data.error) {
this.$store.commit('addNewUsers', [data])
this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null
} else {
this.backgroundUploadError = this.$t('upload.error.base') + data.error
}
this.backgroundUploading = false
})
}
}
}
export default ProfileTab

@ -0,0 +1,82 @@
@import '../../../_variables.scss';
.profile-tab {
.bio {
margin: 0;
}
.visibility-tray {
padding-top: 5px;
}
input[type=file] {
padding: 5px;
height: auto;
}
.banner {
max-width: 100%;
}
.uploading {
font-size: 1.5em;
margin: 0.25em;
}
.name-changer {
width: 100%;
}
.bg {
max-width: 100%;
}
.current-avatar {
display: block;
width: 150px;
height: 150px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
&-usersearch-wrapper {
padding: 1em;
}
&-bulk-actions {
text-align: right;
padding: 0 1em;
min-height: 28px;
button {
width: 10em;
}
}
&-domain-mute-form {
padding: 1em;
display: flex;
flex-direction: column;
button {
align-self: flex-end;
margin-top: 1em;
width: 10em;
}
}
.setting-subitem {
margin-left: 1.75em;
}
}

@ -0,0 +1,213 @@
<template>
<div class="profile-tab">
<div class="setting-item">
<h2>{{ $t('settings.name_bio') }}</h2>
<p>{{ $t('settings.name') }}</p>
<EmojiInput
v-model="newName"
enable-emoji-picker
:suggest="emojiSuggestor"
>
<input
id="username"
v-model="newName"
classname="name-changer"
>
</EmojiInput>
<p>{{ $t('settings.bio') }}</p>
<EmojiInput
v-model="newBio"
enable-emoji-picker
:suggest="emojiUserSuggestor"
>
<textarea
v-model="newBio"
classname="bio"
/>
</EmojiInput>
<p>
<Checkbox v-model="newLocked">
{{ $t('settings.lock_account_description') }}
</Checkbox>
</p>
<div>
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
<div
id="default-vis"
class="visibility-tray"
>
<scope-selector
:show-all="true"
:user-default="newDefaultScope"
:initial-scope="newDefaultScope"
:on-scope-change="changeVis"
/>
</div>
</div>
<p>
<Checkbox v-model="newNoRichText">
{{ $t('settings.no_rich_text_description') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="hideFollows">
{{ $t('settings.hide_follows_description') }}
</Checkbox>
</p>
<p class="setting-subitem">
<Checkbox
v-model="hideFollowsCount"
:disabled="!hideFollows"
>
{{ $t('settings.hide_follows_count_description') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="hideFollowers">
{{ $t('settings.hide_followers_description') }}
</Checkbox>
</p>
<p class="setting-subitem">
<Checkbox
v-model="hideFollowersCount"
:disabled="!hideFollowers"
>
{{ $t('settings.hide_followers_count_description') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="allowFollowingMove">
{{ $t('settings.allow_following_move') }}
</Checkbox>
</p>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
<template v-if="role === 'admin'">
{{ $t('settings.show_admin_badge') }}
</template>
<template v-if="role === 'moderator'">
{{ $t('settings.show_moderator_badge') }}
</template>
</Checkbox>
</p>
<p>
<Checkbox v-model="discoverable">
{{ $t('settings.discoverable') }}
</Checkbox>
</p>
<button
:disabled="newName && newName.length === 0"
class="btn btn-default"
@click="updateProfile"
>
{{ $t('general.submit') }}
</button>
</div>
<div class="setting-item">
<h2>{{ $t('settings.avatar') }}</h2>
<p class="visibility-notice">
{{ $t('settings.avatar_size_instruction') }}
</p>
<p>{{ $t('settings.current_avatar') }}</p>
<img
:src="user.profile_image_url_original"
class="current-avatar"
>
<p>{{ $t('settings.set_new_avatar') }}</p>
<button
v-show="pickAvatarBtnVisible"
id="pick-avatar"
class="btn"
type="button"
>
{{ $t('settings.upload_a_photo') }}
</button>
<image-cropper
trigger="#pick-avatar"
:submit-handler="submitAvatar"
@open="pickAvatarBtnVisible=false"
@close="pickAvatarBtnVisible=true"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.profile_banner') }}</h2>
<p>{{ $t('settings.current_profile_banner') }}</p>
<img
:src="user.cover_photo"
class="banner"
>
<p>{{ $t('settings.set_new_profile_banner') }}</p>
<img
v-if="bannerPreview"
class="banner"
:src="bannerPreview"
>
<div>
<input
type="file"
@change="uploadFile('banner', $event)"
>
</div>
<i
v-if="bannerUploading"
class=" icon-spin4 animate-spin uploading"
/>
<button
v-else-if="bannerPreview"
class="btn btn-default"
@click="submitBanner"
>
{{ $t('general.submit') }}
</button>
<div
v-if="bannerUploadError"
class="alert error"
>
Error: {{ bannerUploadError }}
<i
class="button-icon icon-cancel"
@click="clearUploadError('banner')"
/>
</div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.profile_background') }}</h2>
<p>{{ $t('settings.set_new_profile_background') }}</p>
<img
v-if="backgroundPreview"
class="bg"
:src="backgroundPreview"
>
<div>
<input
type="file"
@change="uploadFile('background', $event)"
>
</div>
<i
v-if="backgroundUploading"
class=" icon-spin4 animate-spin uploading"
/>
<button
v-else-if="backgroundPreview"
class="btn btn-default"
@click="submitBg"
>
{{ $t('general.submit') }}
</button>
<div
v-if="backgroundUploadError"
class="alert error"
>
Error: {{ backgroundUploadError }}
<i
class="button-icon icon-cancel"
@click="clearUploadError('background')"
/>
</div>
</div>
</div>
</template>
<script src="./profile_tab.js"></script>
<style lang="scss" src="./profile_tab.scss"></style>

@ -137,20 +137,20 @@
<script src="./mfa.js"></script> <script src="./mfa.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../../../_variables.scss';
.warning {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
.mfa-settings { .mfa-settings {
.mfa-heading, .method-item { .mfa-heading, .method-item {
overflow: hidden;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
} }
.warning {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
.setup-otp { .setup-otp {
display: flex; display: flex;
justify-content: center; justify-content: center;

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="mfa-backup-codes">
<h4 v-if="displayTitle"> <h4 v-if="displayTitle">
{{ $t('settings.mfa.recovery_codes') }} {{ $t('settings.mfa.recovery_codes') }}
</h4> </h4>
@ -21,8 +21,9 @@
</template> </template>
<script src="./mfa_backup_codes.js"></script> <script src="./mfa_backup_codes.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../../../_variables.scss';
.mfa-backup-codes {
.warning { .warning {
color: $fallback--cOrange; color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange); color: var(--cOrange, $fallback--cOrange);
@ -30,4 +31,5 @@
.backup-codes { .backup-codes {
font-family: var(--postCodeFont, monospace); font-family: var(--postCodeFont, monospace);
} }
}
</style> </style>

@ -0,0 +1,106 @@
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Mfa from './mfa.vue'
const SecurityTab = {
data () {
return {
newEmail: '',
changeEmailError: false,
changeEmailPassword: '',
changedEmail: false,
deletingAccount: false,
deleteAccountConfirmPasswordInput: '',
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
changePasswordError: false
}
},
created () {
this.$store.dispatch('fetchTokens')
},
components: {
ProgressButton,
Mfa,
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
},
pleromaBackend () {
return this.$store.state.instance.pleromaBackend
},
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
}
})
}
},
methods: {
confirmDelete () {
this.deletingAccount = true
},
deleteAccount () {
this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
.then((res) => {
if (res.status === 'success') {
this.$store.dispatch('logout')
this.$router.push({ name: 'root' })
} else {
this.deleteAccountError = res.error
}
})
},
changePassword () {
const params = {
password: this.changePasswordInputs[0],
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
this.$store.state.api.backendInteractor.changePassword(params)
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
this.changePasswordError = false
this.logout()
} else {
this.changedPassword = false
this.changePasswordError = res.error
}
})
},
changeEmail () {
const params = {
email: this.newEmail,
password: this.changeEmailPassword
}
this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
this.changeEmailError = false
} else {
this.changedEmail = false
this.changeEmailError = res.error
}
})
},
logout () {
this.$store.dispatch('logout')
this.$router.replace('/')
},
revokeToken (id) {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
}
}
}
}
export default SecurityTab

@ -0,0 +1,143 @@
<template>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{ $t('settings.change_email') }}</h2>
<div>
<p>{{ $t('settings.new_email') }}</p>
<input
v-model="newEmail"
type="email"
autocomplete="email"
>
</div>
<div>
<p>{{ $t('settings.current_password') }}</p>
<input
v-model="changeEmailPassword"
type="password"
autocomplete="current-password"
>
</div>
<button
class="btn btn-default"
@click="changeEmail"
>
{{ $t('general.submit') }}
</button>
<p v-if="changedEmail">
{{ $t('settings.changed_email') }}
</p>
<template v-if="changeEmailError !== false">
<p>{{ $t('settings.change_email_error') }}</p>
<p>{{ changeEmailError }}</p>
</template>
</div>
<div class="setting-item">
<h2>{{ $t('settings.change_password') }}</h2>
<div>
<p>{{ $t('settings.current_password') }}</p>
<input
v-model="changePasswordInputs[0]"
type="password"
>
</div>
<div>
<p>{{ $t('settings.new_password') }}</p>
<input
v-model="changePasswordInputs[1]"
type="password"
>
</div>
<div>
<p>{{ $t('settings.confirm_new_password') }}</p>
<input
v-model="changePasswordInputs[2]"
type="password"
>
</div>
<button
class="btn btn-default"
@click="changePassword"
>
{{ $t('general.submit') }}
</button>
<p v-if="changedPassword">
{{ $t('settings.changed_password') }}
</p>
<p v-else-if="changePasswordError !== false">
{{ $t('settings.change_password_error') }}
</p>
<p v-if="changePasswordError">
{{ changePasswordError }}
</p>
</div>
<div class="setting-item">
<h2>{{ $t('settings.oauth_tokens') }}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{ $t('settings.app_name') }}</th>
<th>{{ $t('settings.valid_until') }}</th>
<th />
</tr>
</thead>
<tbody>
<tr
v-for="oauthToken in oauthTokens"
:key="oauthToken.id"
>
<td>{{ oauthToken.appName }}</td>
<td>{{ oauthToken.validUntil }}</td>
<td class="actions">
<button
class="btn btn-default"
@click="revokeToken(oauthToken.id)"
>
{{ $t('settings.revoke_token') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<mfa />
<div class="setting-item">
<h2>{{ $t('settings.delete_account') }}</h2>
<p v-if="!deletingAccount">
{{ $t('settings.delete_account_description') }}
</p>
<div v-if="deletingAccount">
<p>{{ $t('settings.delete_account_instructions') }}</p>
<p>{{ $t('login.password') }}</p>
<input
v-model="deleteAccountConfirmPasswordInput"
type="password"
>
<button
class="btn btn-default"
@click="deleteAccount"
>
{{ $t('settings.delete_account') }}
</button>
</div>
<p v-if="deleteAccountError !== false">
{{ $t('settings.delete_account_error') }}
</p>
<p v-if="deleteAccountError">
{{ deleteAccountError }}
</p>
<button
v-if="!deletingAccount"
class="btn btn-default"
@click="confirmDelete"
>
{{ $t('general.submit') }}
</button>
</div>
</div>
</template>
<script src="./security_tab.js"></script>
<!-- <style lang="scss" src="./profile.scss"></style> -->

@ -3,7 +3,7 @@ import {
rgb2hex, rgb2hex,
hex2rgb, hex2rgb,
getContrastRatioLayers getContrastRatioLayers
} from '../../services/color_convert/color_convert.js' } from 'src/services/color_convert/color_convert.js'
import { import {
DEFAULT_SHADOWS, DEFAULT_SHADOWS,
generateColors, generateColors,
@ -14,26 +14,27 @@ import {
getThemes, getThemes,
shadows2to3, shadows2to3,
colors2to3 colors2to3
} from '../../services/style_setter/style_setter.js' } from 'src/services/style_setter/style_setter.js'
import { import {
SLOT_INHERITANCE SLOT_INHERITANCE
} from '../../services/theme_data/pleromafe.js' } from 'src/services/theme_data/pleromafe.js'
import { import {
CURRENT_VERSION, CURRENT_VERSION,
OPACITIES, OPACITIES,
getLayers, getLayers,
getOpacitySlot getOpacitySlot
} from '../../services/theme_data/theme_data.service.js' } from 'src/services/theme_data/theme_data.service.js'
import ColorInput from '../color_input/color_input.vue' import ColorInput from 'src/components/color_input/color_input.vue'
import RangeInput from '../range_input/range_input.vue' import RangeInput from 'src/components/range_input/range_input.vue'
import OpacityInput from '../opacity_input/opacity_input.vue' import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
import ShadowControl from '../shadow_control/shadow_control.vue' import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
import FontControl from '../font_control/font_control.vue' import FontControl from 'src/components/font_control/font_control.vue'
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue' import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import TabSwitcher from '../tab_switcher/tab_switcher.js' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
import ExportImport from 'src/components/export_import/export_import.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Preview from './preview.vue' import Preview from './preview.vue'
import ExportImport from '../export_import/export_import.vue'
import Checkbox from '../checkbox/checkbox.vue'
// List of color values used in v1 // List of color values used in v1
const v1OnlyNames = [ const v1OnlyNames = [

@ -1,5 +1,6 @@
@import '../../_variables.scss'; @import 'src/_variables.scss';
.style-switcher { .theme-tab {
padding-bottom: 2em;
.theme-warning { .theme-warning {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
@ -54,10 +55,6 @@
} }
} }
.tab-switcher {
margin: 0 -1em;
}
.reset-container { .reset-container {
flex-wrap: wrap; flex-wrap: wrap;
} }
@ -98,20 +95,25 @@
align-items: baseline; align-items: baseline;
width: 100%; width: 100%;
min-height: 30px; min-height: 30px;
margin-bottom: 1em;
.btn {
min-width: 1px;
flex: 0 auto;
padding: 0 1em;
}
p { p {
flex: 1; flex: 1;
margin: 0; margin: 0;
margin-right: .5em; margin-right: .5em;
} }
}
margin-bottom: 1em; .tab-header-buttons {
display: flex;
flex-direction: column;
.btn {
min-width: 1px;
flex: 0 auto;
padding: 0 1em;
margin-bottom: .5em;
}
} }
.shadow-selector { .shadow-selector {
@ -161,7 +163,7 @@
border-bottom: 1px dashed; border-bottom: 1px dashed;
border-color: $fallback--border; border-color: $fallback--border;
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
margin: 1em -1em 0; margin: 1em 0;
padding: 1em; padding: 1em;
background: var(--body-background-image); background: var(--body-background-image);
background-size: cover; background-size: cover;
@ -328,6 +330,14 @@
padding: 20px; padding: 20px;
} }
.apply-container {
.btn {
min-height: 28px;
min-width: 10em;
padding: 0 2em;
}
}
.btn { .btn {
margin-left: .25em; margin-left: .25em;
margin-right: .25em; margin-right: .25em;

@ -1,5 +1,5 @@
<template> <template>
<div class="style-switcher"> <div class="theme-tab">
<div class="presets-container"> <div class="presets-container">
<div class="save-load"> <div class="save-load">
<div <div
@ -126,6 +126,7 @@
> >
<div class="tab-header"> <div class="tab-header">
<p>{{ $t('settings.theme_help') }}</p> <p>{{ $t('settings.theme_help') }}</p>
<div class="tab-header-buttons">
<button <button
class="btn" class="btn"
@click="clearOpacity" @click="clearOpacity"
@ -139,6 +140,7 @@
{{ $t('settings.style.switcher.clear_all') }} {{ $t('settings.style.switcher.clear_all') }}
</button> </button>
</div> </div>
</div>
<p>{{ $t('settings.theme_help_v2_1') }}</p> <p>{{ $t('settings.theme_help_v2_1') }}</p>
<h4>{{ $t('settings.style.common_colors.main') }}</h4> <h4>{{ $t('settings.style.common_colors.main') }}</h4>
<div class="color-item"> <div class="color-item">
@ -951,6 +953,6 @@
</div> </div>
</template> </template>
<script src="./style_switcher.js"></script> <script src="./theme_tab.js"></script>
<style src="./style_switcher.scss" lang="scss"></style> <style src="./theme_tab.scss" lang="scss"></style>

@ -0,0 +1,24 @@
import { extractCommit } from 'src/services/version/version.service'
const pleromaFeCommitUrl = 'https://git.pleroma.social/sjw/pleroma-fe/commit/'
const pleromaBeCommitUrl = 'https://git.pleroma.social/sjw/pleroma/commit/'
const VersionTab = {
data () {
const instance = this.$store.state.instance
return {
backendVersion: instance.backendVersion,
frontendVersion: instance.frontendVersion
}
},
computed: {
frontendVersionLink () {
return pleromaFeCommitUrl + this.frontendVersion
},
backendVersionLink () {
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
}
}
}
export default VersionTab

@ -0,0 +1,31 @@
<template>
<div :label="$t('settings.version.title')">
<div class="setting-item">
<ul class="setting-list">
<li>
<p>{{ $t('settings.version.backend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="backendVersionLink"
target="_blank"
>{{ backendVersion }}</a>
</li>
</ul>
</li>
<li>
<p>{{ $t('settings.version.frontend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="frontendVersionLink"
target="_blank"
>{{ frontendVersion }}</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./version_tab.js">

@ -62,6 +62,9 @@ const SideDrawer = {
}, },
touchMove (e) { touchMove (e) {
GestureService.updateSwipe(e, this.closeGesture) GestureService.updateSwipe(e, this.closeGesture)
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
} }
} }
} }

@ -122,9 +122,12 @@
</router-link> </router-link>
</li> </li>
<li @click="toggleDrawer"> <li @click="toggleDrawer">
<router-link :to="{ name: 'settings' }"> <a
href="#"
@click="openSettingsModal"
>
<i class="button-icon icon-cog" /> {{ $t("settings.settings") }} <i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
</router-link> </a>
</li> </li>
<li @click="toggleDrawer"> <li @click="toggleDrawer">
<router-link :to="{ name: 'about'}"> <router-link :to="{ name: 'about'}">

@ -24,6 +24,11 @@ export default Vue.component('tab-switcher', {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: false
},
sideTabBar: {
required: false,
type: Boolean,
default: false
} }
}, },
data () { data () {
@ -55,6 +60,9 @@ export default Vue.component('tab-switcher', {
this.onSwitch.call(null, this.$slots.default[index].key) this.onSwitch.call(null, this.$slots.default[index].key)
} }
this.active = index this.active = index
if (this.scrollableTabs) {
this.$refs.contents.scrollTop = 0
}
} }
} }
}, },
@ -64,7 +72,6 @@ export default Vue.component('tab-switcher', {
if (!slot.tag) return if (!slot.tag) return
const classesTab = ['tab'] const classesTab = ['tab']
const classesWrapper = ['tab-wrapper'] const classesWrapper = ['tab-wrapper']
if (this.activeIndex === index) { if (this.activeIndex === index) {
classesTab.push('active') classesTab.push('active')
classesWrapper.push('active') classesWrapper.push('active')
@ -87,8 +94,14 @@ export default Vue.component('tab-switcher', {
<button <button
disabled={slot.data.attrs.disabled} disabled={slot.data.attrs.disabled}
onClick={this.activateTab(index)} onClick={this.activateTab(index)}
class={classesTab.join(' ')}> class={classesTab.join(' ')}
{slot.data.attrs.label}</button> type="button"
>
{!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)}
<span class="text">
{slot.data.attrs.label}
</span>
</button>
</div> </div>
) )
}) })
@ -96,20 +109,32 @@ export default Vue.component('tab-switcher', {
const contents = this.$slots.default.map((slot, index) => { const contents = this.$slots.default.map((slot, index) => {
if (!slot.tag) return if (!slot.tag) return
const active = this.activeIndex === index const active = this.activeIndex === index
if (this.renderOnlyFocused) { const classes = [ active ? 'active' : 'hidden' ]
return active if (slot.data.attrs.fullHeight) {
? <div class="active">{slot}</div> classes.push('full-height')
: <div class="hidden"></div> }
const renderSlot = (!this.renderOnlyFocused || active)
? slot
: ''
return (
<div class={classes}>
{
this.sideTabBar
? <h1 class="mobile-label">{slot.data.attrs.label}</h1>
: ''
} }
return <div class={active ? 'active' : 'hidden' }>{slot}</div> {renderSlot}
</div>
)
}) })
return ( return (
<div class="tab-switcher"> <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
<div class="tabs"> <div class="tabs">
{tabs} {tabs}
</div> </div>
<div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}> <div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
{contents} {contents}
</div> </div>
</div> </div>

@ -2,60 +2,177 @@
.tab-switcher { .tab-switcher {
display: flex; display: flex;
flex-direction: column;
.contents {
flex: 1 0 auto;
min-height: 0px;
.hidden { .tab-icon {
display: none; font-size: 2em;
display: block;
} }
&.scrollable-tabs { &.top-tabs {
flex-basis: 0; flex-direction: column;
overflow-y: auto;
} > .tabs {
}
.tabs {
display: flex;
position: relative;
width: 100%; width: 100%;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
padding-top: 5px; padding-top: 5px;
box-sizing: border-box; flex-direction: row;
&::after, &::before { &::after, &::before {
display: block;
content: ''; content: '';
flex: 1 1 auto; flex: 1 1 auto;
border-bottom: 1px solid; border-bottom: 1px solid;
border-bottom-color: $fallback--border; border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border); border-bottom-color: var(--border, $fallback--border);
} }
.tab-wrapper { .tab-wrapper {
height: 28px; height: 28px;
position: relative;
display: flex;
flex: 0 0 auto;
&:not(.active)::after {
left: 0;
right: 0;
bottom: 0;
border-bottom: 1px solid;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
}
}
.tab { .tab {
width: 100%; width: 100%;
min-width: 1px; min-width: 1px;
position: relative;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
padding: 6px 1em;
padding-bottom: 99px; padding-bottom: 99px;
margin-bottom: 6px - 99px; margin-bottom: 6px - 99px;
}
}
.contents.scrollable-tabs {
flex-basis: 0;
}
}
&.side-tabs {
flex-direction: row;
@media all and (max-width: 800px) {
overflow-x: auto;
}
> .contents {
flex: 1 1 auto;
}
> .tabs {
flex: 0 0 auto;
overflow-y: auto;
overflow-x: hidden;
flex-direction: column;
&::after, &::before {
flex-shrink: 0;
flex-basis: .5em;
content: '';
border-right: 1px solid;
border-right-color: $fallback--border;
border-right-color: var(--border, $fallback--border);
}
&::after {
flex-grow: 1;
}
&::before {
flex-grow: 0;
}
.tab-wrapper {
min-width: 10em;
display: flex;
flex-direction: column;
@media all and (max-width: 800px) {
min-width: 1em;
}
&:not(.active)::after {
top: 0;
right: 0;
bottom: 0;
border-right: 1px solid;
border-right-color: $fallback--border;
border-right-color: var(--border, $fallback--border);
}
&::before {
flex: 0 0 6px;
content: '';
border-right: 1px solid;
border-right-color: $fallback--border;
border-right-color: var(--border, $fallback--border);
}
&:last-child .tab {
margin-bottom: 0;
}
}
.tab {
flex: 1;
box-sizing: content-box;
min-width: 10em;
min-width: 1px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding-left: 1em;
padding-right: calc(1em + 200px);
margin-right: -200px;
margin-left: 1em;
@media all and (max-width: 800px) {
padding-left: .25em;
padding-right: calc(.25em + 200px);
margin-right: calc(.25em - 200px);
margin-left: .25em;
.text {
display: none
}
}
}
}
}
.contents {
flex: 1 0 auto;
min-height: 0px;
.hidden {
display: none;
}
.full-height:not(.hidden) {
height: 100%;
display: flex;
flex-direction: column;
> *:not(.mobile-label) {
flex: 1;
}
}
&.scrollable-tabs {
overflow-y: auto;
}
}
.tab {
position: relative;
white-space: nowrap; white-space: nowrap;
padding: 6px 1em;
background-color: $fallback--fg;
background-color: var(--tab, $fallback--fg);
&, &:active .tab-icon {
color: $fallback--text; color: $fallback--text;
color: var(--tabText, $fallback--text); color: var(--tabText, $fallback--text);
background-color: $fallback--fg; }
background-color: var(--tab, $fallback--fg);
&:not(.active) { &:not(.active) {
z-index: 4; z-index: 4;
@ -79,20 +196,41 @@
} }
} }
.tabs {
display: flex;
position: relative;
box-sizing: border-box;
&::after, &::before {
display: block;
flex: 1 1 auto;
}
}
.tab-wrapper {
position: relative;
display: flex;
flex: 0 0 auto;
&:not(.active) { &:not(.active) {
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 7; z-index: 7;
border-bottom: 1px solid;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
} }
} }
} }
.mobile-label {
padding-left: .3em;
padding-bottom: .25em;
margin-top: .5em;
margin-left: .2em;
margin-bottom: .25em;
border-bottom: 1px solid var(--border, $fallback--border);
@media all and (min-width: 800px) {
display: none;
}
} }
} }

@ -50,15 +50,6 @@
> >
{{ user.name }} {{ user.name }}
</div> </div>
<router-link
v-if="!isOtherUser"
:to="{ name: 'user-settings' }"
>
<i
class="button-icon icon-wrench usersettings"
:title="$t('tool_tip.user_settings')"
/>
</router-link>
<a <a
v-if="isOtherUser && !user.is_local" v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url" :href="user.statusnet_profile_url"
@ -118,7 +109,7 @@
type="color" type="color"
> >
<label <label
for="style-switcher" for="theme_tab"
class="userHighlightSel select" class="userHighlightSel select"
> >
<select <select

@ -3,6 +3,7 @@ import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
import Conversation from '../conversation/conversation.vue' import Conversation from '../conversation/conversation.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
import List from '../list/list.vue' import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more' import withLoadMore from '../../hocs/with_load_more/with_load_more'
@ -146,6 +147,7 @@ const UserProfile = {
FollowerList, FollowerList,
FriendList, FriendList,
FollowCard, FollowCard,
TabSwitcher,
Conversation Conversation
} }
} }

@ -1,393 +0,0 @@
import unescape from 'lodash/unescape'
import get from 'lodash/get'
import map from 'lodash/map'
import reject from 'lodash/reject'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji_input/emoji_input.vue'
import suggestor from '../emoji_input/suggestor.js'
import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
import Checkbox from '../checkbox/checkbox.vue'
import Mfa from './mfa.vue'
const BlockList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'items'
})(SelectableList)
const MuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'items'
})(SelectableList)
const DomainMuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
childPropName: 'items'
})(SelectableList)
const UserSettings = {
data () {
return {
newEmail: '',
newName: this.$store.state.users.currentUser.name,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
newDefaultScope: this.$store.state.users.currentUser.default_scope,
hideFollows: this.$store.state.users.currentUser.hide_follows,
hideFollowers: this.$store.state.users.currentUser.hide_followers,
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
discoverable: this.$store.state.users.currentUser.discoverable,
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
banner: null,
bannerPreview: null,
background: null,
backgroundPreview: null,
bannerUploadError: null,
backgroundUploadError: null,
changeEmailError: false,
changeEmailPassword: '',
changedEmail: false,
deletingAccount: false,
deleteAccountConfirmPasswordInput: '',
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
changePasswordError: false,
activeTab: 'profile',
notificationSettings: this.$store.state.users.currentUser.notification_settings,
newDomainToMute: ''
}
},
created () {
this.$store.dispatch('fetchTokens')
},
components: {
StyleSwitcher,
ScopeSelector,
TabSwitcher,
ImageCropper,
BlockList,
MuteList,
DomainMuteList,
EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
DomainMuteCard,
ProgressButton,
Importer,
Exporter,
Mfa,
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
},
emojiUserSuggestor () {
return suggestor({
emoji: [
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
],
users: this.$store.state.users.users,
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
})
},
emojiSuggestor () {
return suggestor({ emoji: [
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
] })
},
pleromaBackend () {
return this.$store.state.instance.pleromaBackend
},
minimalScopesMode () {
return this.$store.state.instance.minimalScopesMode
},
vis () {
return {
public: { selected: this.newDefaultScope === 'public' },
unlisted: { selected: this.newDefaultScope === 'unlisted' },
private: { selected: this.newDefaultScope === 'private' },
direct: { selected: this.newDefaultScope === 'direct' }
}
},
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
}
})
}
},
methods: {
updateProfile () {
this.$store.state.api.backendInteractor
.updateProfile({
params: {
note: this.newBio,
locked: this.newLocked,
// Backend notation.
/* eslint-disable camelcase */
display_name: this.newName,
default_scope: this.newDefaultScope,
no_rich_text: this.newNoRichText,
hide_follows: this.hideFollows,
hide_followers: this.hideFollowers,
discoverable: this.discoverable,
allow_following_move: this.allowFollowingMove,
hide_follows_count: this.hideFollowsCount,
hide_followers_count: this.hideFollowersCount,
show_role: this.showRole
/* eslint-enable camelcase */
} }).then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
})
},
updateNotificationSettings () {
this.$store.state.api.backendInteractor
.updateNotificationSettings({ settings: this.notificationSettings })
},
changeVis (visibility) {
this.newDefaultScope = visibility
},
uploadFile (slot, e) {
const file = e.target.files[0]
if (!file) { return }
if (file.size > this.$store.state.instance[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
return
}
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({ target }) => {
const img = target.result
this[slot + 'Preview'] = img
this[slot] = file
}
reader.readAsDataURL(file)
},
submitAvatar (cropper, file) {
const that = this
return new Promise((resolve, reject) => {
function updateAvatar (avatar) {
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
.then((user) => {
that.$store.commit('addNewUsers', [user])
that.$store.commit('setCurrentUser', user)
resolve()
})
.catch((err) => {
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
})
}
if (cropper) {
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
} else {
updateAvatar(file)
}
})
},
clearUploadError (slot) {
this[slot + 'UploadError'] = null
},
submitBanner () {
if (!this.bannerPreview) { return }
this.bannerUploading = true
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
.then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
this.bannerPreview = null
})
.catch((err) => {
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
})
.then(() => { this.bannerUploading = false })
},
submitBg () {
if (!this.backgroundPreview) { return }
let background = this.background
this.backgroundUploading = true
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
if (!data.error) {
this.$store.commit('addNewUsers', [data])
this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null
} else {
this.backgroundUploadError = this.$t('upload.error.base') + data.error
}
this.backgroundUploading = false
})
},
importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
generateExportableUsersContent (users) {
// Get addresses
return users.map((user) => {
// check is it's a local user
if (user && user.is_local) {
// append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname
}
return user.screen_name
}).join('\n')
},
getFollowsContent () {
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
.then(this.generateExportableUsersContent)
},
getBlocksContent () {
return this.$store.state.api.backendInteractor.fetchBlocks()
.then(this.generateExportableUsersContent)
},
confirmDelete () {
this.deletingAccount = true
},
deleteAccount () {
this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
.then((res) => {
if (res.status === 'success') {
this.$store.dispatch('logout')
this.$router.push({ name: 'root' })
} else {
this.deleteAccountError = res.error
}
})
},
changePassword () {
const params = {
password: this.changePasswordInputs[0],
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
this.$store.state.api.backendInteractor.changePassword(params)
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
this.changePasswordError = false
this.logout()
} else {
this.changedPassword = false
this.changePasswordError = res.error
}
})
},
changeEmail () {
const params = {
email: this.newEmail,
password: this.changeEmailPassword
}
this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
this.changeEmailError = false
} else {
this.changedEmail = false
this.changeEmailError = res.error
}
})
},
activateTab (tabName) {
this.activeTab = tabName
},
logout () {
this.$store.dispatch('logout')
this.$router.replace('/')
},
revokeToken (id) {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
}
},
filterUnblockedUsers (userIds) {
return reject(userIds, (userId) => {
const relationship = this.$store.getters.relationship(this.userId)
return relationship.blocking || userId === this.$store.state.users.currentUser.id
})
},
filterUnMutedUsers (userIds) {
return reject(userIds, (userId) => {
const relationship = this.$store.getters.relationship(this.userId)
return relationship.muting || userId === this.$store.state.users.currentUser.id
})
},
queryUserIds (query) {
return this.$store.dispatch('searchUsers', { query })
.then((users) => map(users, 'id'))
},
blockUsers (ids) {
return this.$store.dispatch('blockUsers', ids)
},
unblockUsers (ids) {
return this.$store.dispatch('unblockUsers', ids)
},
muteUsers (ids) {
return this.$store.dispatch('muteUsers', ids)
},
unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
unmuteDomains (domains) {
return this.$store.dispatch('unmuteDomains', domains)
},
muteDomain () {
return this.$store.dispatch('muteDomain', this.newDomainToMute)
.then(() => { this.newDomainToMute = '' })
},
identity (value) {
return value
}
}
}
export default UserSettings

@ -1,728 +0,0 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('settings.user_settings') }}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div
v-if="currentSaveStateNotice.error"
class="alert error"
@click.prevent
>
{{ $t('settings.saving_err') }}
</div>
<div
v-if="!currentSaveStateNotice.error"
class="alert transparent"
@click.prevent
>
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
</div>
<div class="panel-body profile-edit">
<tab-switcher>
<div :label="$t('settings.profile_tab')">
<div class="setting-item">
<h2>{{ $t('settings.name_bio') }}</h2>
<p>{{ $t('settings.name') }}</p>
<EmojiInput
v-model="newName"
enable-emoji-picker
:suggest="emojiSuggestor"
>
<input
id="username"
v-model="newName"
classname="name-changer"
>
</EmojiInput>
<p>{{ $t('settings.bio') }}</p>
<EmojiInput
v-model="newBio"
enable-emoji-picker
:suggest="emojiUserSuggestor"
>
<textarea
v-model="newBio"
classname="bio"
/>
</EmojiInput>
<p>
<Checkbox v-model="newLocked">
{{ $t('settings.lock_account_description') }}
</Checkbox>
</p>
<div>
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
<div
id="default-vis"
class="visibility-tray"
>
<scope-selector
:show-all="true"
:user-default="newDefaultScope"
:initial-scope="newDefaultScope"
:on-scope-change="changeVis"
/>
</div>
</div>
<p>
<Checkbox v-model="newNoRichText">
{{ $t('settings.no_rich_text_description') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="hideFollows">
{{ $t('settings.hide_follows_description') }}
</Checkbox>
</p>
<p class="setting-subitem">
<Checkbox
v-model="hideFollowsCount"
:disabled="!hideFollows"
>
{{ $t('settings.hide_follows_count_description') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="hideFollowers">
{{ $t('settings.hide_followers_description') }}
</Checkbox>
</p>
<p class="setting-subitem">
<Checkbox
v-model="hideFollowersCount"
:disabled="!hideFollowers"
>
{{ $t('settings.hide_followers_count_description') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="allowFollowingMove">
{{ $t('settings.allow_following_move') }}
</Checkbox>
</p>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
<template v-if="role === 'admin'">
{{ $t('settings.show_admin_badge') }}
</template>
<template v-if="role === 'moderator'">
{{ $t('settings.show_moderator_badge') }}
</template>
</Checkbox>
</p>
<p>
<Checkbox v-model="discoverable">
{{ $t('settings.discoverable') }}
</Checkbox>
</p>
<button
:disabled="newName && newName.length === 0"
class="btn btn-default"
@click="updateProfile"
>
{{ $t('general.submit') }}
</button>
</div>
<div class="setting-item">
<h2>{{ $t('settings.avatar') }}</h2>
<p class="visibility-notice">
{{ $t('settings.avatar_size_instruction') }}
</p>
<p>{{ $t('settings.current_avatar') }}</p>
<img
:src="user.profile_image_url_original"
class="current-avatar"
>
<p>{{ $t('settings.set_new_avatar') }}</p>
<button
v-show="pickAvatarBtnVisible"
id="pick-avatar"
class="btn"
type="button"
>
{{ $t('settings.upload_a_photo') }}
</button>
<image-cropper
trigger="#pick-avatar"
:submit-handler="submitAvatar"
@open="pickAvatarBtnVisible=false"
@close="pickAvatarBtnVisible=true"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.profile_banner') }}</h2>
<p>{{ $t('settings.current_profile_banner') }}</p>
<img
:src="user.cover_photo"
class="banner"
>
<p>{{ $t('settings.set_new_profile_banner') }}</p>
<img
v-if="bannerPreview"
class="banner"
:src="bannerPreview"
>
<div>
<input
type="file"
@change="uploadFile('banner', $event)"
>
</div>
<i
v-if="bannerUploading"
class=" icon-spin4 animate-spin uploading"
/>
<button
v-else-if="bannerPreview"
class="btn btn-default"
@click="submitBanner"
>
{{ $t('general.submit') }}
</button>
<div
v-if="bannerUploadError"
class="alert error"
>
Error: {{ bannerUploadError }}
<i
class="button-icon icon-cancel"
@click="clearUploadError('banner')"
/>
</div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.profile_background') }}</h2>
<p>{{ $t('settings.set_new_profile_background') }}</p>
<img
v-if="backgroundPreview"
class="bg"
:src="backgroundPreview"
>
<div>
<input
type="file"
@change="uploadFile('background', $event)"
>
</div>
<i
v-if="backgroundUploading"
class=" icon-spin4 animate-spin uploading"
/>
<button
v-else-if="backgroundPreview"
class="btn btn-default"
@click="submitBg"
>
{{ $t('general.submit') }}
</button>
<div
v-if="backgroundUploadError"
class="alert error"
>
Error: {{ backgroundUploadError }}
<i
class="button-icon icon-cancel"
@click="clearUploadError('background')"
/>
</div>
</div>
</div>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
<h2>{{ $t('settings.change_email') }}</h2>
<div>
<p>{{ $t('settings.new_email') }}</p>
<input
v-model="newEmail"
type="email"
autocomplete="email"
>
</div>
<div>
<p>{{ $t('settings.current_password') }}</p>
<input
v-model="changeEmailPassword"
type="password"
autocomplete="current-password"
>
</div>
<button
class="btn btn-default"
@click="changeEmail"
>
{{ $t('general.submit') }}
</button>
<p v-if="changedEmail">
{{ $t('settings.changed_email') }}
</p>
<template v-if="changeEmailError !== false">
<p>{{ $t('settings.change_email_error') }}</p>
<p>{{ changeEmailError }}</p>
</template>
</div>
<div class="setting-item">
<h2>{{ $t('settings.change_password') }}</h2>
<div>
<p>{{ $t('settings.current_password') }}</p>
<input
v-model="changePasswordInputs[0]"
type="password"
>
</div>
<div>
<p>{{ $t('settings.new_password') }}</p>
<input
v-model="changePasswordInputs[1]"
type="password"
>
</div>
<div>
<p>{{ $t('settings.confirm_new_password') }}</p>
<input
v-model="changePasswordInputs[2]"
type="password"
>
</div>
<button
class="btn btn-default"
@click="changePassword"
>
{{ $t('general.submit') }}
</button>
<p v-if="changedPassword">
{{ $t('settings.changed_password') }}
</p>
<p v-else-if="changePasswordError !== false">
{{ $t('settings.change_password_error') }}
</p>
<p v-if="changePasswordError">
{{ changePasswordError }}
</p>
</div>
<div class="setting-item">
<h2>{{ $t('settings.oauth_tokens') }}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{ $t('settings.app_name') }}</th>
<th>{{ $t('settings.valid_until') }}</th>
<th />
</tr>
</thead>
<tbody>
<tr
v-for="oauthToken in oauthTokens"
:key="oauthToken.id"
>
<td>{{ oauthToken.appName }}</td>
<td>{{ oauthToken.validUntil }}</td>
<td class="actions">
<button
class="btn btn-default"
@click="revokeToken(oauthToken.id)"
>
{{ $t('settings.revoke_token') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<mfa />
<div class="setting-item">
<h2>{{ $t('settings.delete_account') }}</h2>
<p v-if="!deletingAccount">
{{ $t('settings.delete_account_description') }}
</p>
<div v-if="deletingAccount">
<p>{{ $t('settings.delete_account_instructions') }}</p>
<p>{{ $t('login.password') }}</p>
<input
v-model="deleteAccountConfirmPasswordInput"
type="password"
>
<button
class="btn btn-default"
@click="deleteAccount"
>
{{ $t('settings.delete_account') }}
</button>
</div>
<p v-if="deleteAccountError !== false">
{{ $t('settings.delete_account_error') }}
</p>
<p v-if="deleteAccountError">
{{ deleteAccountError }}
</p>
<button
v-if="!deletingAccount"
class="btn btn-default"
@click="confirmDelete"
>
{{ $t('general.submit') }}
</button>
</div>
</div>
<div
v-if="pleromaBackend"
:label="$t('settings.notifications')"
>
<div class="setting-item">
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<div class="select-multiple">
<span class="label">{{ $t('settings.notification_setting') }}</span>
<ul class="option-list">
<li>
<Checkbox v-model="notificationSettings.follows">
{{ $t('settings.notification_setting_follows') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationSettings.followers">
{{ $t('settings.notification_setting_followers') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationSettings.non_follows">
{{ $t('settings.notification_setting_non_follows') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationSettings.non_followers">
{{ $t('settings.notification_setting_non_followers') }}
</Checkbox>
</li>
</ul>
</div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
<p>
<Checkbox v-model="notificationSettings.privacy_option">
{{ $t('settings.notification_setting_privacy_option') }}
</Checkbox>
</p>
</div>
<div class="setting-item">
<p>{{ $t('settings.notification_mutes') }}</p>
<p>{{ $t('settings.notification_blocks') }}</p>
<button
class="btn btn-default"
@click="updateNotificationSettings"
>
{{ $t('general.submit') }}
</button>
</div>
</div>
<div
v-if="pleromaBackend"
:label="$t('settings.data_import_export_tab')"
>
<div class="setting-item">
<h2>{{ $t('settings.follow_import') }}</h2>
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
<Importer
:submit-handler="importFollows"
:success-message="$t('settings.follows_imported')"
:error-message="$t('settings.follow_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.follow_export') }}</h2>
<Exporter
:get-content="getFollowsContent"
filename="friends.csv"
:export-button-label="$t('settings.follow_export_button')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.block_import') }}</h2>
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
<Importer
:submit-handler="importBlocks"
:success-message="$t('settings.blocks_imported')"
:error-message="$t('settings.block_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.block_export') }}</h2>
<Exporter
:get-content="getBlocksContent"
filename="blocks.csv"
:export-button-label="$t('settings.block_export_button')"
/>
</div>
</div>
<div :label="$t('settings.blocks_tab')">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest
:filter="filterUnblockedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_block')"
>
<BlockCard
slot-scope="row"
:user-id="row.item"
/>
</Autosuggest>
</div>
<BlockList
:refresh="true"
:get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => blockUsers(selected)"
>
{{ $t('user_card.block') }}
<template slot="progress">
{{ $t('user_card.block_progress') }}
</template>
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unblockUsers(selected)"
>
{{ $t('user_card.unblock') }}
<template slot="progress">
{{ $t('user_card.unblock_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<BlockCard :user-id="item" />
</template>
<template slot="empty">
{{ $t('settings.no_blocks') }}
</template>
</BlockList>
</div>
<div :label="$t('settings.mutes_tab')">
<tab-switcher>
<div label="Users">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')"
>
<MuteCard
slot-scope="row"
:user-id="row.item"
/>
</Autosuggest>
</div>
<MuteList
:refresh="true"
:get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => muteUsers(selected)"
>
{{ $t('user_card.mute') }}
<template slot="progress">
{{ $t('user_card.mute_progress') }}
</template>
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteUsers(selected)"
>
{{ $t('user_card.unmute') }}
<template slot="progress">
{{ $t('user_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<MuteCard :user-id="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</MuteList>
</div>
<div :label="$t('settings.domain_mutes')">
<div class="profile-edit-domain-mute-form">
<input
v-model="newDomainToMute"
:placeholder="$t('settings.type_domains_to_mute')"
type="text"
@keyup.enter="muteDomain"
>
<ProgressButton
class="btn btn-default"
:click="muteDomain"
>
{{ $t('domain_mute_card.mute') }}
<template slot="progress">
{{ $t('domain_mute_card.mute_progress') }}
</template>
</ProgressButton>
</div>
<DomainMuteList
:refresh="true"
:get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteDomains(selected)"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
{{ $t('domain_mute_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<DomainMuteCard :domain="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</DomainMuteList>
</div>
</tab-switcher>
</div>
</tab-switcher>
</div>
</div>
</template>
<script src="./user_settings.js">
</script>
<style lang="scss">
@import '../../_variables.scss';
.profile-edit {
.bio {
margin: 0;
}
.visibility-tray {
padding-top: 5px;
}
input[type=file] {
padding: 5px;
height: auto;
}
.banner {
max-width: 100%;
}
.uploading {
font-size: 1.5em;
margin: 0.25em;
}
.name-changer {
width: 100%;
}
.bg {
max-width: 100%;
}
.current-avatar {
display: block;
width: 150px;
height: 150px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
&-usersearch-wrapper {
padding: 1em;
}
&-bulk-actions {
text-align: right;
padding: 0 1em;
min-height: 28px;
button {
width: 10em;
}
}
&-domain-mute-form {
padding: 1em;
display: flex;
flex-direction: column;
button {
align-self: flex-end;
margin-top: 1em;
width: 10em;
}
}
.setting-subitem {
margin-left: 1.75em;
}
}
</style>

@ -34,9 +34,9 @@
}, },
"domain_mute_card": { "domain_mute_card": {
"mute": "Mute", "mute": "Mute",
"mute_progress": "Muting...", "mute_progress": "Muting",
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting..." "unmute_progress": "Unmuting"
}, },
"exporter": { "exporter": {
"export": "Export", "export": "Export",
@ -59,7 +59,10 @@
"apply": "Apply", "apply": "Apply",
"submit": "Submit", "submit": "Submit",
"more": "More", "more": "More",
"loading": "Loading…",
"generic_error": "An error occured", "generic_error": "An error occured",
"error_retry": "Please try again",
"retry": "Try again",
"optional": "optional", "optional": "optional",
"show_more": "Show more", "show_more": "Show more",
"show_less": "Show less", "show_less": "Show less",
@ -68,7 +71,9 @@
"disable": "Disable", "disable": "Disable",
"enable": "Enable", "enable": "Enable",
"confirm": "Confirm", "confirm": "Confirm",
"verify": "Verify" "verify": "Verify",
"close": "Close",
"peek": "Peek"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Crop picture", "crop_picture": "Crop picture",
@ -121,7 +126,7 @@
"preferences": "Preferences" "preferences": "Preferences"
}, },
"notifications": { "notifications": {
"broken_favorite": "Unknown status, searching for it...", "broken_favorite": "Unknown status, searching for it",
"favorited_you": "favorited your status", "favorited_you": "favorited your status",
"followed_you": "followed you", "followed_you": "followed you",
"follow_request": "wants to follow you", "follow_request": "wants to follow you",
@ -234,7 +239,7 @@
"generate_new_recovery_codes": "Generate new recovery codes", "generate_new_recovery_codes": "Generate new recovery codes",
"warning_of_generate_new_codes": "When you generate new recovery codes, your old codes wont work anymore.", "warning_of_generate_new_codes": "When you generate new recovery codes, your old codes wont work anymore.",
"recovery_codes": "Recovery codes.", "recovery_codes": "Recovery codes.",
"waiting_a_recovery_codes": "Receiving backup codes...", "waiting_a_recovery_codes": "Receiving backup codes",
"recovery_codes_warning": "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.", "recovery_codes_warning": "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.",
"authentication_methods": "Authentication methods", "authentication_methods": "Authentication methods",
"scan": { "scan": {
@ -278,6 +283,7 @@
"current_avatar": "Your current avatar", "current_avatar": "Your current avatar",
"current_password": "Current password", "current_password": "Current password",
"current_profile_banner": "Your current profile banner", "current_profile_banner": "Your current profile banner",
"mutes_and_blocks": "Mutes and Blocks",
"data_import_export_tab": "Data Import / Export", "data_import_export_tab": "Data Import / Export",
"default_vis": "Default visibility scope", "default_vis": "Default visibility scope",
"delete_account": "Delete Account", "delete_account": "Delete Account",
@ -653,11 +659,11 @@
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"unblock": "Unblock", "unblock": "Unblock",
"unblock_progress": "Unblocking...", "unblock_progress": "Unblocking",
"block_progress": "Blocking...", "block_progress": "Blocking",
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting...", "unmute_progress": "Unmuting",
"mute_progress": "Muting...", "mute_progress": "Muting",
"hide_repeats": "Hide repeats", "hide_repeats": "Hide repeats",
"show_repeats": "Show repeats", "show_repeats": "Show repeats",
"admin_menu": { "admin_menu": {

@ -103,7 +103,7 @@
"single_choice": "Elección única", "single_choice": "Elección única",
"multiple_choices": "Elección múltiple", "multiple_choices": "Elección múltiple",
"expiry": "Tiempo de vida de la encuesta", "expiry": "Tiempo de vida de la encuesta",
"expires_in": "La encuensta termina en {0}", "expires_in": "La encuesta termina en {0}",
"expired": "La encuesta terminó hace {0}", "expired": "La encuesta terminó hace {0}",
"not_enough_options": "Muy pocas opciones únicas en la encuesta" "not_enough_options": "Muy pocas opciones únicas en la encuesta"
}, },
@ -137,7 +137,7 @@
}, },
"content_warning": "Tema (opcional)", "content_warning": "Tema (opcional)",
"default": "Acabo de aterrizar en L.A.", "default": "Acabo de aterrizar en L.A.",
"direct_warning_to_all": "Esta publicación será visible para todos los usarios mencionados.", "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.", "direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.",
"posting": "Publicando", "posting": "Publicando",
"scope_notice": { "scope_notice": {
@ -146,7 +146,7 @@
"unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida" "unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida"
}, },
"scope": { "scope": {
"direct": "Directo - Solo para los usuarios mencionados.", "direct": "Directo - Solo para los usuarios mencionados",
"private": "Solo-seguidores - Solo tus seguidores leerán la publicación", "private": "Solo-seguidores - Solo tus seguidores leerán la publicación",
"public": "Público - Entradas visibles en las Líneas Temporales Públicas", "public": "Público - Entradas visibles en las Líneas Temporales Públicas",
"unlisted": "Sin listar - Entradas no visibles en las Líneas Temporales Públicas" "unlisted": "Sin listar - Entradas no visibles en las Líneas Temporales Públicas"
@ -210,7 +210,7 @@
"background": "Fondo", "background": "Fondo",
"bio": "Biografía", "bio": "Biografía",
"block_export": "Exportar usuarios bloqueados", "block_export": "Exportar usuarios bloqueados",
"block_export_button": "Exporta la lista de tus usarios bloqueados a un archivo csv", "block_export_button": "Exporta la lista de tus usuarios bloqueados a un archivo csv",
"block_import": "Importar usuarios bloqueados", "block_import": "Importar usuarios bloqueados",
"block_import_error": "Error importando la lista de usuarios bloqueados", "block_import_error": "Error importando la lista de usuarios bloqueados",
"blocks_imported": "¡Lista de usuarios bloqueados importada! El procesado puede tardar un poco.", "blocks_imported": "¡Lista de usuarios bloqueados importada! El procesado puede tardar un poco.",
@ -222,7 +222,7 @@
"cRed": "Rojo (Cancelar)", "cRed": "Rojo (Cancelar)",
"change_password": "Cambiar contraseña", "change_password": "Cambiar contraseña",
"change_password_error": "Hubo un problema cambiando la contraseña.", "change_password_error": "Hubo un problema cambiando la contraseña.",
"changed_password": "Contraseña cambiada correctamente!", "changed_password": "¡Contraseña cambiada correctamente!",
"collapse_subject": "Colapsar entradas con tema", "collapse_subject": "Colapsar entradas con tema",
"composing": "Redactando", "composing": "Redactando",
"confirm_new_password": "Confirmar la nueva contraseña", "confirm_new_password": "Confirmar la nueva contraseña",
@ -286,7 +286,7 @@
"notification_visibility_repeats": "Repeticiones (Repeats)", "notification_visibility_repeats": "Repeticiones (Repeats)",
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas", "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
"no_blocks": "No hay usuarios bloqueados", "no_blocks": "No hay usuarios bloqueados",
"no_mutes": "No hay usuarios sinlenciados", "no_mutes": "No hay usuarios silenciados",
"hide_follows_description": "No mostrar a quién sigo", "hide_follows_description": "No mostrar a quién sigo",
"hide_followers_description": "No mostrar quién me sigue", "hide_followers_description": "No mostrar quién me sigue",
"hide_follows_count_description": "No mostrar el número de cuentas que sigo", "hide_follows_count_description": "No mostrar el número de cuentas que sigo",
@ -305,7 +305,7 @@
"profile_background": "Fondo del Perfil", "profile_background": "Fondo del Perfil",
"profile_banner": "Cabecera del Perfil", "profile_banner": "Cabecera del Perfil",
"profile_tab": "Perfil", "profile_tab": "Perfil",
"radii_help": "Estable el redondeo de las esquinas de la interfaz (en píxeles)", "radii_help": "Establezca el redondeo de las esquinas de la interfaz (en píxeles)",
"replies_in_timeline": "Réplicas en la línea temporal", "replies_in_timeline": "Réplicas en la línea temporal",
"reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encima", "reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encima",
"reply_visibility_all": "Mostrar todas las réplicas", "reply_visibility_all": "Mostrar todas las réplicas",

@ -4,7 +4,19 @@
"find_user": "Otsi kasutajaid" "find_user": "Otsi kasutajaid"
}, },
"general": { "general": {
"submit": "Postita" "submit": "Postita",
"verify": "Kinnita",
"confirm": "Kinnita",
"enable": "Luba",
"disable": "Keela",
"cancel": "Tühista",
"dismiss": "Olgu",
"show_less": "Kuva vähem",
"show_more": "Kuva rohkem",
"optional": "valikuline",
"generic_error": "Esines viga",
"more": "Rohkem",
"apply": "Rakenda"
}, },
"login": { "login": {
"login": "Logi sisse", "login": "Logi sisse",
@ -12,29 +24,95 @@
"password": "Parool", "password": "Parool",
"placeholder": "nt lain", "placeholder": "nt lain",
"register": "Registreeru", "register": "Registreeru",
"username": "Kasutajanimi" "username": "Kasutajanimi",
"heading": {
"recovery": "Kaheastmelise autentimise taaste",
"totp": "Kaheastmeline autentimine"
},
"recovery_code": "Taastekood",
"enter_two_factor_code": "Sisesta kaheastmelise autentimise kood",
"enter_recovery_code": "Sisesta taastekood",
"authentication_code": "Autentimiskood",
"hint": "Logi sisse, et liituda vestlusega",
"description": "Logi sisse OAuthiga"
}, },
"nav": { "nav": {
"mentions": "Mainimised", "mentions": "Mainimised",
"public_tl": "Avalik Ajajoon", "public_tl": "Avalik Ajajoon",
"timeline": "Ajajoon", "timeline": "Ajajoon",
"twkn": "Kogu Teadaolev Võrgustik" "twkn": "Kogu Teadaolev Võrgustik",
"preferences": "Eelistused",
"who_to_follow": "Keda jälgida",
"search": "Otsing",
"user_search": "Kasutajaotsing",
"dms": "Privaatsõnumid",
"interactions": "Interaktsioonid",
"friend_requests": "Jägimistaotlused",
"chat": "Kohalik vestlus",
"back": "Tagasi",
"administration": "Administreerimine",
"about": "Meist"
}, },
"notifications": { "notifications": {
"followed_you": "alustas sinu jälgimist", "followed_you": "alustas sinu jälgimist",
"notifications": "Teavitused", "notifications": "Teated",
"read": "Loe!" "read": "Loe!",
"reacted_with": "reageeris {0}",
"migrated_to": "kolis",
"no_more_notifications": "Rohkem teateid ei ole",
"repeated_you": "taaspostitas su staatuse",
"load_older": "Laadi vanemad teated",
"follow_request": "soovib Teid jälgida",
"favorited_you": "lisas su staatuse lemmikuks",
"broken_favorite": "Tundmatu staatus, otsin…"
}, },
"post_status": { "post_status": {
"default": "Just sõitsin elektrirongiga Tallinnast Pääskülla.", "default": "Just sõitsin elektrirongiga Tallinnast Pääskülla.",
"posting": "Postitan" "posting": "Postitan",
"scope": {
"unlisted": "Peidetud - Ära postita avalikele ajajoontele",
"public": "Avalil - Postita avalikele ajajoontele",
"private": "Jälgijatele - Postita ainult jälgijatele",
"direct": "Privaatne - Postita ainult mainitud kasutajatele"
},
"scope_notice": {
"unlisted": "See postitus ei ole nähtav avalikul ega kogu võrgu ajajoonel",
"private": "See postitus on nähtav ainult Teie jälgijatele",
"public": "See postitus on nähtav kõigile"
},
"direct_warning_to_first_only": "See postitus on nähtav ainult kirja alguses mainitud kasutajatele.",
"direct_warning_to_all": "See postitus on nähtav kõikidele mainitud kasutajatele.",
"content_warning": "Pealkiri (valikuline)",
"content_type": {
"text/bbcode": "BBCode",
"text/markdown": "Markdown",
"text/html": "HTML",
"text/plain": "Lihttekst"
},
"attachments_sensitive": "Märgi manused sensitiivseks",
"account_not_locked_warning_link": "lukus",
"account_not_locked_warning": "Teie konto ei ole {0}. Kõik võivad Teid jälgida, et näha Teie ainult-jälgijatele postitusi.",
"new_status": "Postita uus staatus"
}, },
"registration": { "registration": {
"bio": "Bio", "bio": "Bio",
"email": "E-post", "email": "E-post",
"fullname": "Kuvatav nimi", "fullname": "Kuvatav nimi",
"password_confirm": "Parooli kinnitamine", "password_confirm": "Parooli kinnitamine",
"registration": "Registreerimine" "registration": "Registreerimine",
"validations": {
"password_confirmation_match": "peaks olema sama kui salasõna",
"password_confirmation_required": "ei saa jätta tühjaks",
"password_required": "ei saa jätta tühjaks",
"email_required": "ei saa jätta tühjaks",
"fullname_required": "ei saa jätta tühjaks",
"username_required": "ei saa jätta tühjaks"
},
"fullname_placeholder": "Näiteks Lain Iwakura",
"username_placeholder": "Näiteks lain",
"new_captcha": "Vajuta pildile, et saada uus captcha",
"captcha": "CAPTCHA",
"token": "Kutse võti"
}, },
"settings": { "settings": {
"attachments": "Manused", "attachments": "Manused",
@ -44,7 +122,7 @@
"current_avatar": "Sinu praegune profiilipilt", "current_avatar": "Sinu praegune profiilipilt",
"current_profile_banner": "Praegune profiilibänner", "current_profile_banner": "Praegune profiilibänner",
"filtering": "Sisu filtreerimine", "filtering": "Sisu filtreerimine",
"filtering_explanation": "Kõiki staatuseid, mis sisaldavad neid sõnu, ei kuvata. Üks sõna reale.", "filtering_explanation": "Kõiki staatuseid, mis sisaldavad neid sõnu, ei kuvata. Üks sõna reale",
"hide_attachments_in_convo": "Peida manused vastlustes", "hide_attachments_in_convo": "Peida manused vastlustes",
"hide_attachments_in_tl": "Peida manused ajajoonel", "hide_attachments_in_tl": "Peida manused ajajoonel",
"name": "Nimi", "name": "Nimi",
@ -58,7 +136,201 @@
"set_new_profile_banner": "Vali uus profiilibänner", "set_new_profile_banner": "Vali uus profiilibänner",
"settings": "Sätted", "settings": "Sätted",
"theme": "Teema", "theme": "Teema",
"user_settings": "Kasutaja sätted" "user_settings": "Kasutaja sätted",
"subject_line_noop": "Ära kopeeri",
"subject_line_mastodon": "Nagu mastodon: kopeeri nagu on",
"subject_line_email": "Nagu e-post: \"vs: pealkiri\"",
"subject_line_behavior": "Kopeeri pealkiri vastamisel",
"subject_input_always_show": "Alati kuva pealkirja välja",
"minimal_scopes_mode": "Peida postituse nähtavussätted",
"scope_copy": "Kopeeri nähtavussätted vastamisel (Privaatsed on alati kopeeritud)",
"security_tab": "Turvalisus",
"search_user_to_mute": "Otsi, keda soovid vaigistada",
"search_user_to_block": "Otsi, keda soovid blokeerida",
"saving_ok": "Sätted salvestatud",
"saving_err": "Sätete salvestamine ebaõnnestus",
"autohide_floating_post_button": "Automaatselt peida uue postituse nupp (mobiilil)",
"reply_visibility_self": "Näita ainult vastuseid, mis on suunatud mulle",
"reply_visibility_following": "Näita ainult vastuseid, mis on suunatud mulle või kasutajatele, keda jälgin",
"reply_visibility_all": "Näita kõiki vastuseid",
"replies_in_timeline": "Vastused ajajoonel",
"radii_help": "Liidese ümardamine (pikslites)",
"profile_tab": "Profiil",
"presets": "Salvestatud sätted",
"pause_on_unfocused": "Peata reaalajas voog kui leht pole fookuses",
"panelRadius": "Paneelid",
"revoke_token": "Keela",
"valid_until": "Kehtiv kuni",
"refresh_token": "Värskendustoken",
"token": "Token",
"oauth_tokens": "OAuth tokenid",
"show_moderator_badge": "Näita Moderaator silti mu profiilil",
"show_admin_badge": "Näita Admin silti mu profiilil",
"hide_followers_count_description": "Ära näita minu jälgijate arvu",
"hide_follows_count_description": "Ära näita minu jälgimiste arvu",
"hide_followers_description": "Ära näita minu jälgijaid",
"hide_follows_description": "Ära näita minu jälgimisi",
"no_mutes": "Vaigistusi pole",
"no_blocks": "Blokeeringuid pole",
"no_rich_text_description": "Muuda kõik postitused lihttekstiks",
"notification_visibility_emoji_reactions": "Reaktsioonid",
"notification_visibility_moves": "Kasutaja kolimised",
"notification_visibility_repeats": "Taaspostitused",
"notification_visibility_mentions": "Mainimised",
"notification_visibility_likes": "Lemmikud",
"notification_visibility_follows": "Jälgimised",
"notification_visibility": "Milliseid teateid kuvatakse",
"new_password": "Uus salasõna",
"new_email": "Uus e-post",
"use_contain_fit": "Näita eelvaadetes täis suuruses pilte",
"play_videos_in_modal": "Näita videoid eraldi raamis",
"mutes_tab": "Vaigistused",
"loop_video_silent_only": "Loop videod, millel pole heli (nt. Mastodoni \"gifid\")",
"loop_video": "Loop videod",
"lock_account_description": "Piira oma konto ainult lubatud jälgijatele",
"links": "Lingid",
"limited_availability": "Pole Teie veebilehitsejas saadaval",
"invalid_theme_imported": "Valitud fail ei ole Pleroma kujundus. Kujundusele muudatusi ei tehtud.",
"interfaceLanguage": "Liidese keel",
"interface": "Liides",
"instance_default_simple": "(vaikimisi)",
"instance_default": "(vaikimisi: {value})",
"checkboxRadius": "Märkeruudud",
"inputRadius": "Sisestuskastid",
"import_theme": "Lae sätted",
"import_followers_from_a_csv_file": "Impordi jälgimised csv failist",
"import_blocks_from_a_csv_file": "Impordi blokeeringud csv failist",
"hide_filtered_statuses": "Peida filtreeritud staatused",
"hide_user_stats": "Peida kasutaja statistika (nt. jälgijate arv)",
"hide_post_stats": "Peida postituse statistika (nt. lemmikute arv)",
"use_one_click_nsfw": "Ava NSFW manused ühe klikiga",
"preload_images": "Piltide eellaadimine",
"hide_isp": "Peida instantsipõhine paneel",
"max_thumbnails": "Maksimaalne lubatud eelvaadete arv postituste kohta",
"hide_muted_posts": "Peida vaigistatud kasutajate postitused",
"general": "Üldine",
"foreground": "Esiplaan",
"accent": "Rõhk",
"follows_imported": "Jälgimised imporditud! Nende töötlemine võtab natuke aega.",
"follow_import_error": "Jälgimiste importimisel tekkis viga",
"follow_import": "Impordi jälgimised",
"follow_export_button": "Ekspordi oma jälgimised csv failiks",
"follow_export": "Ekspordi jälgimised",
"export_theme": "Salvesta sätted",
"emoji_reactions_on_timeline": "Näita reaktsioone ajajoonel",
"pad_emoji": "Lisa emotikonidele tühikud ette ja järgi neid menüüst valides",
"avatar_size_instruction": "Profiilipildi soovitatud minimaalne suurus on 150x150 pikslit.",
"domain_mutes": "Domeenid",
"discoverable": "Luba selle konto ilmumine otsingutulemustes ning muudes teenustes",
"delete_account_instructions": "Konto kustutamise kinnitamiseks sisestage oma salasõna.",
"delete_account_error": "Teie konto kustutamisel tekkis viga. Kui see jätkub, palun võtke kontakti administraatoriga.",
"delete_account_description": "Jäädavalt kustuta oma andmed ja konto.",
"delete_account": "Kustuta konto",
"default_vis": "Vaikimisi nähtavus",
"data_import_export_tab": "Andmete import / eksport",
"current_password": "Praegune salasõna",
"confirm_new_password": "Kinnita uus salasõna",
"composing": "Koostamine",
"collapse_subject": "Peida postituste pealkirjad",
"changed_password": "Salasõna edukalt muudetud!",
"change_password_error": "Esines viga salasõna muutmisel.",
"change_password": "Muuda salasõna",
"changed_email": "E-post edukalt muudetud!",
"change_email_error": "Esines viga e-posti muutmisel.",
"change_email": "Muuda e-posti",
"cRed": "Punane (Tühista)",
"cOrange": "Oranž (Lisa lemmikuks)",
"cGreen": "Roheline (Taaspostita)",
"cBlue": "Sinine (Vasta, jälgi)",
"btnRadius": "Nupud",
"blocks_tab": "Blokeeringud",
"blocks_imported": "Blokeeringud imporditud! Nende töötlemine võtab natuke aega.",
"block_import_error": "Blokeeringute importimisel esines viga",
"block_import": "Blokeeringute import",
"block_export_button": "Ekspordi oma blokeeringud csv failiks",
"block_export": "Blokeeringute eksport",
"background": "Taust",
"avatarRadius": "Profiilipildid",
"avatarAltRadius": "Profiilipildid (Teated)",
"attachmentRadius": "Manused",
"allow_following_move": "Luba automaatjälgimine kui jälgitav konto kolib",
"mfa": {
"verify": {
"desc": "Et lubada kaheastmelist autentimist, sisestage kood oma äpist:"
},
"scan": {
"desc": "Kasutades oma kaheastmelise autentimise äppi, skännige see QR kood või sisestage tekstiline võti:",
"secret_code": "Võti",
"title": "Skänni"
},
"authentication_methods": "Autentimismeetodid",
"recovery_codes_warning": "Kirjutage need koodid üles ning hoidke need kindlas kohas. Kui Te kaotate ligipääsu oma kaheastmelise autentimise äppile ning nendele koodidele, ei ole Teil võimalik oma kontosse sisse logida.",
"waiting_a_recovery_codes": "Laen taastekoode…",
"recovery_codes": "Taastekoodid.",
"warning_of_generate_new_codes": "Kui Te loote uued taastekoodid, Teie vanad koodid ei tööta enam.",
"generate_new_recovery_codes": "Loo uued taastekoodid",
"title": "Kaheastmeline autentimine",
"confirm_and_enable": "Kinnita & luba OTP",
"wait_pre_setup_otp": "sean üles OTP",
"setup_otp": "Sea üles OTP",
"otp": "OTP"
},
"enter_current_password_to_confirm": "Sisetage isiku tõestamiseks oma salasõna",
"security": "Turvalisus",
"app_name": "Rakenduse nimi",
"style": {
"switcher": {
"help": {
"snapshot_present": "Kujunduse eelvaade on laetud, nii et kõik väärtused on üle kirjutatud. Te saate laadida ka kujunduse päris sisu.",
"older_version_imported": "Teie imporditud fail oli loodud vanemas versioonis.",
"future_version_imported": "Teie imporditud fail oli loodud uuemas versioonis.",
"v2_imported": "Teie imporditud fail oli vanema versiooni jaoks. Me üritame hoida ühilduvust, kuid ikkagi võib esineda erinevusi.",
"upgraded_from_v2": "PleromaFE-d uuendati, teie kujundus võib välja näha natuke erinev, kui mäletate."
},
"use_source": "Uus versioon",
"use_snapshot": "Vana versioon",
"keep_as_is": "Jäta nii, nagu on",
"load_theme": "Lae kujundus",
"clear_opacity": "Tühista läbipaistvus",
"clear_all": "Tühista kõik",
"reset": "Taasta algne",
"keep_fonts": "Jäta fondid",
"keep_roundness": "Jäta ümarus",
"keep_opacity": "Jäta läbipaistvus",
"keep_shadows": "Jäta varjud",
"keep_color": "Jäta värvid"
}
},
"enable_web_push_notifications": "Luba veebipõhised push-teated",
"notification_blocks": "Kasutaja blokeerimisel ei tule neilt enam teateid ning nendele teilt ka mitte.",
"notification_setting_privacy_option": "Peida saatja ning sisu push-teadetelt",
"notification_setting": "Saa teateid nendelt:",
"notifications": "Teated",
"notification_mutes": "Kui soovid mõnelt kasutajalt mitte teateid saada, kasuta vaigistust.",
"notification_setting_privacy": "Privaatsus",
"notification_setting_non_followers": "Kasutajatelt, kes sind ei jälgi",
"notification_setting_followers": "Kasutajatelt, kes jälgivad sind",
"notification_setting_non_follows": "Kasutajatelt, keda sa ei jälgi",
"notification_setting_follows": "Kasutajatelt, keda jälgid",
"notification_setting_filters": "Filtrid",
"greentext": "Meemi nooled",
"fun": "Naljad",
"values": {
"true": "jah",
"false": "ei"
},
"upload_a_photo": "Lae üles foto",
"type_domains_to_mute": "Trüki siia domeene, mida vaigistada",
"tooltipRadius": "Vihjed/hoiatused",
"theme_help_v2_1": "Te saate ka mõndade komponentide värvust ning läbipaistvust üle kirjutada vajutades ruudule. Kasuta \"Tühista kõik\" nuppu, et need tühistada.",
"theme_help": "Kasuta hex värvikoode (#rrggbb) oma kujunduse isikupärastamiseks.",
"text": "Tekst",
"useStreamingApiWarning": "(Pole soovituslik, eksperimentaalne, on teada, et jätab postitusi vahele)",
"useStreamingApi": "Saa postitusi ning teateid reaalajas",
"user_mutes": "Kasutajad",
"streaming": "Luba uute postituste automaatvoog kui oled lehekülje alguses",
"stop_gifs": "Mängi GIFid hiirega ületades",
"post_status_content_type": "Postituse sisutüüp"
}, },
"timeline": { "timeline": {
"conversation": "Vestlus", "conversation": "Vestlus",
@ -79,5 +351,111 @@
"muted": "Vaigistatud", "muted": "Vaigistatud",
"per_day": "päevas", "per_day": "päevas",
"statuses": "Staatuseid" "statuses": "Staatuseid"
},
"about": {
"mrf": {
"mrf_policies_desc": "MRF poliitikad mõjutavad selle instansi föderatsiooni käitumist. Järgmised poliitikad on lubatud:",
"simple": {
"media_nsfw_desc": "See instants määrab nendest instantsidest postituste meedia sensitiivseks:",
"media_nsfw": "Meedia määratakse sensitiivseks",
"media_removal_desc": "See instants eemaldab meedia postitustelt nendest instantsidest:",
"media_removal": "Meedia eemaldamine",
"ftl_removal_desc": "See instants eemaldab postitused nendelt instantsidest \"Kogu teatud võrgu\" ajajoonelt:",
"ftl_removal": "\"Kogu teatud võrgu\" ajajoonelt eemaldamine",
"quarantine_desc": "See instants saadab ainult avalikke postitusi järgmistele instantsidele:",
"quarantine": "Karantiini",
"reject_desc": "See instants ei luba sõnumeid nendest instantsidest:",
"reject": "Keela",
"accept_desc": "See instants lubab sõnumeid ainult nendest instantsidest:",
"accept": "Luba",
"simple_policies": "Instansi-omased poliitikad"
},
"mrf_policies": "Lubatud MRF poliitikad",
"keyword": {
"is_replaced_by": "→",
"replace": "Vaheta",
"reject": "Lükka tagasi",
"ftl_removal": "\"Kogu teatud võrgu\" ajajoonelt eemaldamine",
"keyword_policies": "Võtmesõna poliitikad"
},
"federation": "Föderatsioon"
},
"staff": "Personal"
},
"selectable_list": {
"select_all": "Vali kõik"
},
"remote_user_resolver": {
"error": "Ei leitud.",
"searching_for": "Otsin",
"remote_user_resolver": "Kaugkasutaja leidja"
},
"interactions": {
"load_older": "Laadi vanemad interaktsioonid",
"moves": "Kasutaja kolimised",
"follows": "Uued jälgimised",
"favs_repeats": "Taaspostitused ja lemmikud"
},
"emoji": {
"load_all": "Laen kõik {emojiAmount} emotikoni",
"load_all_hint": "Laadisin esimesed {saneAmount} emotikoni, kõike laadides võib esineda probleeme jõudlusega.",
"unicode": "Unicode emotikonid",
"custom": "Kohandatud emotikonid",
"add_emoji": "Lisa emotikon",
"search_emoji": "Otsi emotikone",
"keep_open": "Hoia valija lahti",
"emoji": "Emotikonid",
"stickers": "Kleepsud"
},
"polls": {
"not_enough_options": "Liiga vähe unikaalseid valikuid hääletuses",
"expired": "Hääletus lõppes {0} tagasi",
"expires_in": "Hääletus lõppeb {0}",
"expiry": "Hääletuse vanus",
"multiple_choices": "Mitu vastust",
"single_choice": "Üks vastus",
"type": "Hääletuse tüüp",
"vote": "Hääleta",
"votes": "häält",
"option": "Valik",
"add_option": "Lisa valik",
"add_poll": "Lisa küsitlus"
},
"media_modal": {
"next": "Järgmine",
"previous": "Eelmine"
},
"importer": {
"error": "Faili importimisel tekkis viga.",
"success": "Import õnnestus.",
"submit": "Esita"
},
"image_cropper": {
"cancel": "Tühista",
"save_without_cropping": "Salvesta muudatusteta",
"save": "Salvesta",
"crop_picture": "Modifitseeri pilti"
},
"features_panel": {
"who_to_follow": "Keda jälgida",
"title": "Featuurid",
"text_limit": "Tekstilimiit",
"scope_options": "Ulatuse valikud",
"media_proxy": "Meedia proksi",
"gopher": "Gopher",
"chat": "Vestlus"
},
"exporter": {
"processing": "Töötlemine, Teilt küsitakse varsti faili allalaadimist",
"export": "Ekspordi"
},
"domain_mute_card": {
"unmute_progress": "Eemaldan vaigistuse…",
"unmute": "Ära vaigista",
"mute_progress": "Vaigistan…",
"mute": "Vaigista"
},
"chat": {
"title": "Vestlus"
} }
} }

@ -31,7 +31,8 @@
"disable": "Désactiver", "disable": "Désactiver",
"enable": "Activer", "enable": "Activer",
"confirm": "Confirmer", "confirm": "Confirmer",
"verify": "Vérifier" "verify": "Vérifier",
"dismiss": "Rejeter"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Rogner l'image", "crop_picture": "Rogner l'image",
@ -167,7 +168,7 @@
"generate_new_recovery_codes": "Générer de nouveaux codes de récupération", "generate_new_recovery_codes": "Générer de nouveaux codes de récupération",
"warning_of_generate_new_codes": "Quand vous générez de nouveauc codes de récupération, vos anciens codes ne fonctionnerons plus.", "warning_of_generate_new_codes": "Quand vous générez de nouveauc codes de récupération, vos anciens codes ne fonctionnerons plus.",
"recovery_codes": "Codes de récupération.", "recovery_codes": "Codes de récupération.",
"waiting_a_recovery_codes": "Récéption des codes de récupération…", "waiting_a_recovery_codes": "Réception des codes de récupération…",
"recovery_codes_warning": "Écrivez les codes ou sauvez les quelquepart sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez vérouillé en dehors de votre compte.", "recovery_codes_warning": "Écrivez les codes ou sauvez les quelquepart sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez vérouillé en dehors de votre compte.",
"authentication_methods": "Methodes d'authentification", "authentication_methods": "Methodes d'authentification",
"scan": { "scan": {
@ -210,7 +211,7 @@
"data_import_export_tab": "Import / Export des Données", "data_import_export_tab": "Import / Export des Données",
"default_vis": "Visibilité par défaut", "default_vis": "Visibilité par défaut",
"delete_account": "Supprimer le compte", "delete_account": "Supprimer le compte",
"delete_account_description": "Supprimer définitivement votre compte et tous vos statuts.", "delete_account_description": "Supprimer définitivement vos données et désactiver votre compte.",
"delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateur⋅ice de cette instance.", "delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateur⋅ice de cette instance.",
"delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.", "delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.",
"avatar_size_instruction": "La taille minimale recommandée pour l'image de l'avatar est de 150x150 pixels.", "avatar_size_instruction": "La taille minimale recommandée pour l'image de l'avatar est de 150x150 pixels.",
@ -343,7 +344,13 @@
"upgraded_from_v2": "PleromaFE à été mis à jour, le thème peut être un peu différent que dans vos souvenirs.", "upgraded_from_v2": "PleromaFE à été mis à jour, le thème peut être un peu différent que dans vos souvenirs.",
"v2_imported": "Le fichier que vous avez importé vient d'un version antérieure. Nous essayons de maximizer la compatibilité mais il peu y avoir quelques incohérences.", "v2_imported": "Le fichier que vous avez importé vient d'un version antérieure. Nous essayons de maximizer la compatibilité mais il peu y avoir quelques incohérences.",
"future_version_imported": "Le fichier importé viens d'une version postérieure de PleromaFE.", "future_version_imported": "Le fichier importé viens d'une version postérieure de PleromaFE.",
"older_version_imported": "Le fichier importé viens d'une version antérieure de PleromaFE." "older_version_imported": "Le fichier importé viens d'une version antérieure de PleromaFE.",
"snapshot_source_mismatch": "Conflict de version : Probablement due à un retour arrière puis remise à jour de la version de PleromaFE, si vous avez charger le thème en utilisant une version antérieure vous voulez probablement utiliser la version antérieure, autrement utiliser la version postérieure.",
"migration_napshot_gone": "Pour une raison inconnue l'instantané est manquant, des parties peuvent rendre différentes que dans vos souvenirs.",
"migration_snapshot_ok": "Pour être sûr un instantanée du thème à été chargé. Vos pouvez essayer de charger ses données.",
"fe_downgraded": "Retour en arrière de la version de PleromaFE.",
"fe_upgraded": "Le moteur de thème PleromaFE à été mis à jour après un changement de version.",
"snapshot_missing": "Aucun instantané du thème à été trouvé dans le fichier, il peut y avoir un rendu différent à la vision originelle."
}, },
"keep_as_is": "Garder tel-quel", "keep_as_is": "Garder tel-quel",
"use_source": "Nouvelle version" "use_source": "Nouvelle version"
@ -392,7 +399,10 @@
"selectedPost": "Message sélectionné", "selectedPost": "Message sélectionné",
"selectedMenu": "Objet sélectionné du menu", "selectedMenu": "Objet sélectionné du menu",
"disabled": "Désactivé", "disabled": "Désactivé",
"tabs": "Onglets" "tabs": "Onglets",
"toggled": "(Dés)activé",
"highlight": "Éléments mis en valeur",
"popover": "Infobulles, menus"
}, },
"radii": { "radii": {
"_tab_label": "Rondeur" "_tab_label": "Rondeur"
@ -482,7 +492,9 @@
"useStreamingApi": "Recevoir les messages et notifications en temps réel", "useStreamingApi": "Recevoir les messages et notifications en temps réel",
"notification_setting_filters": "Filtres", "notification_setting_filters": "Filtres",
"notification_setting_privacy_option": "Masquer l'expéditeur et le contenu des notifications push", "notification_setting_privacy_option": "Masquer l'expéditeur et le contenu des notifications push",
"notification_setting_privacy": "Intimité" "notification_setting_privacy": "Intimité",
"hide_followers_count_description": "Masquer le nombre d'abonnés",
"accent": "Accent"
}, },
"timeline": { "timeline": {
"collapse": "Fermer", "collapse": "Fermer",
@ -666,10 +678,13 @@
"unicode": "émoji unicode", "unicode": "émoji unicode",
"load_all": "Charger tout les {emojiAmount} émojis", "load_all": "Charger tout les {emojiAmount} émojis",
"load_all_hint": "{saneAmount} émojis chargé, charger tout les émojis peuvent causer des problèmes de performances.", "load_all_hint": "{saneAmount} émojis chargé, charger tout les émojis peuvent causer des problèmes de performances.",
"stickers": "Stickers" "stickers": "Stickers",
"keep_open": "Garder le sélecteur ouvert"
}, },
"remote_user_resolver": { "remote_user_resolver": {
"error": "Non trouvé." "error": "Non trouvé.",
"searching_for": "Rechercher",
"remote_user_resolver": "Résolution de compte distant"
}, },
"time": { "time": {
"minutes_short": "{0}min", "minutes_short": "{0}min",

@ -35,14 +35,14 @@
"followed_you": "ti segue", "followed_you": "ti segue",
"notifications": "Notifiche", "notifications": "Notifiche",
"read": "Letto!", "read": "Letto!",
"broken_favorite": "Stato sconosciuto, lo sto cercando...", "broken_favorite": "Stato sconosciuto, lo sto cercando",
"favorited_you": "ha gradito il tuo messaggio", "favorited_you": "ha gradito il tuo messaggio",
"load_older": "Carica notifiche precedenti", "load_older": "Carica notifiche precedenti",
"repeated_you": "ha condiviso il tuo messaggio", "repeated_you": "ha condiviso il tuo messaggio",
"follow_request": "vuole seguirti", "follow_request": "vuole seguirti",
"no_more_notifications": "Fine delle notifiche", "no_more_notifications": "Fine delle notifiche",
"migrated_to": "è migrato verso", "migrated_to": "è migrato verso",
"reacted_with": "ha reagito con" "reacted_with": "ha reagito con {0}"
}, },
"settings": { "settings": {
"attachments": "Allegati", "attachments": "Allegati",
@ -57,7 +57,7 @@
"hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze", "hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze",
"name": "Nome", "name": "Nome",
"name_bio": "Nome ed introduzione", "name_bio": "Nome ed introduzione",
"nsfw_clickthrough": "Fai click per visualizzare gli allegati nascosti", "nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati",
"profile_background": "Sfondo della tua pagina", "profile_background": "Sfondo della tua pagina",
"profile_banner": "Stendardo del tuo profilo", "profile_banner": "Stendardo del tuo profilo",
"reply_link_preview": "Visualizza le risposte al passaggio del cursore", "reply_link_preview": "Visualizza le risposte al passaggio del cursore",
@ -138,12 +138,143 @@
"streaming": "Mostra automaticamente i nuovi messaggi quando sei in cima alla pagina", "streaming": "Mostra automaticamente i nuovi messaggi quando sei in cima alla pagina",
"text": "Testo", "text": "Testo",
"theme_help": "Usa codici colore esadecimali (#rrggbb) per personalizzare il tuo schema di colori.", "theme_help": "Usa codici colore esadecimali (#rrggbb) per personalizzare il tuo schema di colori.",
"tooltipRadius": "Descrizioni/avvisi", "tooltipRadius": "Suggerimenti/avvisi",
"values": { "values": {
"false": "no", "false": "no",
"true": "sì" "true": "sì"
},
"avatar_size_instruction": "La taglia minima per l'icona personale è 150x150 pixel.",
"domain_mutes": "Domini",
"discoverable": "Permetti la scoperta di questo profilo da servizi di ricerca ed altro",
"composing": "Composizione",
"changed_email": "Email cambiata con successo!",
"change_email_error": "C'è stato un problema nel cambiare la tua email.",
"change_email": "Cambia email",
"blocks_tab": "Bloccati",
"blocks_imported": "Blocchi importati! Saranno elaborati a breve.",
"block_import_error": "Errore nell'importazione",
"block_import": "Importa blocchi",
"block_export_button": "Esporta i tuoi blocchi in un file CSV",
"block_export": "Esporta blocchi",
"allow_following_move": "Consenti",
"mfa": {
"verify": {
"desc": "Per abilitare l'autenticazione bifattoriale, inserisci il codice fornito dalla tua applicazione:"
},
"scan": {
"secret_code": "Codice",
"desc": "Con la tua applicazione bifattoriale, acquisisci questo QR o inserisci il codice manualmente:",
"title": "Acquisisci"
},
"authentication_methods": "Metodi di accesso",
"recovery_codes_warning": "Appuntati i codici o salvali in un posto sicuro, altrimenti rischi di non rivederli mai più. Se perderai l'accesso sia alla tua applicazione bifattoriale che ai codici di recupero non potrai più accedere al tuo profilo.",
"waiting_a_recovery_codes": "Ricevo codici di recupero…",
"recovery_codes": "Codici di recupero.",
"warning_of_generate_new_codes": "Alla generazione di nuovi codici di recupero, quelli vecchi saranno disattivati.",
"generate_new_recovery_codes": "Genera nuovi codici di recupero",
"title": "Accesso bifattoriale",
"confirm_and_enable": "Conferma ed abilita OTP",
"wait_pre_setup_otp": "preimposto OTP",
"setup_otp": "Imposta OTP",
"otp": "OTP"
},
"enter_current_password_to_confirm": "Inserisci la tua password per identificarti",
"security": "Sicurezza",
"app_name": "Nome applicazione",
"style": {
"switcher": {
"help": {
"older_version_imported": "Il tema importato è stato creato per una versione precedente dell'interfaccia.",
"future_version_imported": "Il tema importato è stato creato per una versione più recente dell'interfaccia.",
"v2_imported": "Il tema importato è stato creato per una vecchia interfaccia. Non tutto potrebbe essere come prima.",
"upgraded_from_v2": "L'interfaccia è stata aggiornata, il tema potrebbe essere diverso da come lo intendevi.",
"migration_snapshot_ok": "Ho caricato l'anteprima del tema. Puoi provare a caricarne i contenuti.",
"fe_downgraded": "L'interfaccia è stata portata ad una versione precedente.",
"fe_upgraded": "Lo schema dei temi è stato aggiornato insieme all'interfaccia.",
"snapshot_missing": "Il tema non è provvisto di anteprima, quindi potrebbe essere diverso da come appare.",
"snapshot_present": "Tutti i valori sono sostituiti dall'anteprima del tema. Puoi invece caricare i suoi contenuti.",
"snapshot_source_mismatch": "Conflitto di versione: probabilmente l'interfaccia è stata portata ad una versione precedente e poi aggiornata di nuovo. Se hai modificato il tema con una versione precedente dell'interfaccia, usa la vecchia versione del tema, altrimenti puoi usare la nuova.",
"migration_napshot_gone": "Anteprima del tema non trovata, non tutto potrebbe essere come ricordi."
},
"use_source": "Nuova versione",
"use_snapshot": "Versione precedente",
"keep_as_is": "Mantieni tal quale",
"load_theme": "Carica tema",
"clear_opacity": "Rimuovi opacità",
"clear_all": "Azzera tutto",
"reset": "Reimposta",
"save_load_hint": "Le opzioni \"mantieni\" conservano le impostazioni correnti quando selezioni o carichi un tema, e le salvano quando ne esporti uno. Quando nessuna casella è selezionata, tutte le impostazioni correnti saranno salvate nel tema.",
"keep_fonts": "Mantieni font",
"keep_roundness": "Mantieni vertici",
"keep_opacity": "Mantieni opacità",
"keep_shadows": "Mantieni ombre",
"keep_color": "Mantieni colori"
},
"common": {
"opacity": "Opacità",
"color": "Colore"
} }
}, },
"enable_web_push_notifications": "Abilita notifiche web push",
"fun": "Divertimento",
"notification_mutes": "Per non ricevere notifiche da uno specifico utente, zittiscilo.",
"notification_setting_privacy_option": "Nascondi mittente e contenuti delle notifiche push",
"notification_setting_privacy": "Privacy",
"notification_setting_followers": "Utenti che ti seguono",
"notification_setting_non_followers": "Utenti che non ti seguono",
"notification_setting_non_follows": "Utenti che non segui",
"notification_setting_follows": "Utenti che segui",
"notification_setting": "Ricevi notifiche da:",
"notification_setting_filters": "Filtri",
"notifications": "Notifiche",
"greentext": "Frecce da meme",
"upload_a_photo": "Carica un'immagine",
"type_domains_to_mute": "Inserisci domini da zittire",
"theme_help_v2_2": "Le icone dietro alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se si usano delle trasparenze, questi indicatori mostrano il peggior caso possibile.",
"theme_help_v2_1": "Puoi anche forzare colore ed opacità di alcuni elementi selezionando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le forzature.",
"useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)",
"useStreamingApi": "Ricevi messaggi e notifiche in tempo reale",
"user_mutes": "Utenti",
"post_status_content_type": "Tipo di contenuto dei messaggi",
"subject_line_noop": "Non copiare",
"subject_line_mastodon": "Come in Mastodon: copia tal quale",
"subject_line_email": "Come nelle email: \"re: oggetto\"",
"subject_line_behavior": "Copia oggetto quando rispondi",
"subject_input_always_show": "Mostra sempre il campo Oggetto",
"minimal_scopes_mode": "Riduci opzioni di visibilità",
"scope_copy": "Risposte ereditano la visibilità (messaggi privati lo fanno sempre)",
"search_user_to_mute": "Cerca utente da zittire",
"search_user_to_block": "Cerca utente da bloccare",
"autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)",
"show_moderator_badge": "Mostra l'insegna di moderatore sulla mia pagina",
"show_admin_badge": "Mostra l'insegna di amministratore sulla mia pagina",
"hide_followers_count_description": "Non mostrare quanti seguaci ho",
"hide_follows_count_description": "Non mostrare quanti utenti seguo",
"hide_followers_description": "Non mostrare i miei seguaci",
"hide_follows_description": "Non mostrare chi seguo",
"no_mutes": "Nessun utente zittito",
"no_blocks": "Nessun utente bloccato",
"notification_visibility_emoji_reactions": "Reazioni",
"notification_visibility_moves": "Migrazioni utenti",
"new_email": "Nuova email",
"use_contain_fit": "Non ritagliare le anteprime degli allegati",
"play_videos_in_modal": "Riproduci video in un riquadro a sbalzo",
"mutes_tab": "Zittiti",
"interface": "Interfaccia",
"instance_default_simple": "(predefinito)",
"checkboxRadius": "Caselle di selezione",
"import_blocks_from_a_csv_file": "Importa blocchi da un file CSV",
"hide_filtered_statuses": "Nascondi messaggi filtrati",
"use_one_click_nsfw": "Apri media offuscati con un solo click",
"preload_images": "Precarica immagini",
"hide_isp": "Nascondi pannello della stanza",
"max_thumbnails": "Numero massimo di anteprime per messaggio",
"hide_muted_posts": "Nascondi messaggi degli utenti zittiti",
"accent": "Accento",
"emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze",
"pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore",
"notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più."
},
"timeline": { "timeline": {
"error_fetching": "Errore nell'aggiornamento", "error_fetching": "Errore nell'aggiornamento",
"load_older": "Carica messaggi più vecchi", "load_older": "Carica messaggi più vecchi",
@ -209,7 +340,10 @@
"account_not_locked_warning_link": "protetto", "account_not_locked_warning_link": "protetto",
"attachments_sensitive": "Nascondi gli allegati", "attachments_sensitive": "Nascondi gli allegati",
"content_type": { "content_type": {
"text/plain": "Testo normale" "text/plain": "Testo normale",
"text/bbcode": "BBCode",
"text/markdown": "Markdown",
"text/html": "HTML"
}, },
"content_warning": "Oggetto (facoltativo)", "content_warning": "Oggetto (facoltativo)",
"default": "Sono appena atterrato a Fiumicino.", "default": "Sono appena atterrato a Fiumicino.",
@ -220,7 +354,15 @@
"private": "Solo per seguaci - Visibile solo dai tuoi seguaci", "private": "Solo per seguaci - Visibile solo dai tuoi seguaci",
"public": "Pubblico - Visibile sulla sequenza pubblica", "public": "Pubblico - Visibile sulla sequenza pubblica",
"unlisted": "Non elencato - Non visibile sulla sequenza pubblica" "unlisted": "Non elencato - Non visibile sulla sequenza pubblica"
} },
"scope_notice": {
"unlisted": "Questo messaggio non sarà visibile sulla sequenza locale né su quella pubblica",
"private": "Questo messaggio sarà visibile solo ai tuoi seguaci",
"public": "Questo messaggio sarà visibile a tutti"
},
"direct_warning_to_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati all'inizio.",
"direct_warning_to_all": "Questo messaggio sarà visibile a tutti i menzionati.",
"new_status": "Nuovo messaggio"
}, },
"registration": { "registration": {
"bio": "Introduzione", "bio": "Introduzione",
@ -228,7 +370,20 @@
"fullname": "Nome visualizzato", "fullname": "Nome visualizzato",
"password_confirm": "Conferma password", "password_confirm": "Conferma password",
"registration": "Registrazione", "registration": "Registrazione",
"token": "Codice d'invito" "token": "Codice d'invito",
"validations": {
"password_confirmation_match": "dovrebbe essere uguale alla password",
"password_confirmation_required": "non può essere vuoto",
"password_required": "non può essere vuoto",
"email_required": "non può essere vuoto",
"fullname_required": "non può essere vuoto",
"username_required": "non può essere vuoto"
},
"bio_placeholder": "es.\nCiao, sono Lupo Lucio.\nSono un lupo fantastico che vive nel Fantabosco. Forse mi hai visto alla Melevisione.",
"fullname_placeholder": "es. Lupo Lucio",
"username_placeholder": "es. mister_wolf",
"new_captcha": "Clicca l'immagine per avere un altro captcha",
"captcha": "CAPTCHA"
}, },
"user_profile": { "user_profile": {
"timeline_title": "Sequenza dell'Utente" "timeline_title": "Sequenza dell'Utente"
@ -269,9 +424,9 @@
}, },
"domain_mute_card": { "domain_mute_card": {
"mute": "Zittisci", "mute": "Zittisci",
"mute_progress": "Zittisco...", "mute_progress": "Zittisco",
"unmute": "Ascolta", "unmute": "Ascolta",
"unmute_progress": "Procedo..." "unmute_progress": "Procedo"
}, },
"exporter": { "exporter": {
"export": "Esporta", "export": "Esporta",
@ -307,7 +462,10 @@
"not_enough_options": "Aggiungi altre risposte" "not_enough_options": "Aggiungi altre risposte"
}, },
"interactions": { "interactions": {
"favs_repeats": "Condivisi e preferiti" "favs_repeats": "Condivisi e preferiti",
"load_older": "Carica vecchie interazioni",
"moves": "Utenti migrati",
"follows": "Nuovi seguìti"
}, },
"emoji": { "emoji": {
"load_all": "Carico tutti i {emojiAmount} emoji", "load_all": "Carico tutti i {emojiAmount} emoji",
@ -319,5 +477,13 @@
"keep_open": "Tieni aperto il menù", "keep_open": "Tieni aperto il menù",
"emoji": "Emoji", "emoji": "Emoji",
"stickers": "Adesivi" "stickers": "Adesivi"
},
"selectable_list": {
"select_all": "Seleziona tutto"
},
"remote_user_resolver": {
"error": "Non trovato.",
"searching_for": "Cerco",
"remote_user_resolver": "Cerca utenti remoti"
} }
} }

@ -66,7 +66,7 @@
"interactions": "Interacties" "interactions": "Interacties"
}, },
"notifications": { "notifications": {
"broken_favorite": "Onbekende status, aan het zoeken...", "broken_favorite": "Onbekende status, aan het zoeken",
"favorited_you": "vond je status leuk", "favorited_you": "vond je status leuk",
"followed_you": "volgt jou", "followed_you": "volgt jou",
"load_older": "Laad oudere meldingen", "load_older": "Laad oudere meldingen",
@ -402,7 +402,7 @@
"title": "Twee-factor Authenticatie", "title": "Twee-factor Authenticatie",
"generate_new_recovery_codes": "Genereer nieuwe herstelcodes", "generate_new_recovery_codes": "Genereer nieuwe herstelcodes",
"recovery_codes": "Herstelcodes.", "recovery_codes": "Herstelcodes.",
"waiting_a_recovery_codes": "Backup codes ontvangen...", "waiting_a_recovery_codes": "Backup codes ontvangen",
"authentication_methods": "Authenticatie methodes", "authentication_methods": "Authenticatie methodes",
"scan": { "scan": {
"title": "Scannen", "title": "Scannen",
@ -526,11 +526,11 @@
}, },
"show_repeats": "Herhalingen tonen", "show_repeats": "Herhalingen tonen",
"hide_repeats": "Herhalingen verbergen", "hide_repeats": "Herhalingen verbergen",
"mute_progress": "Negeren...", "mute_progress": "Negeren",
"unmute_progress": "Negering opheffen...", "unmute_progress": "Negering opheffen",
"unmute": "Negering opheffen", "unmute": "Negering opheffen",
"block_progress": "Blokkeren...", "block_progress": "Blokkeren",
"unblock_progress": "Blokkade opheffen...", "unblock_progress": "Blokkade opheffen",
"unblock": "Blokkade opheffen", "unblock": "Blokkade opheffen",
"unsubscribe": "Abonnement opzeggen", "unsubscribe": "Abonnement opzeggen",
"subscribe": "Abonneren", "subscribe": "Abonneren",
@ -604,9 +604,9 @@
}, },
"domain_mute_card": { "domain_mute_card": {
"mute": "Negeren", "mute": "Negeren",
"mute_progress": "Negeren...", "mute_progress": "Negeren",
"unmute": "Negering opheffen", "unmute": "Negering opheffen",
"unmute_progress": "Negering wordt opgeheven..." "unmute_progress": "Negering wordt opgeheven"
}, },
"exporter": { "exporter": {
"export": "Exporteren", "export": "Exporteren",

@ -3,6 +3,16 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
const browserLocale = (window.navigator.language || 'en').split('-')[0] const browserLocale = (window.navigator.language || 'en').split('-')[0]
/* TODO this is a bit messy.
* We need to declare settings with their types and also deal with
* instance-default settings in some way, hopefully try to avoid copy-pasta
* in general.
*/
export const multiChoiceProperties = [
'postContentType',
'subjectLineBehavior'
]
export const defaultState = { export const defaultState = {
colors: {}, colors: {},
theme: undefined, theme: undefined,

@ -1,6 +1,8 @@
import { set, delete as del } from 'vue' import { set, delete as del } from 'vue'
const defaultState = { const defaultState = {
settingsModalState: 'hidden',
settingsModalLoaded: false,
settings: { settings: {
currentSaveStateNotice: null, currentSaveStateNotice: null,
noticeClearTimeout: null, noticeClearTimeout: null,
@ -35,6 +37,27 @@ const interfaceMod = {
}, },
setMobileLayout (state, value) { setMobileLayout (state, value) {
state.mobileLayout = value state.mobileLayout = value
},
closeSettingsModal (state) {
state.settingsModalState = 'hidden'
},
togglePeekSettingsModal (state) {
switch (state.settingsModalState) {
case 'minimized':
state.settingsModalState = 'visible'
return
case 'visible':
state.settingsModalState = 'minimized'
return
default:
throw new Error('Illegal minimization state of settings modal')
}
},
openSettingsModal (state) {
state.settingsModalState = 'visible'
if (!state.settingsModalLoaded) {
state.settingsModalLoaded = true
}
} }
}, },
actions: { actions: {
@ -49,6 +72,15 @@ const interfaceMod = {
}, },
setMobileLayout ({ commit }, value) { setMobileLayout ({ commit }, value) {
commit('setMobileLayout', value) commit('setMobileLayout', value)
},
closeSettingsModal ({ commit }) {
commit('closeSettingsModal')
},
openSettingsModal ({ commit }) {
commit('openSettingsModal')
},
togglePeekSettingsModal ({ commit }) {
commit('togglePeekSettingsModal')
} }
} }
} }

@ -0,0 +1,32 @@
import Vue from 'vue'
/* By default async components don't have any way to recover, if component is
* failed, it is failed forever. This helper tries to remedy that by recreating
* async component when retry is requested (by user). You need to emit the
* `resetAsyncComponent` event from child to reset the component. Generally,
* this should be done from error component but could be done from loading or
* actual target component itself if needs to be.
*/
function getResettableAsyncComponent (asyncComponent, options) {
const asyncComponentFactory = () => () => ({
component: asyncComponent(),
...options
})
const observe = Vue.observable({ c: asyncComponentFactory() })
return {
functional: true,
render (createElement, { data, children }) {
// emit event resetAsyncComponent to reloading
data.on = {}
data.on.resetAsyncComponent = () => {
observe.c = asyncComponentFactory()
// parent.$forceUpdate()
}
return createElement(observe.c, data, children)
}
}
}
export default getResettableAsyncComponent

@ -363,6 +363,18 @@
"css": "ok", "css": "ok",
"code": 59431, "code": 59431,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "4109c474ff99cad28fd5a2c38af2ec6f",
"css": "filter",
"code": 61616,
"src": "fontawesome"
},
{
"uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
"css": "download",
"code": 59429,
"src": "fontawesome"
} }
] ]
} }

@ -2327,6 +2327,7 @@ dateformat@^1.0.6:
de-indent@^1.0.2: de-indent@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9" version "2.6.9"
@ -7903,9 +7904,10 @@ vue-style-loader@^4.0.0, vue-style-loader@^4.0.1:
hash-sum "^1.0.2" hash-sum "^1.0.2"
loader-utils "^1.0.2" loader-utils "^1.0.2"
vue-template-compiler@^2.3.4: vue-template-compiler@^2.6.11:
version "2.5.21" version "2.6.11"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.21.tgz#a57ceb903177e8f643560a8d639a0f8db647054a" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
dependencies: dependencies:
de-indent "^1.0.2" de-indent "^1.0.2"
he "^1.1.0" he "^1.1.0"
@ -7914,9 +7916,10 @@ vue-template-es2015-compiler@^1.6.0:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
vue@^2.5.13: vue@^2.6.11:
version "2.5.21" version "2.6.11"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
vuelidate@^0.7.4: vuelidate@^0.7.4:
version "0.7.4" version "0.7.4"

Loading…
Cancel
Save