Merge branch 'develop' into features/favicons
This commit is contained in:
commit
8a9654b511
140 changed files with 7916 additions and 4330 deletions
15
src/App.js
15
src/App.js
|
@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
|||
import FeaturesPanel from './components/features_panel/features_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 SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
|
@ -29,6 +30,7 @@ export default {
|
|||
SideDrawer,
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
SettingsModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal
|
||||
},
|
||||
|
@ -45,7 +47,8 @@ export default {
|
|||
}),
|
||||
created () {
|
||||
// Load the locale from the storage
|
||||
this.$i18n.locale = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
destroyed () {
|
||||
|
@ -99,7 +102,12 @@ export default {
|
|||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
isMobileLayout () { return this.$store.state.interface.mobileLayout },
|
||||
privateMode () { return this.$store.state.instance.private }
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
sidebarAlign () {
|
||||
return {
|
||||
'order': this.$store.state.instance.sidebarRight ? 99 : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scrollToTop () {
|
||||
|
@ -112,6 +120,9 @@ export default {
|
|||
onSearchBarToggled (hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
updateMobileState () {
|
||||
const mobileLayout = windowWidth() <= 800
|
||||
const changed = mobileLayout !== this.isMobileLayout
|
||||
|
|
47
src/App.scss
47
src/App.scss
|
@ -566,7 +566,7 @@ main-router {
|
|||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
margin-left: .25em;
|
||||
margin-left: .5em;
|
||||
min-width: 1px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
@ -860,51 +860,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 {
|
||||
display: flex;
|
||||
.option-list {
|
||||
|
|
13
src/App.vue
13
src/App.vue
|
@ -46,15 +46,16 @@
|
|||
@toggled="onSearchBarToggled"
|
||||
@click.stop.native
|
||||
/>
|
||||
<router-link
|
||||
<a
|
||||
href="#"
|
||||
class="mobile-hidden"
|
||||
:to="{ name: 'settings'}"
|
||||
@click.stop="openSettingsModal"
|
||||
>
|
||||
<i
|
||||
class="button-icon icon-cog nav-icon"
|
||||
:title="$t('nav.preferences')"
|
||||
/>
|
||||
</router-link>
|
||||
</a>
|
||||
<a
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
href="/pleroma/admin/#/login-pleroma"
|
||||
|
@ -80,7 +81,10 @@
|
|||
id="content"
|
||||
class="container underlay"
|
||||
>
|
||||
<div class="sidebar-flexer mobile-hidden">
|
||||
<div
|
||||
class="sidebar-flexer mobile-hidden"
|
||||
:style="sidebarAlign"
|
||||
>
|
||||
<div class="sidebar-bounds">
|
||||
<div class="sidebar-scroller">
|
||||
<div class="sidebar">
|
||||
|
@ -122,6 +126,7 @@
|
|||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<SettingsModal />
|
||||
<portal-target name="modal" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -108,9 +108,9 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('noAttachmentLinks')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
}
|
||||
|
@ -241,6 +241,9 @@ const getNodeInfo = async ({ store }) => {
|
|||
: federation.enabled
|
||||
})
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
|
@ -304,6 +307,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
getNodeInfo({ store })
|
||||
])
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: routes(store),
|
||||
|
|
|
@ -7,10 +7,8 @@ import Interactions from 'components/interactions/interactions.vue'
|
|||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import Settings from 'components/settings/settings.vue'
|
||||
import Registration from 'components/registration/registration.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 OAuthCallback from 'components/oauth_callback/oauth_callback.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: 'interactions', path: '/users/:username/interactions', component: Interactions, 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: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
|
|
|
@ -3,7 +3,7 @@ import Popover from '../popover/popover.vue'
|
|||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
'user'
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return { }
|
||||
|
|
|
@ -3,22 +3,23 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="account-tools-popover"
|
||||
>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="user.following">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
v-if="user.showing_reblogs"
|
||||
v-if="relationship.showing_reblogs"
|
||||
class="btn btn-default dropdown-item"
|
||||
@click="hideRepeats"
|
||||
>
|
||||
{{ $t('user_card.hide_repeats') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!user.showing_reblogs"
|
||||
v-if="!relationship.showing_reblogs"
|
||||
class="btn btn-default dropdown-item"
|
||||
@click="showRepeats"
|
||||
>
|
||||
|
@ -30,7 +31,7 @@
|
|||
/>
|
||||
</template>
|
||||
<button
|
||||
v-if="user.statusnet_blocking"
|
||||
v-if="relationship.blocking"
|
||||
class="btn btn-default btn-block dropdown-item"
|
||||
@click="unblockUser"
|
||||
>
|
||||
|
|
|
@ -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>
|
|
@ -12,7 +12,7 @@
|
|||
class="basic-user-card-expanded-content"
|
||||
>
|
||||
<UserCard
|
||||
:user="user"
|
||||
:user-id="user.id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
|
|
|
@ -11,8 +11,11 @@ const BlockCard = {
|
|||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked () {
|
||||
return this.user.statusnet_blocking
|
||||
return this.relationship.blocking
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -5,9 +5,20 @@ const DomainMuteCard = {
|
|||
components: {
|
||||
ProgressButton
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
muted () {
|
||||
return this.user.domainMutes.includes(this.domain)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
unmuteDomain () {
|
||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||
},
|
||||
muteDomain () {
|
||||
return this.$store.dispatch('muteDomain', this.domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
{{ domain }}
|
||||
</div>
|
||||
<ProgressButton
|
||||
v-if="muted"
|
||||
:click="unmuteDomain"
|
||||
class="btn btn-default"
|
||||
>
|
||||
|
@ -12,6 +13,16 @@
|
|||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-else
|
||||
:click="muteDomain"
|
||||
class="btn btn-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -34,5 +45,9 @@
|
|||
button {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.autosuggest-results & {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,17 +29,29 @@ export default data => input => {
|
|||
export const suggestEmoji = emojis => input => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return emojis
|
||||
.filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
|
||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
||||
// Make custom emojis a priority
|
||||
aScore += a.imageUrl ? 10 : 0
|
||||
bScore += b.imageUrl ? 10 : 0
|
||||
// An exact match always wins
|
||||
aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
|
||||
|
||||
// Sort alphabetically
|
||||
const alphabetically = a.displayText > b.displayText ? 1 : -1
|
||||
// Prioritize custom emoji a lot
|
||||
aScore += a.imageUrl ? 100 : 0
|
||||
bScore += b.imageUrl ? 100 : 0
|
||||
|
||||
// Prioritize prefix matches somewhat
|
||||
aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
|
||||
|
||||
// Sort by length
|
||||
aScore -= a.displayText.length
|
||||
bScore -= b.displayText.length
|
||||
|
||||
// Break ties alphabetically
|
||||
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
|
||||
|
||||
return bScore - aScore + alphabetically
|
||||
})
|
||||
|
|
|
@ -29,6 +29,11 @@ const ExtraButtons = {
|
|||
this.$store.dispatch('unmuteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
copyLink () {
|
||||
navigator.clipboard.writeText(this.statusLink)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -46,6 +51,9 @@ const ExtraButtons = {
|
|||
},
|
||||
canMute () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
statusLink () {
|
||||
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<Popover
|
||||
v-if="canDelete || canMute || canPin"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
:bound-to="{ x: 'container' }"
|
||||
>
|
||||
<div slot="content">
|
||||
<div
|
||||
slot="content"
|
||||
slot-scope="{close}"
|
||||
>
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -23,28 +26,35 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="pinStatus"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.pinned && canPin"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unpinStatus"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="deleteStatus"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="copyLink"
|
||||
@click="close"
|
||||
>
|
||||
<i class="icon-share" /><span>{{ $t("status.copy_link") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
export default {
|
||||
props: ['user', 'labelFollowing', 'buttonClass'],
|
||||
props: ['relationship', 'labelFollowing', 'buttonClass'],
|
||||
data () {
|
||||
return {
|
||||
inProgress: false
|
||||
|
@ -8,12 +8,12 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
isPressed () {
|
||||
return this.inProgress || this.user.following
|
||||
return this.inProgress || this.relationship.following
|
||||
},
|
||||
title () {
|
||||
if (this.inProgress || this.user.following) {
|
||||
if (this.inProgress || this.relationship.following) {
|
||||
return this.$t('user_card.follow_unfollow')
|
||||
} else if (this.user.requested) {
|
||||
} else if (this.relationship.requested) {
|
||||
return this.$t('user_card.follow_again')
|
||||
} else {
|
||||
return this.$t('user_card.follow')
|
||||
|
@ -22,9 +22,9 @@ export default {
|
|||
label () {
|
||||
if (this.inProgress) {
|
||||
return this.$t('user_card.follow_progress')
|
||||
} else if (this.user.following) {
|
||||
} else if (this.relationship.following) {
|
||||
return this.labelFollowing || this.$t('user_card.following')
|
||||
} else if (this.user.requested) {
|
||||
} else if (this.relationship.requested) {
|
||||
return this.$t('user_card.follow_sent')
|
||||
} else {
|
||||
return this.$t('user_card.follow')
|
||||
|
@ -33,20 +33,20 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.user.following ? this.unfollow() : this.follow()
|
||||
this.relationship.following ? this.unfollow() : this.follow()
|
||||
},
|
||||
follow () {
|
||||
this.inProgress = true
|
||||
requestFollow(this.user, this.$store).then(() => {
|
||||
requestFollow(this.relationship.id, this.$store).then(() => {
|
||||
this.inProgress = false
|
||||
})
|
||||
},
|
||||
unfollow () {
|
||||
const store = this.$store
|
||||
this.inProgress = true
|
||||
requestUnfollow(this.user, store).then(() => {
|
||||
requestUnfollow(this.relationship.id, store).then(() => {
|
||||
this.inProgress = false
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ const FollowCard = {
|
|||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
<basic-user-card :user="user">
|
||||
<div class="follow-card-content-container">
|
||||
<span
|
||||
v-if="!noFollowsYou && user.follows_you"
|
||||
v-if="isMe || (!noFollowsYou && relationship.followed_by)"
|
||||
class="faint"
|
||||
>
|
||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
<template v-if="!loggedIn">
|
||||
<div
|
||||
v-if="!user.following"
|
||||
v-if="!relationship.following"
|
||||
class="follow-card-follow-button"
|
||||
>
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-else-if="!isMe">
|
||||
<FollowButton
|
||||
:user="user"
|
||||
class="follow-card-follow-button"
|
||||
:relationship="relationship"
|
||||
:label-following="$t('user_card.follow_unfollow')"
|
||||
class="follow-card-follow-button"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
|
||||
|
||||
const FollowRequestCard = {
|
||||
props: ['user'],
|
||||
|
@ -6,13 +7,32 @@ const FollowRequestCard = {
|
|||
BasicUserCard
|
||||
},
|
||||
methods: {
|
||||
findFollowRequestNotificationId () {
|
||||
const notif = notificationsFromStore(this.$store).find(
|
||||
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
|
||||
)
|
||||
return notif && notif.id
|
||||
},
|
||||
approveUser () {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
||||
this.$store.dispatch('updateNotification', {
|
||||
id: notifId,
|
||||
updater: notification => {
|
||||
notification.type = 'follow'
|
||||
}
|
||||
})
|
||||
},
|
||||
denyUser () {
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
video,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ import _ from 'lodash'
|
|||
export default {
|
||||
computed: {
|
||||
languageCodes () {
|
||||
return Object.keys(languagesObject)
|
||||
return languagesObject.languages
|
||||
},
|
||||
|
||||
languageNames () {
|
||||
|
@ -43,7 +43,6 @@ export default {
|
|||
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
|
||||
set: function (val) {
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
this.$i18n.locale = val
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -84,10 +84,12 @@ const MediaModal = {
|
|||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('popstate', this.hide)
|
||||
document.addEventListener('keyup', this.handleKeyupEvent)
|
||||
document.addEventListener('keydown', this.handleKeydownEvent)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('popstate', this.hide)
|
||||
document.removeEventListener('keyup', this.handleKeyupEvent)
|
||||
document.removeEventListener('keydown', this.handleKeydownEvent)
|
||||
}
|
||||
|
|
|
@ -5,10 +5,15 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
|||
const mediaUpload = {
|
||||
data () {
|
||||
return {
|
||||
uploading: false,
|
||||
uploadCount: 0,
|
||||
uploadReady: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uploading () {
|
||||
return this.uploadCount > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
uploadFile (file) {
|
||||
const self = this
|
||||
|
@ -23,29 +28,21 @@ const mediaUpload = {
|
|||
formData.append('file', file)
|
||||
|
||||
self.$emit('uploading')
|
||||
self.uploading = true
|
||||
self.uploadCount++
|
||||
|
||||
statusPosterService.uploadMedia({ store, formData })
|
||||
.then((fileData) => {
|
||||
self.$emit('uploaded', fileData)
|
||||
self.uploading = false
|
||||
self.decreaseUploadCount()
|
||||
}, (error) => { // eslint-disable-line handle-callback-err
|
||||
self.$emit('upload-failed', 'default')
|
||||
self.uploading = false
|
||||
self.decreaseUploadCount()
|
||||
})
|
||||
},
|
||||
fileDrop (e) {
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
e.preventDefault() // allow dropping text like before
|
||||
this.uploadFile(e.dataTransfer.files[0])
|
||||
}
|
||||
},
|
||||
fileDrag (e) {
|
||||
let types = e.dataTransfer.types
|
||||
if (types.contains('Files')) {
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
} else {
|
||||
e.dataTransfer.dropEffect = 'none'
|
||||
decreaseUploadCount () {
|
||||
this.uploadCount--
|
||||
if (this.uploadCount === 0) {
|
||||
this.$emit('all-uploaded')
|
||||
}
|
||||
},
|
||||
clearFile () {
|
||||
|
@ -54,11 +51,13 @@ const mediaUpload = {
|
|||
this.uploadReady = true
|
||||
})
|
||||
},
|
||||
change ({ target }) {
|
||||
for (var i = 0; i < target.files.length; i++) {
|
||||
let file = target.files[i]
|
||||
multiUpload (files) {
|
||||
for (const file of files) {
|
||||
this.uploadFile(file)
|
||||
}
|
||||
},
|
||||
change ({ target }) {
|
||||
this.multiUpload(target.files)
|
||||
}
|
||||
},
|
||||
props: [
|
||||
|
@ -67,7 +66,7 @@ const mediaUpload = {
|
|||
watch: {
|
||||
'dropFiles': function (fileInfos) {
|
||||
if (!this.uploading) {
|
||||
this.uploadFile(fileInfos[0])
|
||||
this.multiUpload(fileInfos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
class="media-upload"
|
||||
@drop.prevent
|
||||
@dragover.prevent="fileDrag"
|
||||
@drop="fileDrop"
|
||||
>
|
||||
<div class="media-upload">
|
||||
<label
|
||||
class="label"
|
||||
:title="$t('tool_tip.media_upload')"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="isOpen"
|
||||
v-body-scroll-lock="isOpen"
|
||||
v-body-scroll-lock="isOpen && !noBackground"
|
||||
class="modal-view"
|
||||
:class="classes"
|
||||
@click.self="$emit('backdropClicked')"
|
||||
>
|
||||
<slot />
|
||||
|
@ -15,6 +16,18 @@ export default {
|
|||
isOpen: {
|
||||
type: Boolean,
|
||||
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;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
pointer-events: none;
|
||||
animation-duration: 0.2s;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation-name: modal-background-fadein;
|
||||
opacity: 0;
|
||||
|
||||
body:not(.scroll-locked) & {
|
||||
opacity: 0;
|
||||
> * {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
&.modal-background {
|
||||
pointer-events: initial;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,11 @@ const MuteCard = {
|
|||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
muted () {
|
||||
return this.user.muted
|
||||
return this.relationship.muting
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -21,13 +24,13 @@ const MuteCard = {
|
|||
methods: {
|
||||
unmuteUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unmuteUser', this.user.id).then(() => {
|
||||
this.$store.dispatch('unmuteUser', this.userId).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
},
|
||||
muteUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('muteUser', this.user.id).then(() => {
|
||||
this.$store.dispatch('muteUser', this.userId).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import StatusContent from '../status_content/status_content.vue'
|
||||
import Status from '../status/status.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
@ -15,10 +17,11 @@ const Notification = {
|
|||
},
|
||||
props: [ 'notification' ],
|
||||
components: {
|
||||
Status,
|
||||
StatusContent,
|
||||
UserAvatar,
|
||||
UserCard,
|
||||
Timeago
|
||||
Timeago,
|
||||
Status
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
@ -32,6 +35,24 @@ const Notification = {
|
|||
},
|
||||
toggleMute () {
|
||||
this.unmuted = !this.unmuted
|
||||
},
|
||||
approveUser () {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
|
||||
this.$store.dispatch('updateNotification', {
|
||||
id: this.notification.id,
|
||||
updater: notification => {
|
||||
notification.type = 'follow'
|
||||
}
|
||||
})
|
||||
},
|
||||
denyUser () {
|
||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -56,7 +77,10 @@ const Notification = {
|
|||
return this.generateUserProfileLink(this.targetUser)
|
||||
},
|
||||
needMute () {
|
||||
return this.user.muted
|
||||
return this.$store.getters.relationship(this.user.id).muting
|
||||
},
|
||||
isStatusNotification () {
|
||||
return isStatusNotification(this.notification.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,14 +40,14 @@
|
|||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="getUser(notification)"
|
||||
:user-id="getUser(notification).id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
<bdi
|
||||
v-if="!!notification.from_profile.name_html"
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name"
|
||||
|
@ -74,6 +74,10 @@
|
|||
<i class="fa icon-user-plus lit" />
|
||||
<small>{{ $t('notifications.followed_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'follow_request'">
|
||||
<i class="fa icon-user lit" />
|
||||
<small>{{ $t('notifications.follow_request') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'move'">
|
||||
<i class="fa icon-arrow-curved lit" />
|
||||
<small>{{ $t('notifications.migrated_to') }}</small>
|
||||
|
@ -87,18 +91,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'follow' || notification.type === 'move'"
|
||||
class="timeago"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-if="isStatusNotification"
|
||||
class="timeago"
|
||||
>
|
||||
<router-link
|
||||
|
@ -112,6 +105,17 @@
|
|||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="timeago"
|
||||
>
|
||||
<span class="faint">
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
v-if="needMute"
|
||||
href="#"
|
||||
|
@ -119,12 +123,30 @@
|
|||
><i class="button-icon icon-eye-off" /></a>
|
||||
</span>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
|
||||
class="follow-text"
|
||||
>
|
||||
<router-link :to="userProfileLink">
|
||||
<router-link
|
||||
:to="userProfileLink"
|
||||
class="follow-name"
|
||||
>
|
||||
@{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
<div
|
||||
v-if="notification.type === 'follow_request'"
|
||||
style="white-space: nowrap;"
|
||||
>
|
||||
<i
|
||||
class="icon-ok button-icon follow-request-accept"
|
||||
:title="$t('tool_tip.accept_follow_request')"
|
||||
@click="approveUser()"
|
||||
/>
|
||||
<i
|
||||
class="icon-cancel button-icon follow-request-reject"
|
||||
:title="$t('tool_tip.reject_follow_request')"
|
||||
@click="denyUser()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="notification.type === 'move'"
|
||||
|
@ -135,11 +157,9 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<template v-else>
|
||||
<status
|
||||
<status-content
|
||||
class="faint"
|
||||
:compact="true"
|
||||
:statusoid="notification.action"
|
||||
:no-heading="true"
|
||||
:status="notification.action"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
||||
&:hover .animated.avatar {
|
||||
canvas {
|
||||
|
@ -46,41 +48,62 @@
|
|||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: .25em .6em;
|
||||
}
|
||||
|
||||
.non-mention {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0.6em;
|
||||
min-width: 0;
|
||||
|
||||
.avatar-container {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.status-el {
|
||||
.status {
|
||||
padding: 0.25em 0;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
a {
|
||||
color: var(--faintLink);
|
||||
}
|
||||
.status-content a {
|
||||
color: var(--postFaintLink);
|
||||
}
|
||||
|
||||
.status-body {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
a {
|
||||
color: var(--faintLink);
|
||||
}
|
||||
padding: 0;
|
||||
.media-body {
|
||||
margin: 0;
|
||||
.status-content a {
|
||||
color: var(--postFaintLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follow-request-accept {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
.follow-request-reject {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $fallback--cRed;
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.follow-text, .move-text {
|
||||
padding: 0.5em 0;
|
||||
overflow-wrap: break-word;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.follow-name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.status-el {
|
||||
|
@ -142,6 +165,11 @@
|
|||
color: var(--cGreen, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.icon-user.lit {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
}
|
||||
|
||||
.icon-user-plus.lit {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
|
|
29
src/components/panel_loading/panel_loading.vue
Normal file
29
src/components/panel_loading/panel_loading.vue
Normal file
|
@ -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>
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
const Popover = {
|
||||
name: 'Popover',
|
||||
props: {
|
||||
|
@ -10,6 +9,9 @@ const Popover = {
|
|||
// 'container' for using offsetParent as boundaries for either axis
|
||||
// or 'viewport'
|
||||
boundTo: Object,
|
||||
// Takes a selector to use as a replacement for the parent container
|
||||
// for getting boundaries for x an y axis
|
||||
boundToSelector: String,
|
||||
// Takes a top/bottom/left/right object, how much space to leave
|
||||
// between boundary and popover element
|
||||
margin: Object,
|
||||
|
@ -27,6 +29,10 @@ const Popover = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
containerBoundingClientRect () {
|
||||
const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
|
||||
return container.getBoundingClientRect()
|
||||
},
|
||||
updateStyles () {
|
||||
if (this.hidden) {
|
||||
this.styles = {
|
||||
|
@ -45,7 +51,8 @@ const Popover = {
|
|||
// Minor optimization, don't call a slow reflow call if we don't have to
|
||||
const parentBounds = this.boundTo &&
|
||||
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
||||
this.$el.offsetParent.getBoundingClientRect()
|
||||
this.containerBoundingClientRect()
|
||||
|
||||
const margin = this.margin || {}
|
||||
|
||||
// What are the screen bounds for the popover? Viewport vs container
|
||||
|
|
|
@ -82,7 +82,9 @@ const PostStatusForm = {
|
|||
contentType
|
||||
},
|
||||
caret: 0,
|
||||
pollFormVisible: false
|
||||
pollFormVisible: false,
|
||||
showDropIcon: 'hide',
|
||||
dropStopTimeout: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -102,7 +104,7 @@ const PostStatusForm = {
|
|||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
|
||||
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
|
@ -218,7 +220,6 @@ const PostStatusForm = {
|
|||
},
|
||||
addMediaFile (fileInfo) {
|
||||
this.newStatus.files.push(fileInfo)
|
||||
this.enableSubmit()
|
||||
},
|
||||
removeMediaFile (fileInfo) {
|
||||
let index = this.newStatus.files.indexOf(fileInfo)
|
||||
|
@ -227,7 +228,6 @@ const PostStatusForm = {
|
|||
uploadFailed (errString, templateArgs) {
|
||||
templateArgs = templateArgs || {}
|
||||
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
|
||||
this.enableSubmit()
|
||||
},
|
||||
disableSubmit () {
|
||||
this.submitDisabled = true
|
||||
|
@ -250,13 +250,27 @@ const PostStatusForm = {
|
|||
}
|
||||
},
|
||||
fileDrop (e) {
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
e.preventDefault() // allow dropping text like before
|
||||
this.dropFiles = e.dataTransfer.files
|
||||
clearTimeout(this.dropStopTimeout)
|
||||
this.showDropIcon = 'hide'
|
||||
}
|
||||
},
|
||||
fileDragStop (e) {
|
||||
// The false-setting is done with delay because just using leave-events
|
||||
// directly caused unwanted flickering, this is not perfect either but
|
||||
// much less noticable.
|
||||
clearTimeout(this.dropStopTimeout)
|
||||
this.showDropIcon = 'fade'
|
||||
this.dropStopTimeout = setTimeout(() => (this.showDropIcon = 'hide'), 500)
|
||||
},
|
||||
fileDrag (e) {
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
clearTimeout(this.dropStopTimeout)
|
||||
this.showDropIcon = 'show'
|
||||
}
|
||||
},
|
||||
onEmojiInputInput (e) {
|
||||
this.$nextTick(() => {
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
<form
|
||||
autocomplete="off"
|
||||
@submit.prevent="postStatus(newStatus)"
|
||||
@dragover.prevent="fileDrag"
|
||||
>
|
||||
<div
|
||||
v-show="showDropIcon !== 'hide'"
|
||||
:style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }"
|
||||
class="drop-indicator icon-upload"
|
||||
@dragleave="fileDragStop"
|
||||
@drop.stop="fileDrop"
|
||||
/>
|
||||
<div class="form-group">
|
||||
<i18n
|
||||
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
|
||||
|
@ -73,6 +81,7 @@
|
|||
v-model="newStatus.spoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
:disabled="posting"
|
||||
class="form-post-subject"
|
||||
>
|
||||
</EmojiInput>
|
||||
|
@ -96,9 +105,7 @@
|
|||
:disabled="posting"
|
||||
class="form-post-body"
|
||||
@keydown.meta.enter="postStatus(newStatus)"
|
||||
@keyup.ctrl.enter="postStatus(newStatus)"
|
||||
@drop="fileDrop"
|
||||
@dragover.prevent="fileDrag"
|
||||
@keydown.ctrl.enter="postStatus(newStatus)"
|
||||
@input="resize"
|
||||
@compositionupdate="resize"
|
||||
@paste="paste"
|
||||
|
@ -172,6 +179,7 @@
|
|||
@uploading="disableSubmit"
|
||||
@uploaded="addMediaFile"
|
||||
@upload-failed="uploadFailed"
|
||||
@all-uploaded="enableSubmit"
|
||||
/>
|
||||
<div
|
||||
class="emoji-icon"
|
||||
|
@ -446,7 +454,8 @@
|
|||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.6em;
|
||||
margin: 0.6em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
|
@ -504,5 +513,35 @@
|
|||
cursor: pointer;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.6; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 0.6; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.drop-indicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
border: 2px dashed $fallback--text;
|
||||
border: 2px dashed var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue'
|
|||
import { mapGetters } from 'vuex'
|
||||
|
||||
const ReactButton = {
|
||||
props: ['status', 'loggedIn'],
|
||||
props: ['status'],
|
||||
data () {
|
||||
return {
|
||||
filterWord: ''
|
||||
|
@ -24,7 +24,7 @@ const ReactButton = {
|
|||
},
|
||||
computed: {
|
||||
commonEmojis () {
|
||||
return ['❤️', '😠', '👀', '😂', '🔥']
|
||||
return ['👍', '😠', '👀', '😂', '🔥']
|
||||
},
|
||||
emojis () {
|
||||
if (this.filterWord !== '') {
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<i
|
||||
v-if="loggedIn"
|
||||
slot="trigger"
|
||||
class="icon-smile button-icon add-reaction-button"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { validationMixin } from 'vuelidate'
|
||||
import { required, sameAs } from 'vuelidate/lib/validators'
|
||||
import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
|
||||
const registration = {
|
||||
|
@ -14,15 +14,17 @@ const registration = {
|
|||
},
|
||||
captcha: {}
|
||||
}),
|
||||
validations: {
|
||||
user: {
|
||||
email: { required },
|
||||
username: { required },
|
||||
fullname: { required },
|
||||
password: { required },
|
||||
confirm: {
|
||||
required,
|
||||
sameAsPassword: sameAs('password')
|
||||
validations () {
|
||||
return {
|
||||
user: {
|
||||
email: { required: requiredIf(() => this.accountActivationRequired) },
|
||||
username: { required },
|
||||
fullname: { required },
|
||||
password: { required },
|
||||
confirm: {
|
||||
required,
|
||||
sameAsPassword: sameAs('password')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -43,7 +45,8 @@ const registration = {
|
|||
signedIn: (state) => !!state.users.currentUser,
|
||||
isPending: (state) => state.users.signUpPending,
|
||||
serverValidationErrors: (state) => state.users.signUpErrors,
|
||||
termsOfService: (state) => state.instance.tos
|
||||
termsOfService: (state) => state.instance.tos,
|
||||
accountActivationRequired: (state) => state.instance.accountActivationRequired
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -187,6 +187,9 @@
|
|||
class="form-control"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -1,128 +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/pleroma/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/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
|
||||
}
|
||||
},
|
||||
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.$store.getters.mergedConfig.muteWords.join('\n') },
|
||||
set (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
|
42
src/components/settings_modal/settings_modal.js
Normal file
42
src/components/settings_modal/settings_modal.js
Normal file
|
@ -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
|
44
src/components/settings_modal/settings_modal.scss
Normal file
44
src/components/settings_modal/settings_modal.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
src/components/settings_modal/settings_modal.vue
Normal file
54
src/components/settings_modal/settings_modal.vue
Normal file
|
@ -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>
|
34
src/components/settings_modal/settings_modal_content.js
Normal file
34
src/components/settings_modal/settings_modal_content.js
Normal file
|
@ -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
|
43
src/components/settings_modal/settings_modal_content.scss
Normal file
43
src/components/settings_modal/settings_modal_content.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
73
src/components/settings_modal/settings_modal_content.vue
Normal file
73
src/components/settings_modal/settings_modal_content.vue
Normal file
|
@ -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>
|
65
src/components/settings_modal/tabs/data_import_export_tab.js
Normal file
65
src/components/settings_modal/tabs/data_import_export_tab.js
Normal file
|
@ -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> -->
|
44
src/components/settings_modal/tabs/filtering_tab.js
Normal file
44
src/components/settings_modal/tabs/filtering_tab.js
Normal file
|
@ -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
|
86
src/components/settings_modal/tabs/filtering_tab.vue
Normal file
86
src/components/settings_modal/tabs/filtering_tab.vue
Normal file
|
@ -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>
|
31
src/components/settings_modal/tabs/general_tab.js
Normal file
31
src/components/settings_modal/tabs/general_tab.js
Normal file
|
@ -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
|
272
src/components/settings_modal/tabs/general_tab.vue
Normal file
272
src/components/settings_modal/tabs/general_tab.vue
Normal file
|
@ -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>
|
136
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
136
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
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'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
this.$store.dispatch('getKnownDomains')
|
||||
},
|
||||
components: {
|
||||
TabSwitcher,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
ProgressButton,
|
||||
Autosuggest,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
knownDomains () {
|
||||
return this.$store.state.instance.knownDomains
|
||||
},
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
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.user.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.muting || userId === this.user.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)
|
||||
},
|
||||
filterUnMutedDomains (urls) {
|
||||
return urls.filter(url => !this.user.domainMutes.includes(url))
|
||||
},
|
||||
queryKnownDomains (query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.knownDomains.filter(url => url.toLowerCase().includes(query)))
|
||||
})
|
||||
},
|
||||
unmuteDomains (domains) {
|
||||
return this.$store.dispatch('unmuteDomains', domains)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MutesAndBlocks
|
29
src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
Normal file
29
src/components/settings_modal/tabs/mutes_and_blocks_tab.scss
Normal file
|
@ -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
|
||||
}
|
||||
}
|
171
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
171
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<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">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedDomains"
|
||||
:query="queryKnownDomains"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
>
|
||||
<DomainMuteCard
|
||||
slot-scope="row"
|
||||
:domain="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</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>
|
27
src/components/settings_modal/tabs/notifications_tab.js
Normal file
27
src/components/settings_modal/tabs/notifications_tab.js
Normal file
|
@ -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
|
54
src/components/settings_modal/tabs/notifications_tab.vue
Normal file
54
src/components/settings_modal/tabs/notifications_tab.vue
Normal file
|
@ -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> -->
|
179
src/components/settings_modal/tabs/profile_tab.js
Normal file
179
src/components/settings_modal/tabs/profile_tab.js
Normal file
|
@ -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
|
82
src/components/settings_modal/tabs/profile_tab.scss
Normal file
82
src/components/settings_modal/tabs/profile_tab.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
213
src/components/settings_modal/tabs/profile_tab.vue
Normal file
213
src/components/settings_modal/tabs/profile_tab.vue
Normal file
|
@ -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>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
@import '../../../../_variables.scss';
|
||||
.mfa-settings {
|
||||
.mfa-heading, .method-item {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
|
||||
.setup-otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mfa-backup-codes">
|
||||
<h4 v-if="displayTitle">
|
||||
{{ $t('settings.mfa.recovery_codes') }}
|
||||
</h4>
|
||||
|
@ -21,13 +21,15 @@
|
|||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../../../_variables.scss';
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
.mfa-backup-codes {
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
}
|
||||
</style>
|
106
src/components/settings_modal/tabs/security_tab/security_tab.js
Normal file
106
src/components/settings_modal/tabs/security_tab/security_tab.js
Normal file
|
@ -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
|
143
src/components/settings_modal/tabs/security_tab/security_tab.vue
Normal file
143
src/components/settings_modal/tabs/security_tab/security_tab.vue
Normal file
|
@ -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,
|
||||
hex2rgb,
|
||||
getContrastRatioLayers
|
||||
} from '../../services/color_convert/color_convert.js'
|
||||
} from 'src/services/color_convert/color_convert.js'
|
||||
import {
|
||||
DEFAULT_SHADOWS,
|
||||
generateColors,
|
||||
|
@ -14,26 +14,27 @@ import {
|
|||
getThemes,
|
||||
shadows2to3,
|
||||
colors2to3
|
||||
} from '../../services/style_setter/style_setter.js'
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
import {
|
||||
SLOT_INHERITANCE
|
||||
} from '../../services/theme_data/pleromafe.js'
|
||||
} from 'src/services/theme_data/pleromafe.js'
|
||||
import {
|
||||
CURRENT_VERSION,
|
||||
OPACITIES,
|
||||
getLayers,
|
||||
getOpacitySlot
|
||||
} from '../../services/theme_data/theme_data.service.js'
|
||||
import ColorInput from '../color_input/color_input.vue'
|
||||
import RangeInput from '../range_input/range_input.vue'
|
||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||
import ShadowControl from '../shadow_control/shadow_control.vue'
|
||||
import FontControl from '../font_control/font_control.vue'
|
||||
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
} from 'src/services/theme_data/theme_data.service.js'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import RangeInput from 'src/components/range_input/range_input.vue'
|
||||
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||
import FontControl from 'src/components/font_control/font_control.vue'
|
||||
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||
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 ExportImport from '../export_import/export_import.vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
// List of color values used in v1
|
||||
const v1OnlyNames = [
|
|
@ -1,5 +1,6 @@
|
|||
@import '../../_variables.scss';
|
||||
.style-switcher {
|
||||
@import 'src/_variables.scss';
|
||||
.theme-tab {
|
||||
padding-bottom: 2em;
|
||||
.theme-warning {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
@ -54,10 +55,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tab-switcher {
|
||||
margin: 0 -1em;
|
||||
}
|
||||
|
||||
.reset-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
@ -98,20 +95,25 @@
|
|||
align-items: baseline;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
|
||||
.btn {
|
||||
min-width: 1px;
|
||||
flex: 0 auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
margin-bottom: 1em;
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
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 {
|
||||
|
@ -161,7 +163,7 @@
|
|||
border-bottom: 1px dashed;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
margin: 1em -1em 0;
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: var(--body-background-image);
|
||||
background-size: cover;
|
||||
|
@ -328,6 +330,14 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: .25em;
|
||||
margin-right: .25em;
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="style-switcher">
|
||||
<div class="theme-tab">
|
||||
<div class="presets-container">
|
||||
<div class="save-load">
|
||||
<div
|
||||
|
@ -126,18 +126,20 @@
|
|||
>
|
||||
<div class="tab-header">
|
||||
<p>{{ $t('settings.theme_help') }}</p>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearOpacity"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_opacity') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearV1"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
<div class="tab-header-buttons">
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearOpacity"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_opacity') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearV1"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ $t('settings.theme_help_v2_1') }}</p>
|
||||
<h4>{{ $t('settings.style.common_colors.main') }}</h4>
|
||||
|
@ -254,6 +256,13 @@
|
|||
:label="$t('settings.links')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.postLink" />
|
||||
<ColorInput
|
||||
v-model="postGreentextColorLocal"
|
||||
name="postGreentextColor"
|
||||
:fallback="previewTheme.colors.cGreen"
|
||||
:label="$t('settings.greentext')"
|
||||
/>
|
||||
<ContrastRatio :contrast="previewContrast.postGreentext" />
|
||||
<h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
|
||||
<ColorInput
|
||||
v-model="alertErrorColorLocal"
|
||||
|
@ -951,6 +960,6 @@
|
|||
</div>
|
||||
</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>
|
24
src/components/settings_modal/tabs/version_tab.js
Normal file
24
src/components/settings_modal/tabs/version_tab.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { extractCommit } from 'src/services/version/version.service'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/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
|
31
src/components/settings_modal/tabs/version_tab.vue
Normal file
31
src/components/settings_modal/tabs/version_tab.vue
Normal file
|
@ -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) {
|
||||
GestureService.updateSwipe(e, this.closeGesture)
|
||||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
>
|
||||
<UserCard
|
||||
v-if="currentUser"
|
||||
:user="currentUser"
|
||||
:user-id="currentUser.id"
|
||||
:hide-bio="true"
|
||||
/>
|
||||
<div
|
||||
|
@ -122,9 +122,12 @@
|
|||
</router-link>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'settings' }">
|
||||
<a
|
||||
href="#"
|
||||
@click="openSettingsModal"
|
||||
>
|
||||
<i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
|
||||
</router-link>
|
||||
</a>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'about'}">
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
||||
import ReactButton from '../react_button/react_button.vue'
|
||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||
import Poll from '../poll/poll.vue'
|
||||
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import StatusPopover from '../status_popover/status_popover.vue'
|
||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
import { filter, unescape, uniqBy } from 'lodash'
|
||||
import { muteWordHits } from '../../services/status_parser/status_parser.js'
|
||||
import { unescape, uniqBy } from 'lodash'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
const Status = {
|
||||
|
@ -43,20 +38,19 @@ const Status = {
|
|||
replying: false,
|
||||
unmuted: false,
|
||||
userExpanded: false,
|
||||
showingTall: this.inConversation && this.focused,
|
||||
showingLongSubject: false,
|
||||
error: null,
|
||||
// not as computed because it sets the initial state which will be changed later
|
||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||
error: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localCollapseSubjectDefault () {
|
||||
return this.mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
muteWords () {
|
||||
return this.mergedConfig.muteWords
|
||||
},
|
||||
showReasonMutedThread () {
|
||||
return (
|
||||
this.status.thread_muted ||
|
||||
(this.status.reblog && this.status.reblog.thread_muted)
|
||||
) && !this.inConversation
|
||||
},
|
||||
repeaterClass () {
|
||||
const user = this.statusoid.user
|
||||
return highlightClass(user)
|
||||
|
@ -79,10 +73,6 @@ const Status = {
|
|||
const highlight = this.mergedConfig.highlight
|
||||
return highlightStyle(highlight[user.screen_name])
|
||||
},
|
||||
hideAttachments () {
|
||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||
},
|
||||
userProfileLink () {
|
||||
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
|
||||
},
|
||||
|
@ -110,15 +100,43 @@ const Status = {
|
|||
return !!this.currentUser
|
||||
},
|
||||
muteWordHits () {
|
||||
const statusText = this.status.text.toLowerCase()
|
||||
const statusSummary = this.status.summary.toLowerCase()
|
||||
const hits = filter(this.muteWords, (muteWord) => {
|
||||
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
|
||||
})
|
||||
|
||||
return hits
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
},
|
||||
muted () {
|
||||
const { status } = this
|
||||
const { reblog } = status
|
||||
const relationship = this.$store.getters.relationship(status.user.id)
|
||||
const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id)
|
||||
const reasonsToMute = (
|
||||
// Post is muted according to BE
|
||||
status.muted ||
|
||||
// Reprööt of a muted post according to BE
|
||||
(reblog && reblog.muted) ||
|
||||
// Muted user
|
||||
relationship.muting ||
|
||||
// Muted user of a reprööt
|
||||
(relationshipReblog && relationshipReblog.muting) ||
|
||||
// Thread is muted
|
||||
status.thread_muted ||
|
||||
// Wordfiltered
|
||||
this.muteWordHits.length > 0
|
||||
)
|
||||
const excusesNotToMute = (
|
||||
(
|
||||
this.inProfile && (
|
||||
// Don't mute user's posts on user timeline (except reblogs)
|
||||
(!reblog && status.user.id === this.profileUserId) ||
|
||||
// Same as above but also allow self-reblogs
|
||||
(reblog && reblog.user.id === this.profileUserId)
|
||||
)
|
||||
) ||
|
||||
// Don't mute statuses in muted conversation when said conversation is opened
|
||||
(this.inConversation && status.thread_muted)
|
||||
// No excuses if post has muted words
|
||||
) && !this.muteWordHits.length > 0
|
||||
|
||||
return !this.unmuted && !excusesNotToMute && reasonsToMute
|
||||
},
|
||||
muted () { return !this.unmuted && ((!(this.inProfile && this.status.user.id === this.profileUserId) && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
|
||||
hideFilteredStatuses () {
|
||||
return this.mergedConfig.hideFilteredStatuses
|
||||
},
|
||||
|
@ -135,20 +153,6 @@ const Status = {
|
|||
// use conversation highlight only when in conversation
|
||||
return this.status.id === this.highlight
|
||||
},
|
||||
// This is a bit hacky, but we want to approximate post height before rendering
|
||||
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
|
||||
// as well as approximate line count by counting characters and approximating ~80
|
||||
// per line.
|
||||
//
|
||||
// Using max-height + overflow: auto for status components resulted in false positives
|
||||
// very often with japanese characters, and it was very annoying.
|
||||
tallStatus () {
|
||||
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
|
||||
return lengthScore > 20
|
||||
},
|
||||
longSubject () {
|
||||
return this.status.summary.length > 900
|
||||
},
|
||||
isReply () {
|
||||
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
|
||||
},
|
||||
|
@ -178,8 +182,11 @@ const Status = {
|
|||
if (this.status.user.id === this.status.attentions[i].id) {
|
||||
continue
|
||||
}
|
||||
const taggedUser = this.$store.getters.findUser(this.status.attentions[i].id)
|
||||
if (checkFollowing && taggedUser && taggedUser.following) {
|
||||
// There's zero guarantee of this working. If we happen to have that user and their
|
||||
// relationship in store then it will work, but there's kinda little chance of having
|
||||
// them for people you're not following.
|
||||
const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]
|
||||
if (checkFollowing && relationship && relationship.following) {
|
||||
return false
|
||||
}
|
||||
if (this.status.attentions[i].id === this.currentUser.id) {
|
||||
|
@ -188,33 +195,6 @@ const Status = {
|
|||
}
|
||||
return this.status.attentions.length > 0
|
||||
},
|
||||
hideSubjectStatus () {
|
||||
if (this.tallStatus && !this.localCollapseSubjectDefault) {
|
||||
return false
|
||||
}
|
||||
return !this.expandingSubject && this.status.summary
|
||||
},
|
||||
hideTallStatus () {
|
||||
if (this.status.summary && this.localCollapseSubjectDefault) {
|
||||
return false
|
||||
}
|
||||
if (this.showingTall) {
|
||||
return false
|
||||
}
|
||||
return this.tallStatus
|
||||
},
|
||||
showingMore () {
|
||||
return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
|
||||
},
|
||||
nsfwClickthrough () {
|
||||
if (!this.status.nsfw) {
|
||||
return false
|
||||
}
|
||||
if (this.status.summary && this.localCollapseSubjectDefault) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
replySubject () {
|
||||
if (!this.status.summary) return ''
|
||||
const decodedSummary = unescape(this.status.summary)
|
||||
|
@ -228,83 +208,6 @@ const Status = {
|
|||
return ''
|
||||
}
|
||||
},
|
||||
attachmentSize () {
|
||||
if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
|
||||
(this.status.attachments.length > this.maxThumbnails)) {
|
||||
return 'hide'
|
||||
} else if (this.compact) {
|
||||
return 'small'
|
||||
}
|
||||
return 'normal'
|
||||
},
|
||||
galleryTypes () {
|
||||
if (this.attachmentSize === 'hide') {
|
||||
return []
|
||||
}
|
||||
return this.mergedConfig.playVideosInModal
|
||||
? ['image', 'video']
|
||||
: ['image']
|
||||
},
|
||||
galleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
nonGalleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
hasImageAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'image'
|
||||
)
|
||||
},
|
||||
hasVideoAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'video'
|
||||
)
|
||||
},
|
||||
maxThumbnails () {
|
||||
return this.mergedConfig.maxThumbnails
|
||||
},
|
||||
postBodyHtml () {
|
||||
const html = this.status.statusnet_html
|
||||
|
||||
if (this.mergedConfig.greentext) {
|
||||
try {
|
||||
if (html.includes('>')) {
|
||||
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
||||
return processHtml(html, (string) => {
|
||||
if (string.includes('>') &&
|
||||
string
|
||||
.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else {
|
||||
return string
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
} catch (e) {
|
||||
console.err('Failed to process status html', e)
|
||||
return html
|
||||
}
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
},
|
||||
contentHtml () {
|
||||
if (!this.status.summary_html) {
|
||||
return this.postBodyHtml
|
||||
}
|
||||
return this.status.summary_html + '<br />' + this.postBodyHtml
|
||||
},
|
||||
combinedFavsAndRepeatsUsers () {
|
||||
// Use the status from the global status repository since favs and repeats are saved in it
|
||||
const combinedUsers = [].concat(
|
||||
|
@ -313,9 +216,6 @@ const Status = {
|
|||
)
|
||||
return uniqBy(combinedUsers, 'id')
|
||||
},
|
||||
ownStatus () {
|
||||
return this.status.user.id === this.currentUser.id
|
||||
},
|
||||
tags () {
|
||||
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
|
||||
},
|
||||
|
@ -329,21 +229,18 @@ const Status = {
|
|||
})
|
||||
},
|
||||
components: {
|
||||
Attachment,
|
||||
FavoriteButton,
|
||||
ReactButton,
|
||||
RetweetButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
Poll,
|
||||
UserCard,
|
||||
UserAvatar,
|
||||
Gallery,
|
||||
LinkPreview,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
StatusPopover,
|
||||
EmojiReactions
|
||||
EmojiReactions,
|
||||
StatusContent
|
||||
},
|
||||
methods: {
|
||||
visibilityIcon (visibility) {
|
||||
|
@ -364,32 +261,6 @@ const Status = {
|
|||
clearError () {
|
||||
this.error = undefined
|
||||
},
|
||||
linkClicked (event) {
|
||||
const target = event.target.closest('.status-content a')
|
||||
if (target) {
|
||||
if (target.className.match(/mention/)) {
|
||||
const href = target.href
|
||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||
if (attn) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from link url
|
||||
const tag = extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
toggleReplying () {
|
||||
this.replying = !this.replying
|
||||
},
|
||||
|
@ -407,26 +278,8 @@ const Status = {
|
|||
toggleUserExpanded () {
|
||||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
toggleShowMore () {
|
||||
if (this.showingTall) {
|
||||
this.showingTall = false
|
||||
} else if (this.expandingSubject && this.status.summary) {
|
||||
this.expandingSubject = false
|
||||
} else if (this.hideTallStatus) {
|
||||
this.showingTall = true
|
||||
} else if (this.hideSubjectStatus && this.status.summary) {
|
||||
this.expandingSubject = true
|
||||
}
|
||||
},
|
||||
generateUserProfileLink (id, name) {
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -17,12 +17,33 @@
|
|||
</div>
|
||||
<template v-if="muted && !isPreview">
|
||||
<div class="media status container muted">
|
||||
<small>
|
||||
<small class="username">
|
||||
<i
|
||||
v-if="muted && retweet"
|
||||
class="button-icon icon-retweet"
|
||||
/>
|
||||
<router-link :to="userProfileLink">
|
||||
{{ status.user.screen_name }}
|
||||
</router-link>
|
||||
</small>
|
||||
<small class="muteWords">{{ muteWordHits.join(', ') }}</small>
|
||||
<small
|
||||
v-if="showReasonMutedThread"
|
||||
class="mute-thread"
|
||||
>
|
||||
{{ $t('status.thread_muted') }}
|
||||
</small>
|
||||
<small
|
||||
v-if="showReasonMutedThread && muteWordHits.length > 0"
|
||||
class="mute-thread"
|
||||
>
|
||||
{{ $t('status.thread_muted_and_words') }}
|
||||
</small>
|
||||
<small
|
||||
class="mute-words"
|
||||
:title="muteWordHits.join(', ')"
|
||||
>
|
||||
{{ muteWordHits.join(', ') }}
|
||||
</small>
|
||||
<a
|
||||
href="#"
|
||||
class="unmute"
|
||||
|
@ -94,7 +115,7 @@
|
|||
<div class="status-body">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user="status.user"
|
||||
:user-id="status.user.id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
class="status-usercard"
|
||||
|
@ -241,118 +262,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="longSubject"
|
||||
class="status-content-wrapper"
|
||||
:class="{ 'tall-status': !showingLongSubject }"
|
||||
>
|
||||
<a
|
||||
v-if="!showingLongSubject"
|
||||
class="tall-status-hider"
|
||||
:class="{ 'tall-status-hider_focused': isFocused }"
|
||||
href="#"
|
||||
@click.prevent="showingLongSubject=true"
|
||||
>{{ $t("general.show_more") }}</a>
|
||||
<div
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="contentHtml"
|
||||
/>
|
||||
<a
|
||||
v-if="showingLongSubject"
|
||||
href="#"
|
||||
class="status-unhider"
|
||||
@click.prevent="showingLongSubject=false"
|
||||
>{{ $t("general.show_less") }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{'tall-status': hideTallStatus}"
|
||||
class="status-content-wrapper"
|
||||
>
|
||||
<a
|
||||
v-if="hideTallStatus"
|
||||
class="tall-status-hider"
|
||||
:class="{ 'tall-status-hider_focused': isFocused }"
|
||||
href="#"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_more") }}</a>
|
||||
<div
|
||||
v-if="!hideSubjectStatus"
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="contentHtml"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="status.summary_html"
|
||||
/>
|
||||
<a
|
||||
v-if="hideSubjectStatus"
|
||||
href="#"
|
||||
class="cw-status-hider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ $t("general.show_more") }}
|
||||
<span
|
||||
v-if="hasImageAttachments"
|
||||
class="icon-picture"
|
||||
/>
|
||||
<span
|
||||
v-if="hasVideoAttachments"
|
||||
class="icon-video"
|
||||
/>
|
||||
<span
|
||||
v-if="status.card"
|
||||
class="icon-link"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="showingMore"
|
||||
href="#"
|
||||
class="status-unhider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_less") }}</a>
|
||||
</div>
|
||||
|
||||
<div v-if="status.poll && status.poll.options">
|
||||
<poll :base-poll="status.poll" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)"
|
||||
class="attachments media-body"
|
||||
>
|
||||
<attachment
|
||||
v-for="attachment in nonGalleryAttachments"
|
||||
:key="attachment.id"
|
||||
class="non-gallery"
|
||||
:size="attachmentSize"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachment="attachment"
|
||||
:allow-play="true"
|
||||
:set-media="setMedia()"
|
||||
/>
|
||||
<gallery
|
||||
v-if="galleryAttachments.length > 0"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachments="galleryAttachments"
|
||||
:set-media="setMedia()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.card && !hideSubjectStatus && !noHeading"
|
||||
class="link-preview media-body"
|
||||
>
|
||||
<link-preview
|
||||
:card="status.card"
|
||||
:size="attachmentSize"
|
||||
:nsfw="nsfwClickthrough"
|
||||
/>
|
||||
</div>
|
||||
<StatusContent
|
||||
:status="status"
|
||||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
:focused="isFocused"
|
||||
/>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
|
@ -419,7 +334,7 @@
|
|||
:status="status"
|
||||
/>
|
||||
<ReactButton
|
||||
:logged-in="loggedIn"
|
||||
v-if="loggedIn"
|
||||
:status="status"
|
||||
/>
|
||||
<extra-buttons
|
||||
|
@ -645,105 +560,6 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.tall-status {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
.status-content {
|
||||
height: 100%;
|
||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
||||
.tall-status-hider {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
position: absolute;
|
||||
height: 70px;
|
||||
margin-top: 150px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 110px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.status-unhider, .cw-status-hider {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
font-family: var(--postFont, sans-serif);
|
||||
line-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
color: var(--postLink, $fallback--link);
|
||||
}
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0.2em 0 0.2em 2em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code, samp, kbd, var, pre {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1em;
|
||||
margin: 1.0em 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1.1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.retweet-info {
|
||||
padding: 0.4em $status-margin;
|
||||
margin: 0;
|
||||
|
@ -805,11 +621,6 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.greentext {
|
||||
color: $fallback--cGreen;
|
||||
color: var(--cGreen, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.status-conversation {
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
@ -862,33 +673,54 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.muted {
|
||||
padding: 0.25em 0.5em;
|
||||
button {
|
||||
padding: .25em .6em;
|
||||
height: 1.2em;
|
||||
line-height: 1.2em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.username, .mute-thread, .mute-words {
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.username, .mute-words {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 0 1 auto;
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
.mute-thread {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mute-words {
|
||||
flex: 1 0 5em;
|
||||
margin-left: .2em;
|
||||
&::before {
|
||||
content: ' '
|
||||
}
|
||||
}
|
||||
|
||||
.unmute {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.muteWords {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
a.unmute {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.reply-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline :not(.panel-disabled) > {
|
||||
.status-el:last-child {
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.favs-repeated-users {
|
||||
margin-top: $status-margin;
|
||||
|
||||
|
|
210
src/components/status_content/status_content.js
Normal file
210
src/components/status_content/status_content.js
Normal file
|
@ -0,0 +1,210 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import Poll from '../poll/poll.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
const StatusContent = {
|
||||
name: 'StatusContent',
|
||||
props: [
|
||||
'status',
|
||||
'focused',
|
||||
'noHeading',
|
||||
'fullContent'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingTall: this.inConversation && this.focused,
|
||||
showingLongSubject: false,
|
||||
// not as computed because it sets the initial state which will be changed later
|
||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localCollapseSubjectDefault () {
|
||||
return this.mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
hideAttachments () {
|
||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||
},
|
||||
// This is a bit hacky, but we want to approximate post height before rendering
|
||||
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
|
||||
// as well as approximate line count by counting characters and approximating ~80
|
||||
// per line.
|
||||
//
|
||||
// Using max-height + overflow: auto for status components resulted in false positives
|
||||
// very often with japanese characters, and it was very annoying.
|
||||
tallStatus () {
|
||||
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
|
||||
return lengthScore > 20
|
||||
},
|
||||
longSubject () {
|
||||
return this.status.summary.length > 900
|
||||
},
|
||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
||||
mightHideBecauseSubject () {
|
||||
return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
|
||||
},
|
||||
mightHideBecauseTall () {
|
||||
return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
|
||||
},
|
||||
hideSubjectStatus () {
|
||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||
},
|
||||
hideTallStatus () {
|
||||
return this.mightHideBecauseTall && !this.showingTall
|
||||
},
|
||||
showingMore () {
|
||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
nsfwClickthrough () {
|
||||
if (!this.status.nsfw) {
|
||||
return false
|
||||
}
|
||||
if (this.status.summary && this.localCollapseSubjectDefault) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
attachmentSize () {
|
||||
if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
|
||||
(this.status.attachments.length > this.maxThumbnails)) {
|
||||
return 'hide'
|
||||
} else if (this.compact) {
|
||||
return 'small'
|
||||
}
|
||||
return 'normal'
|
||||
},
|
||||
galleryTypes () {
|
||||
if (this.attachmentSize === 'hide') {
|
||||
return []
|
||||
}
|
||||
return this.mergedConfig.playVideosInModal
|
||||
? ['image', 'video']
|
||||
: ['image']
|
||||
},
|
||||
galleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
nonGalleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
hasImageAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'image'
|
||||
)
|
||||
},
|
||||
hasVideoAttachments () {
|
||||
return this.status.attachments.some(
|
||||
file => fileType.fileType(file.mimetype) === 'video'
|
||||
)
|
||||
},
|
||||
maxThumbnails () {
|
||||
return this.mergedConfig.maxThumbnails
|
||||
},
|
||||
postBodyHtml () {
|
||||
const html = this.status.statusnet_html
|
||||
|
||||
if (this.mergedConfig.greentext) {
|
||||
try {
|
||||
if (html.includes('>')) {
|
||||
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
||||
return processHtml(html, (string) => {
|
||||
if (string.includes('>') &&
|
||||
string
|
||||
.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else {
|
||||
return string
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
} catch (e) {
|
||||
console.err('Failed to process status html', e)
|
||||
return html
|
||||
}
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
},
|
||||
contentHtml () {
|
||||
if (!this.status.summary_html) {
|
||||
return this.postBodyHtml
|
||||
}
|
||||
return this.status.summary_html + '<br />' + this.postBodyHtml
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
},
|
||||
components: {
|
||||
Attachment,
|
||||
Poll,
|
||||
Gallery,
|
||||
LinkPreview
|
||||
},
|
||||
methods: {
|
||||
linkClicked (event) {
|
||||
const target = event.target.closest('.status-content a')
|
||||
if (target) {
|
||||
if (target.className.match(/mention/)) {
|
||||
const href = target.href
|
||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||
if (attn) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from dataset or link url
|
||||
const tag = target.dataset.tag || extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
toggleShowMore () {
|
||||
if (this.mightHideBecauseTall) {
|
||||
this.showingTall = !this.showingTall
|
||||
} else if (this.mightHideBecauseSubject) {
|
||||
this.expandingSubject = !this.expandingSubject
|
||||
}
|
||||
},
|
||||
generateUserProfileLink (id, name) {
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StatusContent
|
240
src/components/status_content/status_content.vue
Normal file
240
src/components/status_content/status_content.vue
Normal file
|
@ -0,0 +1,240 @@
|
|||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div class="status-body">
|
||||
<slot name="header" />
|
||||
<div
|
||||
v-if="longSubject"
|
||||
class="status-content-wrapper"
|
||||
:class="{ 'tall-status': !showingLongSubject }"
|
||||
>
|
||||
<a
|
||||
v-if="!showingLongSubject"
|
||||
class="tall-status-hider"
|
||||
:class="{ 'tall-status-hider_focused': focused }"
|
||||
href="#"
|
||||
@click.prevent="showingLongSubject=true"
|
||||
>
|
||||
{{ $t("general.show_more") }}
|
||||
<span
|
||||
v-if="hasImageAttachments"
|
||||
class="icon-picture"
|
||||
/>
|
||||
<span
|
||||
v-if="hasVideoAttachments"
|
||||
class="icon-video"
|
||||
/>
|
||||
<span
|
||||
v-if="status.card"
|
||||
class="icon-link"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="contentHtml"
|
||||
/>
|
||||
<a
|
||||
v-if="showingLongSubject"
|
||||
href="#"
|
||||
class="status-unhider"
|
||||
@click.prevent="showingLongSubject=false"
|
||||
>{{ $t("general.show_less") }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{'tall-status': hideTallStatus}"
|
||||
class="status-content-wrapper"
|
||||
>
|
||||
<a
|
||||
v-if="hideTallStatus"
|
||||
class="tall-status-hider"
|
||||
:class="{ 'tall-status-hider_focused': focused }"
|
||||
href="#"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_more") }}</a>
|
||||
<div
|
||||
v-if="!hideSubjectStatus"
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="contentHtml"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="status.summary_html"
|
||||
/>
|
||||
<a
|
||||
v-if="hideSubjectStatus"
|
||||
href="#"
|
||||
class="cw-status-hider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_more") }}</a>
|
||||
<a
|
||||
v-if="showingMore"
|
||||
href="#"
|
||||
class="status-unhider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>{{ $t("general.show_less") }}</a>
|
||||
</div>
|
||||
|
||||
<div v-if="status.poll && status.poll.options">
|
||||
<poll :base-poll="status.poll" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
|
||||
class="attachments media-body"
|
||||
>
|
||||
<attachment
|
||||
v-for="attachment in nonGalleryAttachments"
|
||||
:key="attachment.id"
|
||||
class="non-gallery"
|
||||
:size="attachmentSize"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachment="attachment"
|
||||
:allow-play="true"
|
||||
:set-media="setMedia()"
|
||||
/>
|
||||
<gallery
|
||||
v-if="galleryAttachments.length > 0"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachments="galleryAttachments"
|
||||
:set-media="setMedia()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.card && !hideSubjectStatus && !noHeading"
|
||||
class="link-preview media-body"
|
||||
>
|
||||
<link-preview
|
||||
:card="status.card"
|
||||
:size="attachmentSize"
|
||||
:nsfw="nsfwClickthrough"
|
||||
/>
|
||||
</div>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</template>
|
||||
|
||||
<script src="./status_content.js" ></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
.status-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.tall-status {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
.status-content {
|
||||
height: 100%;
|
||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
||||
.tall-status-hider {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
position: absolute;
|
||||
height: 70px;
|
||||
margin-top: 150px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 110px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.status-unhider, .cw-status-hider {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
font-family: var(--postFont, sans-serif);
|
||||
line-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0.2em 0 0.2em 2em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code, samp, kbd, var, pre {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1em;
|
||||
margin: 1.0em 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1.1em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.greentext {
|
||||
color: $fallback--cGreen;
|
||||
color: var(--postGreentext, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.timeline :not(.panel-disabled) > {
|
||||
.status-el:last-child {
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -5,6 +5,11 @@ const StatusPopover = {
|
|||
props: [
|
||||
'statusId'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
error: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
status () {
|
||||
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
|
||||
|
@ -18,6 +23,8 @@ const StatusPopover = {
|
|||
enter () {
|
||||
if (!this.status) {
|
||||
this.$store.dispatch('fetchStatus', this.statusId)
|
||||
.then(data => (this.error = false))
|
||||
.catch(e => (this.error = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,15 @@
|
|||
:statusoid="status"
|
||||
:compact="true"
|
||||
/>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="status-preview-no-content faint"
|
||||
>
|
||||
{{ $t('status.status_unavailable') }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="status-preview-loading"
|
||||
class="status-preview-no-content"
|
||||
>
|
||||
<i class="icon-spin4 animate-spin" />
|
||||
</div>
|
||||
|
@ -50,7 +56,7 @@
|
|||
border: none;
|
||||
}
|
||||
|
||||
.status-preview-loading {
|
||||
.status-preview-no-content {
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
|
||||
|
|
|
@ -23,12 +23,15 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.still-image {
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover canvas {
|
||||
display: none;
|
||||
|
@ -36,7 +39,7 @@
|
|||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,11 @@ export default Vue.component('tab-switcher', {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sideTabBar: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
@ -55,6 +60,9 @@ export default Vue.component('tab-switcher', {
|
|||
this.onSwitch.call(null, this.$slots.default[index].key)
|
||||
}
|
||||
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
|
||||
const classesTab = ['tab']
|
||||
const classesWrapper = ['tab-wrapper']
|
||||
|
||||
if (this.activeIndex === index) {
|
||||
classesTab.push('active')
|
||||
classesWrapper.push('active')
|
||||
|
@ -87,8 +94,14 @@ export default Vue.component('tab-switcher', {
|
|||
<button
|
||||
disabled={slot.data.attrs.disabled}
|
||||
onClick={this.activateTab(index)}
|
||||
class={classesTab.join(' ')}>
|
||||
{slot.data.attrs.label}</button>
|
||||
class={classesTab.join(' ')}
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
@ -96,20 +109,32 @@ export default Vue.component('tab-switcher', {
|
|||
const contents = this.$slots.default.map((slot, index) => {
|
||||
if (!slot.tag) return
|
||||
const active = this.activeIndex === index
|
||||
if (this.renderOnlyFocused) {
|
||||
return active
|
||||
? <div class="active">{slot}</div>
|
||||
: <div class="hidden"></div>
|
||||
const classes = [ active ? 'active' : 'hidden' ]
|
||||
if (slot.data.attrs.fullHeight) {
|
||||
classes.push('full-height')
|
||||
}
|
||||
return <div class={active ? 'active' : 'hidden' }>{slot}</div>
|
||||
const renderSlot = (!this.renderOnlyFocused || active)
|
||||
? slot
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div class={classes}>
|
||||
{
|
||||
this.sideTabBar
|
||||
? <h1 class="mobile-label">{slot.data.attrs.label}</h1>
|
||||
: ''
|
||||
}
|
||||
{renderSlot}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="tab-switcher">
|
||||
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
|
||||
<div class="tabs">
|
||||
{tabs}
|
||||
</div>
|
||||
<div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
|
||||
<div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
|
||||
{contents}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,144 @@
|
|||
|
||||
.tab-switcher {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tab-icon {
|
||||
font-size: 2em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.top-tabs {
|
||||
flex-direction: column;
|
||||
|
||||
> .tabs {
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding-top: 5px;
|
||||
flex-direction: row;
|
||||
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
flex: 1 1 auto;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
.tab-wrapper {
|
||||
height: 28px;
|
||||
|
||||
&: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 {
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-bottom: 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;
|
||||
|
@ -11,88 +148,89 @@
|
|||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.full-height:not(.hidden) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> *:not(.mobile-label) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.scrollable-tabs {
|
||||
flex-basis: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
padding: 6px 1em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--tab, $fallback--fg);
|
||||
|
||||
&, &:active .tab-icon {
|
||||
color: $fallback--text;
|
||||
color: var(--tabText, $fallback--text);
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
z-index: 4;
|
||||
|
||||
&:hover {
|
||||
z-index: 6;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
color: $fallback--text;
|
||||
color: var(--tabActiveText, $fallback--text);
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 26px;
|
||||
vertical-align: top;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding-top: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after, &::before {
|
||||
display: block;
|
||||
content: '';
|
||||
flex: 1 1 auto;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-wrapper {
|
||||
height: 28px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
.tab-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.tab {
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
position: relative;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding: 6px 1em;
|
||||
padding-bottom: 99px;
|
||||
margin-bottom: 6px - 99px;
|
||||
white-space: nowrap;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--tabText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--tab, $fallback--fg);
|
||||
|
||||
&:not(.active) {
|
||||
z-index: 4;
|
||||
|
||||
&:hover {
|
||||
z-index: 6;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
color: $fallback--text;
|
||||
color: var(--tabActiveText, $fallback--text);
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 26px;
|
||||
vertical-align: top;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 7;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
&:not(.active) {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { mapGetters } from 'vuex'
|
|||
|
||||
export default {
|
||||
props: [
|
||||
'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
|
||||
'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -21,6 +21,12 @@ export default {
|
|||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
classes () {
|
||||
return [{
|
||||
'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
|
||||
|
|
|
@ -50,15 +50,6 @@
|
|||
>
|
||||
{{ user.name }}
|
||||
</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
|
||||
v-if="isOtherUser && !user.is_local"
|
||||
:href="user.statusnet_profile_url"
|
||||
|
@ -69,6 +60,7 @@
|
|||
<AccountActions
|
||||
v-if="isOtherUser && loggedIn"
|
||||
:user="user"
|
||||
:relationship="relationship"
|
||||
/>
|
||||
</div>
|
||||
<div class="bottom-line">
|
||||
|
@ -92,7 +84,7 @@
|
|||
</div>
|
||||
<div class="user-meta">
|
||||
<div
|
||||
v-if="user.follows_you && loggedIn && isOtherUser"
|
||||
v-if="relationship.followed_by && loggedIn && isOtherUser"
|
||||
class="following"
|
||||
>
|
||||
{{ $t('user_card.follows_you') }}
|
||||
|
@ -117,7 +109,7 @@
|
|||
type="color"
|
||||
>
|
||||
<label
|
||||
for="style-switcher"
|
||||
for="theme_tab"
|
||||
class="userHighlightSel select"
|
||||
>
|
||||
<select
|
||||
|
@ -139,10 +131,10 @@
|
|||
class="user-interactions"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<FollowButton :user="user" />
|
||||
<template v-if="user.following">
|
||||
<FollowButton :relationship="relationship" />
|
||||
<template v-if="relationship.following">
|
||||
<ProgressButton
|
||||
v-if="!user.subscribed"
|
||||
v-if="!relationship.subscribing"
|
||||
class="btn btn-default"
|
||||
:click="subscribeUser"
|
||||
:title="$t('user_card.subscribe')"
|
||||
|
@ -161,7 +153,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<button
|
||||
v-if="user.muted"
|
||||
v-if="relationship.muting"
|
||||
class="btn btn-default btn-block toggled"
|
||||
@click="unmuteUser"
|
||||
>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
class="panel panel-default signed-in"
|
||||
>
|
||||
<UserCard
|
||||
:user="user"
|
||||
:user-id="user.id"
|
||||
:hide-bio="true"
|
||||
rounded="top"
|
||||
/>
|
||||
|
|
|
@ -3,6 +3,7 @@ import UserCard from '../user_card/user_card.vue'
|
|||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import Timeline from '../timeline/timeline.vue'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import List from '../list/list.vue'
|
||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||
|
||||
|
@ -146,6 +147,7 @@ const UserProfile = {
|
|||
FollowerList,
|
||||
FriendList,
|
||||
FollowCard,
|
||||
TabSwitcher,
|
||||
Conversation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
class="user-profile panel panel-default"
|
||||
>
|
||||
<UserCard
|
||||
:user="user"
|
||||
:user-id="userId"
|
||||
:switcher="true"
|
||||
:selected="timeline.viewing"
|
||||
:allow-zooming-avatar="true"
|
||||
|
|
|
@ -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: (input) => this.$store.dispatch('searchUsers', input)
|
||||
})
|
||||
},
|
||||
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 user = this.$store.getters.findUser(userId)
|
||||
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const user = this.$store.getters.findUser(userId)
|
||||
return !user || user.muted || user.id === 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,716 +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">
|
||||
<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>
|
||||
<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>
|
406
src/i18n/ar.json
406
src/i18n/ar.json
|
@ -1,206 +1,206 @@
|
|||
{
|
||||
"chat": {
|
||||
"title": "الدردشة"
|
||||
"chat": {
|
||||
"title": "الدردشة"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "الدردشة",
|
||||
"gopher": "غوفر",
|
||||
"media_proxy": "بروكسي الوسائط",
|
||||
"scope_options": "",
|
||||
"text_limit": "الحد الأقصى للنص",
|
||||
"title": "الميّزات",
|
||||
"who_to_follow": "للمتابعة"
|
||||
},
|
||||
"finder": {
|
||||
"error_fetching_user": "خطأ أثناء جلب صفحة المستخدم",
|
||||
"find_user": "البحث عن مستخدِم"
|
||||
},
|
||||
"general": {
|
||||
"apply": "تطبيق",
|
||||
"submit": "إرسال"
|
||||
},
|
||||
"login": {
|
||||
"login": "تسجيل الدخول",
|
||||
"logout": "الخروج",
|
||||
"password": "الكلمة السرية",
|
||||
"placeholder": "مثال lain",
|
||||
"register": "انشاء حساب",
|
||||
"username": "إسم المستخدم"
|
||||
},
|
||||
"nav": {
|
||||
"chat": "الدردشة المحلية",
|
||||
"friend_requests": "طلبات المتابَعة",
|
||||
"mentions": "الإشارات",
|
||||
"public_tl": "الخيط الزمني العام",
|
||||
"timeline": "الخيط الزمني",
|
||||
"twkn": "كافة الشبكة المعروفة"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "منشور مجهول، جارٍ البحث عنه…",
|
||||
"favorited_you": "أعجِب بمنشورك",
|
||||
"followed_you": "يُتابعك",
|
||||
"load_older": "تحميل الإشعارات الأقدم",
|
||||
"notifications": "الإخطارات",
|
||||
"read": "مقروء!",
|
||||
"repeated_you": "شارَك منشورك"
|
||||
},
|
||||
"post_status": {
|
||||
"account_not_locked_warning": "",
|
||||
"account_not_locked_warning_link": "مقفل",
|
||||
"attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس",
|
||||
"content_type": {
|
||||
"text/plain": "نص صافٍ"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "الدردشة",
|
||||
"gopher": "غوفر",
|
||||
"media_proxy": "بروكسي الوسائط",
|
||||
"scope_options": "",
|
||||
"text_limit": "الحد الأقصى للنص",
|
||||
"title": "الميّزات",
|
||||
"who_to_follow": "للمتابعة"
|
||||
},
|
||||
"finder": {
|
||||
"error_fetching_user": "خطأ أثناء جلب صفحة المستخدم",
|
||||
"find_user": "البحث عن مستخدِم"
|
||||
},
|
||||
"general": {
|
||||
"apply": "تطبيق",
|
||||
"submit": "إرسال"
|
||||
},
|
||||
"login": {
|
||||
"login": "تسجيل الدخول",
|
||||
"logout": "الخروج",
|
||||
"password": "الكلمة السرية",
|
||||
"placeholder": "مثال lain",
|
||||
"register": "انشاء حساب",
|
||||
"username": "إسم المستخدم"
|
||||
},
|
||||
"nav": {
|
||||
"chat": "الدردشة المحلية",
|
||||
"friend_requests": "طلبات المتابَعة",
|
||||
"mentions": "الإشارات",
|
||||
"public_tl": "الخيط الزمني العام",
|
||||
"timeline": "الخيط الزمني",
|
||||
"twkn": "كافة الشبكة المعروفة"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "منشور مجهول، جارٍ البحث عنه…",
|
||||
"favorited_you": "أعجِب بمنشورك",
|
||||
"followed_you": "يُتابعك",
|
||||
"load_older": "تحميل الإشعارات الأقدم",
|
||||
"notifications": "الإخطارات",
|
||||
"read": "مقروء!",
|
||||
"repeated_you": "شارَك منشورك"
|
||||
},
|
||||
"post_status": {
|
||||
"account_not_locked_warning": "",
|
||||
"account_not_locked_warning_link": "مقفل",
|
||||
"attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس",
|
||||
"content_type": {
|
||||
"text/plain": "نص صافٍ"
|
||||
},
|
||||
"content_warning": "الموضوع (اختياري)",
|
||||
"default": "وصلت للتوّ إلى لوس أنجلس.",
|
||||
"direct_warning": "",
|
||||
"posting": "النشر",
|
||||
"scope": {
|
||||
"direct": "",
|
||||
"private": "",
|
||||
"public": "علني - يُنشر على الخيوط الزمنية العمومية",
|
||||
"unlisted": "غير مُدرَج - لا يُنشَر على الخيوط الزمنية العمومية"
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
"bio": "السيرة الذاتية",
|
||||
"email": "عنوان البريد الإلكتروني",
|
||||
"fullname": "الإسم المعروض",
|
||||
"password_confirm": "تأكيد الكلمة السرية",
|
||||
"registration": "التسجيل",
|
||||
"token": "رمز الدعوة"
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "المُرفَقات",
|
||||
"attachments": "المُرفَقات",
|
||||
"autoload": "",
|
||||
"avatar": "الصورة الرمزية",
|
||||
"avatarAltRadius": "الصور الرمزية (الإشعارات)",
|
||||
"avatarRadius": "الصور الرمزية",
|
||||
"background": "الخلفية",
|
||||
"bio": "السيرة الذاتية",
|
||||
"btnRadius": "الأزرار",
|
||||
"cBlue": "أزرق (الرد، المتابَعة)",
|
||||
"cGreen": "أخضر (إعادة النشر)",
|
||||
"cOrange": "برتقالي (مفضلة)",
|
||||
"cRed": "أحمر (إلغاء)",
|
||||
"change_password": "تغيير كلمة السر",
|
||||
"change_password_error": "وقع هناك خلل أثناء تعديل كلمتك السرية.",
|
||||
"changed_password": "تم تغيير كلمة المرور بنجاح!",
|
||||
"collapse_subject": "",
|
||||
"confirm_new_password": "تأكيد كلمة السر الجديدة",
|
||||
"current_avatar": "صورتك الرمزية الحالية",
|
||||
"current_password": "كلمة السر الحالية",
|
||||
"current_profile_banner": "الرأسية الحالية لصفحتك الشخصية",
|
||||
"data_import_export_tab": "تصدير واستيراد البيانات",
|
||||
"default_vis": "أسلوب العرض الافتراضي",
|
||||
"delete_account": "حذف الحساب",
|
||||
"delete_account_description": "حذف حسابك و كافة منشوراتك نهائيًا.",
|
||||
"delete_account_error": "",
|
||||
"delete_account_instructions": "يُرجى إدخال كلمتك السرية أدناه لتأكيد عملية حذف الحساب.",
|
||||
"export_theme": "حفظ النموذج",
|
||||
"filtering": "التصفية",
|
||||
"filtering_explanation": "سيتم إخفاء كافة المنشورات التي تحتوي على هذه الكلمات، كلمة واحدة في كل سطر",
|
||||
"follow_export": "تصدير الاشتراكات",
|
||||
"follow_export_button": "تصدير الاشتراكات كملف csv",
|
||||
"follow_export_processing": "التصدير جارٍ، سوف يُطلَب منك تنزيل ملفك بعد حين",
|
||||
"follow_import": "استيراد الاشتراكات",
|
||||
"follow_import_error": "خطأ أثناء استيراد المتابِعين",
|
||||
"follows_imported": "",
|
||||
"foreground": "الأمامية",
|
||||
"general": "الإعدادات العامة",
|
||||
"hide_attachments_in_convo": "إخفاء المرفقات على المحادثات",
|
||||
"hide_attachments_in_tl": "إخفاء المرفقات على الخيط الزمني",
|
||||
"hide_post_stats": "",
|
||||
"hide_user_stats": "",
|
||||
"import_followers_from_a_csv_file": "",
|
||||
"import_theme": "تحميل نموذج",
|
||||
"inputRadius": "",
|
||||
"instance_default": "",
|
||||
"interfaceLanguage": "لغة الواجهة",
|
||||
"invalid_theme_imported": "",
|
||||
"limited_availability": "غير متوفر على متصفحك",
|
||||
"links": "الروابط",
|
||||
"lock_account_description": "",
|
||||
"loop_video": "",
|
||||
"loop_video_silent_only": "",
|
||||
"name": "الاسم",
|
||||
"name_bio": "الاسم والسيرة الذاتية",
|
||||
"new_password": "كلمة السر الجديدة",
|
||||
"no_rich_text_description": "",
|
||||
"notification_visibility": "نوع الإشعارات التي تريد عرضها",
|
||||
"notification_visibility_follows": "يتابع",
|
||||
"notification_visibility_likes": "الإعجابات",
|
||||
"notification_visibility_mentions": "الإشارات",
|
||||
"notification_visibility_repeats": "",
|
||||
"nsfw_clickthrough": "",
|
||||
"oauth_tokens": "رموز OAuth",
|
||||
"token": "رمز",
|
||||
"refresh_token": "رمز التحديث",
|
||||
"valid_until": "صالح حتى",
|
||||
"revoke_token": "سحب",
|
||||
"panelRadius": "",
|
||||
"pause_on_unfocused": "",
|
||||
"presets": "النماذج",
|
||||
"profile_background": "خلفية الصفحة الشخصية",
|
||||
"profile_banner": "رأسية الصفحة الشخصية",
|
||||
"profile_tab": "الملف الشخصي",
|
||||
"radii_help": "",
|
||||
"replies_in_timeline": "الردود على الخيط الزمني",
|
||||
"reply_link_preview": "",
|
||||
"reply_visibility_all": "عرض كافة الردود",
|
||||
"reply_visibility_following": "",
|
||||
"reply_visibility_self": "",
|
||||
"saving_err": "خطأ أثناء حفظ الإعدادات",
|
||||
"saving_ok": "تم حفظ الإعدادات",
|
||||
"security_tab": "الأمان",
|
||||
"set_new_avatar": "اختيار صورة رمزية جديدة",
|
||||
"set_new_profile_background": "اختيار خلفية جديدة للملف الشخصي",
|
||||
"set_new_profile_banner": "اختيار رأسية جديدة للصفحة الشخصية",
|
||||
"settings": "الإعدادات",
|
||||
"stop_gifs": "",
|
||||
"streaming": "",
|
||||
"text": "النص",
|
||||
"theme": "المظهر",
|
||||
"theme_help": "",
|
||||
"tooltipRadius": "",
|
||||
"user_settings": "إعدادات المستخدم",
|
||||
"values": {
|
||||
"false": "لا",
|
||||
"true": "نعم"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "",
|
||||
"conversation": "محادثة",
|
||||
"error_fetching": "خطأ أثناء جلب التحديثات",
|
||||
"load_older": "تحميل المنشورات القديمة",
|
||||
"no_retweet_hint": "",
|
||||
"repeated": "",
|
||||
"show_new": "عرض الجديد",
|
||||
"up_to_date": "تم تحديثه"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "قبول",
|
||||
"block": "حظر",
|
||||
"blocked": "تم حظره!",
|
||||
"deny": "رفض",
|
||||
"follow": "اتبع",
|
||||
"followees": "",
|
||||
"followers": "مُتابِعون",
|
||||
"following": "",
|
||||
"follows_you": "يتابعك!",
|
||||
"mute": "كتم",
|
||||
"muted": "تم كتمه",
|
||||
"per_day": "في اليوم",
|
||||
"remote_follow": "مُتابَعة عن بُعد",
|
||||
"statuses": "المنشورات"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "الخيط الزمني للمستخدم"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "المزيد",
|
||||
"who_to_follow": "للمتابعة"
|
||||
"content_warning": "الموضوع (اختياري)",
|
||||
"default": "وصلت للتوّ إلى لوس أنجلس.",
|
||||
"direct_warning": "",
|
||||
"posting": "النشر",
|
||||
"scope": {
|
||||
"direct": "",
|
||||
"private": "",
|
||||
"public": "علني - يُنشر على الخيوط الزمنية العمومية",
|
||||
"unlisted": "غير مُدرَج - لا يُنشَر على الخيوط الزمنية العمومية"
|
||||
}
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
"bio": "السيرة الذاتية",
|
||||
"email": "عنوان البريد الإلكتروني",
|
||||
"fullname": "الإسم المعروض",
|
||||
"password_confirm": "تأكيد الكلمة السرية",
|
||||
"registration": "التسجيل",
|
||||
"token": "رمز الدعوة"
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "المُرفَقات",
|
||||
"attachments": "المُرفَقات",
|
||||
"autoload": "",
|
||||
"avatar": "الصورة الرمزية",
|
||||
"avatarAltRadius": "الصور الرمزية (الإشعارات)",
|
||||
"avatarRadius": "الصور الرمزية",
|
||||
"background": "الخلفية",
|
||||
"bio": "السيرة الذاتية",
|
||||
"btnRadius": "الأزرار",
|
||||
"cBlue": "أزرق (الرد، المتابَعة)",
|
||||
"cGreen": "أخضر (إعادة النشر)",
|
||||
"cOrange": "برتقالي (مفضلة)",
|
||||
"cRed": "أحمر (إلغاء)",
|
||||
"change_password": "تغيير كلمة السر",
|
||||
"change_password_error": "وقع هناك خلل أثناء تعديل كلمتك السرية.",
|
||||
"changed_password": "تم تغيير كلمة المرور بنجاح!",
|
||||
"collapse_subject": "",
|
||||
"confirm_new_password": "تأكيد كلمة السر الجديدة",
|
||||
"current_avatar": "صورتك الرمزية الحالية",
|
||||
"current_password": "كلمة السر الحالية",
|
||||
"current_profile_banner": "الرأسية الحالية لصفحتك الشخصية",
|
||||
"data_import_export_tab": "تصدير واستيراد البيانات",
|
||||
"default_vis": "أسلوب العرض الافتراضي",
|
||||
"delete_account": "حذف الحساب",
|
||||
"delete_account_description": "حذف حسابك و كافة منشوراتك نهائيًا.",
|
||||
"delete_account_error": "",
|
||||
"delete_account_instructions": "يُرجى إدخال كلمتك السرية أدناه لتأكيد عملية حذف الحساب.",
|
||||
"export_theme": "حفظ النموذج",
|
||||
"filtering": "التصفية",
|
||||
"filtering_explanation": "سيتم إخفاء كافة المنشورات التي تحتوي على هذه الكلمات، كلمة واحدة في كل سطر",
|
||||
"follow_export": "تصدير الاشتراكات",
|
||||
"follow_export_button": "تصدير الاشتراكات كملف csv",
|
||||
"follow_export_processing": "التصدير جارٍ، سوف يُطلَب منك تنزيل ملفك بعد حين",
|
||||
"follow_import": "استيراد الاشتراكات",
|
||||
"follow_import_error": "خطأ أثناء استيراد المتابِعين",
|
||||
"follows_imported": "",
|
||||
"foreground": "الأمامية",
|
||||
"general": "الإعدادات العامة",
|
||||
"hide_attachments_in_convo": "إخفاء المرفقات على المحادثات",
|
||||
"hide_attachments_in_tl": "إخفاء المرفقات على الخيط الزمني",
|
||||
"hide_post_stats": "",
|
||||
"hide_user_stats": "",
|
||||
"import_followers_from_a_csv_file": "",
|
||||
"import_theme": "تحميل نموذج",
|
||||
"inputRadius": "",
|
||||
"instance_default": "",
|
||||
"interfaceLanguage": "لغة الواجهة",
|
||||
"invalid_theme_imported": "",
|
||||
"limited_availability": "غير متوفر على متصفحك",
|
||||
"links": "الروابط",
|
||||
"lock_account_description": "",
|
||||
"loop_video": "",
|
||||
"loop_video_silent_only": "",
|
||||
"name": "الاسم",
|
||||
"name_bio": "الاسم والسيرة الذاتية",
|
||||
"new_password": "كلمة السر الجديدة",
|
||||
"no_rich_text_description": "",
|
||||
"notification_visibility": "نوع الإشعارات التي تريد عرضها",
|
||||
"notification_visibility_follows": "يتابع",
|
||||
"notification_visibility_likes": "الإعجابات",
|
||||
"notification_visibility_mentions": "الإشارات",
|
||||
"notification_visibility_repeats": "",
|
||||
"nsfw_clickthrough": "",
|
||||
"oauth_tokens": "رموز OAuth",
|
||||
"token": "رمز",
|
||||
"refresh_token": "رمز التحديث",
|
||||
"valid_until": "صالح حتى",
|
||||
"revoke_token": "سحب",
|
||||
"panelRadius": "",
|
||||
"pause_on_unfocused": "",
|
||||
"presets": "النماذج",
|
||||
"profile_background": "خلفية الصفحة الشخصية",
|
||||
"profile_banner": "رأسية الصفحة الشخصية",
|
||||
"profile_tab": "الملف الشخصي",
|
||||
"radii_help": "",
|
||||
"replies_in_timeline": "الردود على الخيط الزمني",
|
||||
"reply_link_preview": "",
|
||||
"reply_visibility_all": "عرض كافة الردود",
|
||||
"reply_visibility_following": "",
|
||||
"reply_visibility_self": "",
|
||||
"saving_err": "خطأ أثناء حفظ الإعدادات",
|
||||
"saving_ok": "تم حفظ الإعدادات",
|
||||
"security_tab": "الأمان",
|
||||
"set_new_avatar": "اختيار صورة رمزية جديدة",
|
||||
"set_new_profile_background": "اختيار خلفية جديدة للملف الشخصي",
|
||||
"set_new_profile_banner": "اختيار رأسية جديدة للصفحة الشخصية",
|
||||
"settings": "الإعدادات",
|
||||
"stop_gifs": "",
|
||||
"streaming": "",
|
||||
"text": "النص",
|
||||
"theme": "المظهر",
|
||||
"theme_help": "",
|
||||
"tooltipRadius": "",
|
||||
"user_settings": "إعدادات المستخدم",
|
||||
"values": {
|
||||
"false": "لا",
|
||||
"true": "نعم"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "",
|
||||
"conversation": "محادثة",
|
||||
"error_fetching": "خطأ أثناء جلب التحديثات",
|
||||
"load_older": "تحميل المنشورات القديمة",
|
||||
"no_retweet_hint": "",
|
||||
"repeated": "",
|
||||
"show_new": "عرض الجديد",
|
||||
"up_to_date": "تم تحديثه"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "قبول",
|
||||
"block": "حظر",
|
||||
"blocked": "تم حظره!",
|
||||
"deny": "رفض",
|
||||
"follow": "اتبع",
|
||||
"followees": "",
|
||||
"followers": "مُتابِعون",
|
||||
"following": "",
|
||||
"follows_you": "يتابعك!",
|
||||
"mute": "كتم",
|
||||
"muted": "تم كتمه",
|
||||
"per_day": "في اليوم",
|
||||
"remote_follow": "مُتابَعة عن بُعد",
|
||||
"statuses": "المنشورات"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "الخيط الزمني للمستخدم"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "المزيد",
|
||||
"who_to_follow": "للمتابعة"
|
||||
}
|
||||
}
|
||||
|
|
223
src/i18n/de.json
223
src/i18n/de.json
|
@ -7,8 +7,8 @@
|
|||
"gopher": "Gopher",
|
||||
"media_proxy": "Medienproxy",
|
||||
"scope_options": "Reichweitenoptionen",
|
||||
"text_limit": "Textlimit",
|
||||
"title": "Features",
|
||||
"text_limit": "Zeichenlimit",
|
||||
"title": "Funktionen",
|
||||
"who_to_follow": "Wem folgen?"
|
||||
},
|
||||
"finder": {
|
||||
|
@ -17,7 +17,18 @@
|
|||
},
|
||||
"general": {
|
||||
"apply": "Anwenden",
|
||||
"submit": "Absenden"
|
||||
"submit": "Absenden",
|
||||
"more": "Mehr",
|
||||
"generic_error": "Ein Fehler ist aufgetreten",
|
||||
"optional": "Optional",
|
||||
"show_more": "Zeige mehr",
|
||||
"show_less": "Zeige weniger",
|
||||
"dismiss": "Ablehnen",
|
||||
"cancel": "Abbrechen",
|
||||
"disable": "Deaktivieren",
|
||||
"enable": "Aktivieren",
|
||||
"confirm": "Bestätigen",
|
||||
"verify": "Verifizieren"
|
||||
},
|
||||
"login": {
|
||||
"login": "Anmelden",
|
||||
|
@ -26,7 +37,16 @@
|
|||
"password": "Passwort",
|
||||
"placeholder": "z.B. lain",
|
||||
"register": "Registrieren",
|
||||
"username": "Benutzername"
|
||||
"username": "Benutzername",
|
||||
"authentication_code": "Authentifizierungscode",
|
||||
"enter_recovery_code": "Gebe einen Wiederherstellungscode ein",
|
||||
"recovery_code": "Wiederherstellungscode",
|
||||
"heading": {
|
||||
"totp": "Zwei-Faktor Authentifizierung",
|
||||
"recovery": "Zwei-Faktor Wiederherstellung"
|
||||
},
|
||||
"hint": "Anmelden um an der Diskussion teilzunehmen",
|
||||
"enter_two_factor_code": "Gebe einen Zwei-Faktor-Code ein"
|
||||
},
|
||||
"nav": {
|
||||
"about": "Über",
|
||||
|
@ -41,7 +61,9 @@
|
|||
"twkn": "Das gesamte bekannte Netzwerk",
|
||||
"user_search": "Benutzersuche",
|
||||
"search": "Suche",
|
||||
"preferences": "Voreinstellungen"
|
||||
"preferences": "Voreinstellungen",
|
||||
"administration": "Administration",
|
||||
"who_to_follow": "Wem folgen"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Unbekannte Nachricht, suche danach...",
|
||||
|
@ -50,7 +72,11 @@
|
|||
"load_older": "Ältere Benachrichtigungen laden",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"read": "Gelesen!",
|
||||
"repeated_you": "wiederholte deine Nachricht"
|
||||
"repeated_you": "wiederholte deine Nachricht",
|
||||
"follow_request": "möchte dir folgen",
|
||||
"migrated_to": "migrierte zu",
|
||||
"reacted_with": "reagierte mit {0}",
|
||||
"no_more_notifications": "Keine Benachrichtigungen mehr"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Neuen Status veröffentlichen",
|
||||
|
@ -58,7 +84,10 @@
|
|||
"account_not_locked_warning_link": "gesperrt",
|
||||
"attachments_sensitive": "Anhänge als heikel markieren",
|
||||
"content_type": {
|
||||
"text/plain": "Nur Text"
|
||||
"text/plain": "Nur Text",
|
||||
"text/bbcode": "BBCode",
|
||||
"text/markdown": "Markdown",
|
||||
"text/html": "HTML"
|
||||
},
|
||||
"content_warning": "Betreff (optional)",
|
||||
"default": "Sitze gerade im Hofbräuhaus.",
|
||||
|
@ -69,6 +98,13 @@
|
|||
"private": "Nur Follower - Beitrag nur für Follower sichtbar",
|
||||
"public": "Öffentlich - Beitrag an öffentliche Zeitleisten",
|
||||
"unlisted": "Nicht gelistet - Nicht in öffentlichen Zeitleisten anzeigen"
|
||||
},
|
||||
"direct_warning_to_all": "Dieser Beitrag wird für alle erwähnten Benutzer sichtbar sein.",
|
||||
"direct_warning_to_first_only": "Dieser Beitrag wird für alle Benutzer, die am Anfang der Nachricht erwähnt wurden, sichtbar sein.",
|
||||
"scope_notice": {
|
||||
"public": "Dieser Beitrag wird für alle sichtbar sein",
|
||||
"private": "Dieser Beitrag wird nur für deine Follower sichtbar sein",
|
||||
"unlisted": "Dieser Beitrag wird weder in der öffentlichen Zeitleiste noch im gesamten bekannten Netzwerk sichtbar sein"
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
|
@ -86,8 +122,11 @@
|
|||
"email_required": "darf nicht leer sein",
|
||||
"password_required": "darf nicht leer sein",
|
||||
"password_confirmation_required": "darf nicht leer sein",
|
||||
"password_confirmation_match": "sollte mit dem Passwort identisch sein."
|
||||
}
|
||||
"password_confirmation_match": "sollte mit dem Passwort identisch sein"
|
||||
},
|
||||
"bio_placeholder": "z.B.\nHallo, ich bin Lain.\nIch bin ein Anime Mödchen aus dem vorstädtischen Japan. Du kennst mich vielleicht vom Wired.",
|
||||
"fullname_placeholder": "z.B. Lain Iwakura",
|
||||
"username_placeholder": "z.B. lain"
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "Anhänge",
|
||||
|
@ -99,7 +138,7 @@
|
|||
"background": "Hintergrund",
|
||||
"bio": "Bio",
|
||||
"btnRadius": "Buttons",
|
||||
"cBlue": "Blau (Antworten, Folgt dir)",
|
||||
"cBlue": "Blau (Antworten, folgt dir)",
|
||||
"cGreen": "Grün (Retweet)",
|
||||
"cOrange": "Orange (Favorisieren)",
|
||||
"cRed": "Rot (Abbrechen)",
|
||||
|
@ -115,21 +154,21 @@
|
|||
"data_import_export_tab": "Datenimport/-export",
|
||||
"default_vis": "Standard-Sichtbarkeitsumfang",
|
||||
"delete_account": "Account löschen",
|
||||
"delete_account_description": "Lösche deinen Account und alle deine Nachrichten unwiderruflich.",
|
||||
"delete_account_description": "Lösche deine Daten und deaktiviere deinen Account unwiderruflich.",
|
||||
"delete_account_error": "Es ist ein Fehler beim Löschen deines Accounts aufgetreten. Tritt dies weiterhin auf, wende dich an den Administrator der Instanz.",
|
||||
"delete_account_instructions": "Tippe dein Passwort unten in das Feld ein, um die Löschung deines Accounts zu bestätigen.",
|
||||
"discoverable": "Erlaubnis für automatisches Suchen nach diesem Account",
|
||||
"discoverable": "Erlaube, dass dieser Account in Suchergebnissen auftaucht",
|
||||
"avatar_size_instruction": "Die empfohlene minimale Größe für Avatare ist 150x150 Pixel.",
|
||||
"pad_emoji": "Emojis mit Leerzeichen umrahmen",
|
||||
"export_theme": "Farbschema speichern",
|
||||
"filtering": "Filtern",
|
||||
"filtering_explanation": "Alle Beiträge die diese Wörter enthalten werden ausgeblendet. Ein Wort pro Zeile.",
|
||||
"filtering_explanation": "Alle Beiträge, welche diese Wörter enthalten, werden ausgeblendet. Ein Wort pro Zeile.",
|
||||
"follow_export": "Follower exportieren",
|
||||
"follow_export_button": "Exportiere deine Follows in eine csv-Datei",
|
||||
"follow_export_processing": "In Bearbeitung. Die Liste steht gleich zum herunterladen bereit.",
|
||||
"follow_import": "Followers importieren",
|
||||
"follow_import_error": "Fehler beim importieren der Follower",
|
||||
"follows_imported": "Followers importiert! Die Bearbeitung kann eine Zeit lang dauern.",
|
||||
"follow_import": "Follower importieren",
|
||||
"follow_import_error": "Fehler beim Importieren der Follower",
|
||||
"follows_imported": "Follower importiert! Die Bearbeitung kann einen Moment dauern.",
|
||||
"foreground": "Vordergrund",
|
||||
"general": "Allgemein",
|
||||
"hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden",
|
||||
|
@ -142,7 +181,7 @@
|
|||
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
||||
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
|
||||
"hide_filtered_statuses": "Gefilterte Beiträge verbergen",
|
||||
"import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
|
||||
"import_followers_from_a_csv_file": "Importiere Follower aus einer CSV-Datei",
|
||||
"import_theme": "Farbschema laden",
|
||||
"inputRadius": "Eingabefelder",
|
||||
"checkboxRadius": "Auswahlfelder",
|
||||
|
@ -156,7 +195,7 @@
|
|||
"lock_account_description": "Sperre deinen Account, um neue Follower zu genehmigen oder abzulehnen",
|
||||
"loop_video": "Videos wiederholen",
|
||||
"loop_video_silent_only": "Nur Videos ohne Ton wiederholen (z.B. Mastodons \"gifs\")",
|
||||
"mutes_tab": "Mutes",
|
||||
"mutes_tab": "Stummschaltungen",
|
||||
"play_videos_in_modal": "Videos in größerem Medienfenster abspielen",
|
||||
"use_contain_fit": "Vorschaubilder nicht zuschneiden",
|
||||
"name": "Name",
|
||||
|
@ -221,7 +260,7 @@
|
|||
},
|
||||
"notifications": "Benachrichtigungen",
|
||||
"enable_web_push_notifications": "Web-Pushbenachrichtigungen aktivieren",
|
||||
"style": {
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "Farben beibehalten",
|
||||
"keep_shadows": "Schatten beibehalten",
|
||||
|
@ -329,7 +368,43 @@
|
|||
"checkbox": "Ich habe die Allgemeinen Geschäftsbedingungen überflogen",
|
||||
"link": "ein netter kleiner Link"
|
||||
}
|
||||
}
|
||||
},
|
||||
"app_name": "Anwendungsname",
|
||||
"mfa": {
|
||||
"otp": "OTP",
|
||||
"recovery_codes_warning": "Schreibe dir die Codes auf oder speichere sie an einem sicheren Ort - ansonsten wirst du sie nicht wiederfinden. Wenn du den Zugriff zu deiner 2FA App und die Wiederherstellungs-Codes verlierst, wirst du aus deinem Account ausgeschlossen sein.",
|
||||
"recovery_codes": "Wiederherstellungs-Codes.",
|
||||
"warning_of_generate_new_codes": "Wenn du neue Wiederherstellungs-Codes generierst, werden die alten Codes nicht mehr funktionieren.",
|
||||
"generate_new_recovery_codes": "Generiere neue Wiederherstellungs-Codes",
|
||||
"title": "Zwei-Faktor Authentifizierung",
|
||||
"waiting_a_recovery_codes": "Erhalte Wiederherstellungscodes...",
|
||||
"authentication_methods": "Authentifizierungsmethoden",
|
||||
"scan": {
|
||||
"title": "Scan",
|
||||
"secret_code": "Schlüssel",
|
||||
"desc": "Wenn du deine 2FA App verwendest, scanne diesen QR Code oder gebe den Schlüssel ein:"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "Um 2FA zu aktivieren, gib den Code von deiner 2FA-App ein:"
|
||||
}
|
||||
},
|
||||
"enter_current_password_to_confirm": "Gib dein aktuelles Passwort ein, um deine Identität zu bestätigen",
|
||||
"security": "Sicherheit",
|
||||
"allow_following_move": "Erlaube automatisches Folgen, sobald ein gefolgter Nutzer umzieht",
|
||||
"blocks_imported": "Blocks importiert! Die Verarbeitung wird einen Moment brauchen.",
|
||||
"block_import_error": "Fehler beim Importieren der Blocks",
|
||||
"block_import": "Block Import",
|
||||
"block_export_button": "Exportiere deine Blocks in eine csv Datei",
|
||||
"block_export": "Block Export",
|
||||
"emoji_reactions_on_timeline": "Zeige Emoji-Reaktionen auf der Zeitleiste",
|
||||
"domain_mutes": "Domains",
|
||||
"changed_email": "Email Adresse erfolgreich geändert!",
|
||||
"change_email_error": "Es trat ein Problem auf beim Versuch, deine Email Adresse zu ändern.",
|
||||
"change_email": "Ändere Email",
|
||||
"notification_setting_non_followers": "Nutzer, die dir nicht folgen",
|
||||
"notification_setting_followers": "Nutzer, die dir folgen",
|
||||
"import_blocks_from_a_csv_file": "Importiere Blocks von einer CSV Datei",
|
||||
"accent": "Akzent"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Einklappen",
|
||||
|
@ -352,7 +427,7 @@
|
|||
"follow_again": "Anfrage erneut senden?",
|
||||
"follow_unfollow": "Folgen beenden",
|
||||
"followees": "Folgt",
|
||||
"followers": "Followers",
|
||||
"followers": "Folgende",
|
||||
"following": "Folgst du!",
|
||||
"follows_you": "Folgt dir!",
|
||||
"its_you": "Das bist du!",
|
||||
|
@ -360,7 +435,10 @@
|
|||
"muted": "Stummgeschaltet",
|
||||
"per_day": "pro Tag",
|
||||
"remote_follow": "Folgen",
|
||||
"statuses": "Beiträge"
|
||||
"statuses": "Beiträge",
|
||||
"admin_menu": {
|
||||
"sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein"
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Beiträge"
|
||||
|
@ -376,11 +454,11 @@
|
|||
"favorite": "Favorisieren",
|
||||
"user_settings": "Benutzereinstellungen"
|
||||
},
|
||||
"upload":{
|
||||
"upload": {
|
||||
"error": {
|
||||
"base": "Hochladen fehlgeschlagen.",
|
||||
"file_too_big": "Datei ist zu groß [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "Bitte versuche es später erneut"
|
||||
"base": "Hochladen fehlgeschlagen.",
|
||||
"file_too_big": "Datei ist zu groß [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "Bitte versuche es später erneut"
|
||||
},
|
||||
"file_size_units": {
|
||||
"B": "B",
|
||||
|
@ -409,5 +487,98 @@
|
|||
"password_reset_disabled": "Passwortzurücksetzen deaktiviert. Bitte Administrator kontaktieren.",
|
||||
"password_reset_required": "Passwortzurücksetzen erforderlich",
|
||||
"password_reset_required_but_mailer_is_disabled": "Passwortzurücksetzen wäre erforderlich, ist aber deaktiviert. Bitte Administrator kontaktieren."
|
||||
},
|
||||
"about": {
|
||||
"mrf": {
|
||||
"federation": "Föderation",
|
||||
"mrf_policies": "Aktivierte MRF Richtlinien",
|
||||
"simple": {
|
||||
"simple_policies": "Instanzspezifische Richtlinien",
|
||||
"accept": "Akzeptieren",
|
||||
"reject": "Ablehnen",
|
||||
"reject_desc": "Diese Instanz akzeptiert keine Nachrichten der folgenden Instanzen:",
|
||||
"quarantine": "Quarantäne",
|
||||
"ftl_removal": "Von der Zeitleiste \"Das gesamte bekannte Netzwerk\" entfernen",
|
||||
"media_removal": "Medienentfernung",
|
||||
"media_removal_desc": "Diese Instanz entfernt Medien von den Beiträgen der folgenden Instanzen:",
|
||||
"media_nsfw": "Erzwingen Medien als heikel zu makieren",
|
||||
"media_nsfw_desc": "Diese Instanz makiert die Medien in Beiträgen der folgenden Instanzen als heikel:",
|
||||
"accept_desc": "Diese Instanz akzeptiert nur Nachrichten von den folgenden Instanzen:",
|
||||
"quarantine_desc": "Diese Instanz sendet nur öffentliche Beiträge zu den folgenden Instanzen:",
|
||||
"ftl_removal_desc": "Dieser Instanz entfernt folgende Instanzen von der \"Das gesamte bekannte Netzwerk\" Zeitleiste:"
|
||||
},
|
||||
"keyword": {
|
||||
"keyword_policies": "Keyword Richtlinien",
|
||||
"reject": "Ablehnen",
|
||||
"replace": "Ersetzen",
|
||||
"is_replaced_by": "→",
|
||||
"ftl_removal": "Von der Zeitleiste \"Das gesamte bekannte Netzwerk\" entfernen"
|
||||
},
|
||||
"mrf_policies_desc": "MRF Richtlinien manipulieren das Föderationsverhalten dieser Instanz. Die folgenden Richtlinien sind aktiv:"
|
||||
},
|
||||
"staff": "Mitarbeiter"
|
||||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Stummschalten",
|
||||
"mute_progress": "Wird stummgeschaltet..",
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"unmute_progress": "Stummschaltung wird aufgehoben.."
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Exportieren",
|
||||
"processing": "Verarbeitung läuft, bald wird Du dazu aufgefordert, deine Datei herunterzuladen"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Bild zuschneiden",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"save_without_cropping": "Ohne Zuschneiden speichern"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "Absenden",
|
||||
"success": "Erfolgreich importiert.",
|
||||
"error": "Ein Fehler ist beim Verabeiten der Datei aufgetreten."
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "Zurück",
|
||||
"next": "Weiter"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Umfrage hinzufügen",
|
||||
"add_option": "Option hinzufügen",
|
||||
"option": "Option",
|
||||
"votes": "Stimmen",
|
||||
"vote": "Abstimmen",
|
||||
"type": "Umfragetyp",
|
||||
"multiple_choices": "Mehrere Auswahlmöglichkeiten",
|
||||
"single_choice": "Eine Auswahlmöglichkeit",
|
||||
"expiry": "Alter der Umfrage",
|
||||
"expired": "Die Umfrage endete vor {0}",
|
||||
"not_enough_options": "Zu wenig einzigartige Auswahlmöglichkeiten in der Umfrage",
|
||||
"expires_in": "Die Umfrage endet in {0}"
|
||||
},
|
||||
"emoji": {
|
||||
"stickers": "Sticker",
|
||||
"emoji": "Emoji",
|
||||
"search_emoji": "Nach einem Emoji suchen",
|
||||
"custom": "Benutzerdefinierter Emoji",
|
||||
"keep_open": "Auswahlfenster offen halten",
|
||||
"add_emoji": "Emoji einfügen",
|
||||
"load_all": "Lade alle {emojiAmount} Emoji",
|
||||
"load_all_hint": "Erfolgreich erste {saneAmount} Emoji geladen, alle Emojis zu laden würde Leistungsprobleme hervorrufen.",
|
||||
"unicode": "Unicode Emoji"
|
||||
},
|
||||
"interactions": {
|
||||
"load_older": "Lade ältere Interaktionen",
|
||||
"follows": "Neue Follows",
|
||||
"favs_repeats": "Wiederholungen und Favoriten",
|
||||
"moves": "Benutzer migriert zu"
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Wähle alle"
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"searching_for": "Suche nach",
|
||||
"error": "Nicht gefunden."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@
|
|||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Mute",
|
||||
"mute_progress": "Muting...",
|
||||
"mute_progress": "Muting…",
|
||||
"unmute": "Unmute",
|
||||
"unmute_progress": "Unmuting..."
|
||||
"unmute_progress": "Unmuting…"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Export",
|
||||
|
@ -59,7 +59,10 @@
|
|||
"apply": "Apply",
|
||||
"submit": "Submit",
|
||||
"more": "More",
|
||||
"loading": "Loading…",
|
||||
"generic_error": "An error occured",
|
||||
"error_retry": "Please try again",
|
||||
"retry": "Try again",
|
||||
"optional": "optional",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
|
@ -68,7 +71,9 @@
|
|||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"confirm": "Confirm",
|
||||
"verify": "Verify"
|
||||
"verify": "Verify",
|
||||
"close": "Close",
|
||||
"peek": "Peek"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Crop picture",
|
||||
|
@ -94,9 +99,9 @@
|
|||
"enter_recovery_code": "Enter a recovery code",
|
||||
"enter_two_factor_code": "Enter a two-factor code",
|
||||
"recovery_code": "Recovery code",
|
||||
"heading" : {
|
||||
"totp" : "Two-factor authentication",
|
||||
"recovery" : "Two-factor recovery"
|
||||
"heading": {
|
||||
"totp": "Two-factor authentication",
|
||||
"recovery": "Two-factor recovery"
|
||||
}
|
||||
},
|
||||
"media_modal": {
|
||||
|
@ -121,9 +126,10 @@
|
|||
"preferences": "Preferences"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Unknown status, searching for it...",
|
||||
"broken_favorite": "Unknown status, searching for it…",
|
||||
"favorited_you": "favorited your status",
|
||||
"followed_you": "followed you",
|
||||
"follow_request": "wants to follow you",
|
||||
"load_older": "Load older notifications",
|
||||
"notifications": "Notifications",
|
||||
"read": "Read!",
|
||||
|
@ -225,17 +231,17 @@
|
|||
"security": "Security",
|
||||
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "Setup OTP",
|
||||
"wait_pre_setup_otp" : "presetting OTP",
|
||||
"confirm_and_enable" : "Confirm & enable OTP",
|
||||
"otp": "OTP",
|
||||
"setup_otp": "Setup OTP",
|
||||
"wait_pre_setup_otp": "presetting OTP",
|
||||
"confirm_and_enable": "Confirm & enable OTP",
|
||||
"title": "Two-factor Authentication",
|
||||
"generate_new_recovery_codes" : "Generate new recovery codes",
|
||||
"warning_of_generate_new_codes" : "When you generate new recovery codes, your old codes won’t work anymore.",
|
||||
"recovery_codes" : "Recovery 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.",
|
||||
"authentication_methods" : "Authentication methods",
|
||||
"generate_new_recovery_codes": "Generate new recovery codes",
|
||||
"warning_of_generate_new_codes": "When you generate new recovery codes, your old codes won’t work anymore.",
|
||||
"recovery_codes": "Recovery 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.",
|
||||
"authentication_methods": "Authentication methods",
|
||||
"scan": {
|
||||
"title": "Scan",
|
||||
"desc": "Using your two-factor app, scan this QR code or enter text key:",
|
||||
|
@ -277,10 +283,11 @@
|
|||
"current_avatar": "Your current avatar",
|
||||
"current_password": "Current password",
|
||||
"current_profile_banner": "Your current profile banner",
|
||||
"mutes_and_blocks": "Mutes and Blocks",
|
||||
"data_import_export_tab": "Data Import / Export",
|
||||
"default_vis": "Default visibility scope",
|
||||
"delete_account": "Delete Account",
|
||||
"delete_account_description": "Permanently delete your account and all your messages.",
|
||||
"delete_account_description": "Permanently delete your data and deactivate your account.",
|
||||
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
|
||||
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
|
||||
"discoverable": "Allow discovery of this account in search results and other services",
|
||||
|
@ -394,7 +401,7 @@
|
|||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||
"tooltipRadius": "Tooltips/alerts",
|
||||
"type_domains_to_mute": "Type in domains to mute",
|
||||
"type_domains_to_mute": "Search domains to mute",
|
||||
"upload_a_photo": "Upload a photo",
|
||||
"user_settings": "User Settings",
|
||||
"values": {
|
||||
|
@ -404,11 +411,14 @@
|
|||
"fun": "Fun",
|
||||
"greentext": "Meme arrows",
|
||||
"notifications": "Notifications",
|
||||
"notification_setting_filters": "Filters",
|
||||
"notification_setting": "Receive notifications from:",
|
||||
"notification_setting_follows": "Users you follow",
|
||||
"notification_setting_non_follows": "Users you do not follow",
|
||||
"notification_setting_followers": "Users who follow you",
|
||||
"notification_setting_non_followers": "Users who do not follow you",
|
||||
"notification_setting_privacy": "Privacy",
|
||||
"notification_setting_privacy_option": "Hide the sender and contents of push notifications",
|
||||
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
|
||||
"notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
|
||||
"enable_web_push_notifications": "Enable web push notifications",
|
||||
|
@ -429,7 +439,7 @@
|
|||
"use_source": "New version",
|
||||
"help": {
|
||||
"upgraded_from_v2": "PleromaFE has been upgraded, theme could look a little bit different than you remember.",
|
||||
"v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.",
|
||||
"v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsistencies.",
|
||||
"future_version_imported": "File you imported was made in newer version of FE.",
|
||||
"older_version_imported": "File you imported was made in older version of FE.",
|
||||
"snapshot_present": "Theme snapshot is loaded, so all values are overriden. You can load theme's actual data instead.",
|
||||
|
@ -615,7 +625,11 @@
|
|||
"reply_to": "Reply to",
|
||||
"replies_list": "Replies:",
|
||||
"mute_conversation": "Mute conversation",
|
||||
"unmute_conversation": "Unmute conversation"
|
||||
"unmute_conversation": "Unmute conversation",
|
||||
"status_unavailable": "Status unavailable",
|
||||
"copy_link": "Copy link to status",
|
||||
"thread_muted": "Thread muted",
|
||||
"thread_muted_and_words": ", has words:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Approve",
|
||||
|
@ -645,11 +659,11 @@
|
|||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"unblock": "Unblock",
|
||||
"unblock_progress": "Unblocking...",
|
||||
"block_progress": "Blocking...",
|
||||
"unblock_progress": "Unblocking…",
|
||||
"block_progress": "Blocking…",
|
||||
"unmute": "Unmute",
|
||||
"unmute_progress": "Unmuting...",
|
||||
"mute_progress": "Muting...",
|
||||
"unmute_progress": "Unmuting…",
|
||||
"mute_progress": "Muting…",
|
||||
"hide_repeats": "Hide repeats",
|
||||
"show_repeats": "Show repeats",
|
||||
"admin_menu": {
|
||||
|
@ -696,9 +710,11 @@
|
|||
"reply": "Reply",
|
||||
"favorite": "Favorite",
|
||||
"add_reaction": "Add Reaction",
|
||||
"user_settings": "User Settings"
|
||||
"user_settings": "User Settings",
|
||||
"accept_follow_request": "Accept follow request",
|
||||
"reject_follow_request": "Reject follow request"
|
||||
},
|
||||
"upload":{
|
||||
"upload": {
|
||||
"error": {
|
||||
"base": "Upload failed.",
|
||||
"file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
|
|
|
@ -57,12 +57,12 @@
|
|||
"enter_recovery_code": "Inserta el código de recuperación",
|
||||
"enter_two_factor_code": "Inserta el código de dos factores",
|
||||
"recovery_code": "Código de recuperación",
|
||||
"heading" : {
|
||||
"totp" : "Autenticación de dos factores",
|
||||
"recovery" : "Recuperación de dos factores"
|
||||
"heading": {
|
||||
"totp": "Autenticación de dos factores",
|
||||
"recovery": "Recuperación de dos factores"
|
||||
}
|
||||
},
|
||||
"media_modal": {
|
||||
"media_modal": {
|
||||
"previous": "Anterior",
|
||||
"next": "Siguiente"
|
||||
},
|
||||
|
@ -103,7 +103,7 @@
|
|||
"single_choice": "Elección única",
|
||||
"multiple_choices": "Elección múltiple",
|
||||
"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}",
|
||||
"not_enough_options": "Muy pocas opciones únicas en la encuesta"
|
||||
},
|
||||
|
@ -137,7 +137,7 @@
|
|||
},
|
||||
"content_warning": "Tema (opcional)",
|
||||
"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.",
|
||||
"posting": "Publicando",
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
|
@ -173,7 +173,7 @@
|
|||
"password_confirmation_match": "la contraseña no coincide"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"selectable_list": {
|
||||
"select_all": "Seleccionar todo"
|
||||
},
|
||||
"settings": {
|
||||
|
@ -181,17 +181,17 @@
|
|||
"security": "Seguridad",
|
||||
"enter_current_password_to_confirm": "Introduce la contraseña actual para confirmar tu identidad",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "Configurar OTP",
|
||||
"wait_pre_setup_otp" : "preconfiguración OTP",
|
||||
"confirm_and_enable" : "Confirmar y habilitar OTP",
|
||||
"otp": "OTP",
|
||||
"setup_otp": "Configurar OTP",
|
||||
"wait_pre_setup_otp": "preconfiguración OTP",
|
||||
"confirm_and_enable": "Confirmar y habilitar OTP",
|
||||
"title": "Autentificación de dos factores",
|
||||
"generate_new_recovery_codes" : "Generar códigos de recuperación nuevos",
|
||||
"warning_of_generate_new_codes" : "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.",
|
||||
"recovery_codes" : "Códigos de recuperación.",
|
||||
"generate_new_recovery_codes": "Generar códigos de recuperación nuevos",
|
||||
"warning_of_generate_new_codes": "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.",
|
||||
"recovery_codes": "Códigos de recuperación.",
|
||||
"waiting_a_recovery_codes": "Recibiendo códigos de respaldo",
|
||||
"recovery_codes_warning" : "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.",
|
||||
"authentication_methods" : "Métodos de autentificación",
|
||||
"recovery_codes_warning": "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.",
|
||||
"authentication_methods": "Métodos de autentificación",
|
||||
"scan": {
|
||||
"title": "Escanear",
|
||||
"desc": "Usando su aplicación de dos factores, escanee este código QR o ingrese la clave de texto:",
|
||||
|
@ -210,7 +210,7 @@
|
|||
"background": "Fondo",
|
||||
"bio": "Biografía",
|
||||
"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_error": "Error importando la lista de usuarios bloqueados",
|
||||
"blocks_imported": "¡Lista de usuarios bloqueados importada! El procesado puede tardar un poco.",
|
||||
|
@ -222,7 +222,7 @@
|
|||
"cRed": "Rojo (Cancelar)",
|
||||
"change_password": "Cambiar 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",
|
||||
"composing": "Redactando",
|
||||
"confirm_new_password": "Confirmar la nueva contraseña",
|
||||
|
@ -286,7 +286,7 @@
|
|||
"notification_visibility_repeats": "Repeticiones (Repeats)",
|
||||
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
|
||||
"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_followers_description": "No mostrar quién me sigue",
|
||||
"hide_follows_count_description": "No mostrar el número de cuentas que sigo",
|
||||
|
@ -305,7 +305,7 @@
|
|||
"profile_background": "Fondo del Perfil",
|
||||
"profile_banner": "Cabecera del 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",
|
||||
"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",
|
||||
|
@ -337,7 +337,7 @@
|
|||
"theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación. Use el botón \"Borrar todo\" para deshacer los cambios.",
|
||||
"theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón por encima para obtener información más detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.",
|
||||
"tooltipRadius": "Información/alertas",
|
||||
"upload_a_photo": "Subir una foto",
|
||||
"upload_a_photo": "Subir una foto",
|
||||
"user_settings": "Ajustes del Usuario",
|
||||
"values": {
|
||||
"false": "no",
|
||||
|
@ -583,7 +583,7 @@
|
|||
"profile_does_not_exist": "Lo sentimos, este perfil no existe.",
|
||||
"profile_loading_error": "Lo sentimos, hubo un error al cargar este perfil."
|
||||
},
|
||||
"user_reporting": {
|
||||
"user_reporting": {
|
||||
"title": "Reportando a {0}",
|
||||
"add_comment_description": "El informe será enviado a los moderadores de su instancia. Puedes proporcionar una explicación de por qué estás reportando esta cuenta a continuación:",
|
||||
"additional_comments": "Comentarios adicionales",
|
||||
|
@ -603,7 +603,7 @@
|
|||
"favorite": "Favorito",
|
||||
"user_settings": "Ajustes de usuario"
|
||||
},
|
||||
"upload":{
|
||||
"upload": {
|
||||
"error": {
|
||||
"base": "Subida fallida.",
|
||||
"file_too_big": "Archivo demasiado grande [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
|
@ -635,4 +635,4 @@
|
|||
"too_many_requests": "Has alcanzado el límite de intentos, vuelve a intentarlo más tarde.",
|
||||
"password_reset_disabled": "El restablecimiento de contraseñas está deshabilitado. Póngase en contacto con el administrador de su instancia."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
396
src/i18n/et.json
396
src/i18n/et.json
|
@ -4,7 +4,19 @@
|
|||
"find_user": "Otsi kasutajaid"
|
||||
},
|
||||
"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": "Logi sisse",
|
||||
|
@ -12,29 +24,95 @@
|
|||
"password": "Parool",
|
||||
"placeholder": "nt lain",
|
||||
"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": {
|
||||
"mentions": "Mainimised",
|
||||
"public_tl": "Avalik 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": {
|
||||
"followed_you": "alustas sinu jälgimist",
|
||||
"notifications": "Teavitused",
|
||||
"read": "Loe!"
|
||||
"notifications": "Teated",
|
||||
"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": {
|
||||
"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": {
|
||||
"bio": "Bio",
|
||||
"email": "E-post",
|
||||
"fullname": "Kuvatav nimi",
|
||||
"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": {
|
||||
"attachments": "Manused",
|
||||
|
@ -44,7 +122,7 @@
|
|||
"current_avatar": "Sinu praegune profiilipilt",
|
||||
"current_profile_banner": "Praegune profiilibänner",
|
||||
"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_tl": "Peida manused ajajoonel",
|
||||
"name": "Nimi",
|
||||
|
@ -58,7 +136,201 @@
|
|||
"set_new_profile_banner": "Vali uus profiilibänner",
|
||||
"settings": "Sätted",
|
||||
"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": {
|
||||
"conversation": "Vestlus",
|
||||
|
@ -79,5 +351,111 @@
|
|||
"muted": "Vaigistatud",
|
||||
"per_day": "päevas",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
450
src/i18n/fi.json
450
src/i18n/fi.json
|
@ -19,7 +19,16 @@
|
|||
"apply": "Aseta",
|
||||
"submit": "Lähetä",
|
||||
"more": "Lisää",
|
||||
"generic_error": "Virhe tapahtui"
|
||||
"generic_error": "Virhe tapahtui",
|
||||
"optional": "valinnainen",
|
||||
"show_more": "Näytä lisää",
|
||||
"show_less": "Näytä vähemmän",
|
||||
"dismiss": "Sulje",
|
||||
"cancel": "Peruuta",
|
||||
"disable": "Poista käytöstä",
|
||||
"confirm": "Hyväksy",
|
||||
"verify": "Varmenna",
|
||||
"enable": "Ota käyttöön"
|
||||
},
|
||||
"login": {
|
||||
"login": "Kirjaudu sisään",
|
||||
|
@ -28,7 +37,16 @@
|
|||
"password": "Salasana",
|
||||
"placeholder": "esim. Seppo",
|
||||
"register": "Rekisteröidy",
|
||||
"username": "Käyttäjänimi"
|
||||
"username": "Käyttäjänimi",
|
||||
"hint": "Kirjaudu sisään liittyäksesi keskusteluun",
|
||||
"authentication_code": "Todennuskoodi",
|
||||
"enter_recovery_code": "Syötä palautuskoodi",
|
||||
"recovery_code": "Palautuskoodi",
|
||||
"heading": {
|
||||
"totp": "Monivaihetodennus",
|
||||
"recovery": "Monivaihepalautus"
|
||||
},
|
||||
"enter_two_factor_code": "Syötä monivaihetodennuskoodi"
|
||||
},
|
||||
"nav": {
|
||||
"about": "Tietoja",
|
||||
|
@ -43,7 +61,9 @@
|
|||
"twkn": "Koko Tunnettu Verkosto",
|
||||
"user_search": "Käyttäjähaku",
|
||||
"who_to_follow": "Seurausehdotukset",
|
||||
"preferences": "Asetukset"
|
||||
"preferences": "Asetukset",
|
||||
"administration": "Ylläpito",
|
||||
"search": "Haku"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Viestiä ei löydetty...",
|
||||
|
@ -54,7 +74,9 @@
|
|||
"read": "Lue!",
|
||||
"repeated_you": "toisti viestisi",
|
||||
"no_more_notifications": "Ei enempää ilmoituksia",
|
||||
"reacted_with": "lisäsi reaktion {0}"
|
||||
"reacted_with": "lisäsi reaktion {0}",
|
||||
"migrated_to": "siirtyi sivulle",
|
||||
"follow_request": "haluaa seurata sinua"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Lisää äänestys",
|
||||
|
@ -68,12 +90,14 @@
|
|||
"expiry": "Äänestyksen kesto",
|
||||
"expires_in": "Päättyy {0} päästä",
|
||||
"expired": "Päättyi {0} sitten",
|
||||
"not_enough_option": "Liian vähän uniikkeja vaihtoehtoja äänestyksessä"
|
||||
"not_enough_option": "Liian vähän uniikkeja vaihtoehtoja äänestyksessä",
|
||||
"not_enough_options": "Liian vähän ainutkertaisia vaihtoehtoja"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Toistot ja tykkäykset",
|
||||
"follows": "Uudet seuraukset",
|
||||
"load_older": "Lataa vanhempia interaktioita"
|
||||
"load_older": "Lataa vanhempia interaktioita",
|
||||
"moves": "Käyttäjien siirtymiset"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Uusi viesti",
|
||||
|
@ -81,7 +105,10 @@
|
|||
"account_not_locked_warning_link": "lukittu",
|
||||
"attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi",
|
||||
"content_type": {
|
||||
"text/plain": "Tavallinen teksti"
|
||||
"text/plain": "Tavallinen teksti",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Aihe (valinnainen)",
|
||||
"default": "Tulin juuri saunasta.",
|
||||
|
@ -92,6 +119,13 @@
|
|||
"private": "Vain-seuraajille - Näkyy vain seuraajillesi",
|
||||
"public": "Julkinen - Näkyy julkisilla aikajanoilla",
|
||||
"unlisted": "Listaamaton - Ei näy julkisilla aikajanoilla"
|
||||
},
|
||||
"direct_warning_to_all": "Tämä viesti näkyy vain viestissä mainituille käyttäjille.",
|
||||
"direct_warning_to_first_only": "Tämä viesti näkyy vain viestin alussa mainituille käyttäjille.",
|
||||
"scope_notice": {
|
||||
"public": "Tämä viesti näkyy kaikille",
|
||||
"private": "Tämä viesti näkyy vain sinun seuraajillesi",
|
||||
"unlisted": "Tämä viesti ei näy Julkisella Aikajanalla tai Koko Tunnettu Verkosto -aikajanalla"
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
|
@ -110,7 +144,10 @@
|
|||
"password_required": "ei voi olla tyhjä",
|
||||
"password_confirmation_required": "ei voi olla tyhjä",
|
||||
"password_confirmation_match": "pitää vastata salasanaa"
|
||||
}
|
||||
},
|
||||
"username_placeholder": "esim. peke",
|
||||
"fullname_placeholder": "esim. Pekka Postaaja",
|
||||
"bio_placeholder": "esim.\nHei, olen Pekka.\nOlen esimerkkikäyttäjä tässä verkostossa."
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "Liitteet",
|
||||
|
@ -151,7 +188,7 @@
|
|||
"follow_import": "Seurausten tuonti",
|
||||
"follow_import_error": "Virhe tuodessa seuraksia",
|
||||
"follows_imported": "Seuraukset tuotu! Niiden käsittely vie hetken.",
|
||||
"foreground": "Korostus",
|
||||
"foreground": "Etuala",
|
||||
"general": "Yleinen",
|
||||
"hide_attachments_in_convo": "Piilota liitteet keskusteluissa",
|
||||
"hide_attachments_in_tl": "Piilota liitteet aikajanalla",
|
||||
|
@ -186,14 +223,14 @@
|
|||
"notification_visibility_mentions": "Maininnat",
|
||||
"notification_visibility_repeats": "Toistot",
|
||||
"notification_visibility_emoji_reactions": "Reaktiot",
|
||||
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
|
||||
"no_rich_text_description": "Älä näytä tekstin muotoilua",
|
||||
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
|
||||
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
|
||||
"oauth_tokens": "OAuth-merkit",
|
||||
"token": "Token",
|
||||
"refresh_token": "Päivitä token",
|
||||
"valid_until": "Voimassa asti",
|
||||
"revoke_token": "Peruuttaa",
|
||||
"revoke_token": "Peruuta",
|
||||
"panelRadius": "Ruudut",
|
||||
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
|
||||
"presets": "Valmiit teemat",
|
||||
|
@ -231,6 +268,228 @@
|
|||
"values": {
|
||||
"false": "pois päältä",
|
||||
"true": "päällä"
|
||||
},
|
||||
"hide_follows_description": "Älä näytä ketä seuraan",
|
||||
"show_moderator_badge": "Näytä Moderaattori-merkki profiilissani",
|
||||
"useStreamingApi": "Vastaanota viestiejä ja ilmoituksia reaaliajassa",
|
||||
"notification_setting_filters": "Suodattimet",
|
||||
"notification_setting": "Vastaanota ilmoituksia seuraavista:",
|
||||
"notification_setting_privacy_option": "Piilota lähettäjä ja sisältö sovelluksen ulkopuolisista ilmoituksista",
|
||||
"enable_web_push_notifications": "Ota käyttöön sovelluksen ulkopuoliset ilmoitukset",
|
||||
"app_name": "Sovelluksen nimi",
|
||||
"security": "Turvallisuus",
|
||||
"mfa": {
|
||||
"otp": "OTP",
|
||||
"setup_otp": "OTP-asetukset",
|
||||
"wait_pre_setup_otp": "esiasetetaan OTP:ta",
|
||||
"confirm_and_enable": "Hyväksy ja käytä OTP",
|
||||
"title": "Monivaihetodennus",
|
||||
"generate_new_recovery_codes": "Luo uudet palautuskoodit",
|
||||
"authentication_methods": "Todennus",
|
||||
"warning_of_generate_new_codes": "Luodessasi uudet palautuskoodit, vanhat koodisi lakkaavat toimimasta.",
|
||||
"recovery_codes": "Palautuskoodit.",
|
||||
"waiting_a_recovery_codes": "Odotetaan palautuskoodeja...",
|
||||
"recovery_codes_warning": "Kirjoita koodit ylös tai tallenna ne turvallisesti, muuten et näe niitä uudestaan. Jos et voi käyttää monivaihetodennusta ja sinulla ei ole palautuskoodeja, et voi enää kirjautua sisään tilillesi.",
|
||||
"scan": {
|
||||
"title": "Skannaa",
|
||||
"secret_code": "Avain",
|
||||
"desc": "Käytä monivaihetodennus-sovellusta skannakksesi tämän QR-kooding, tai syötä avain:"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "Kytkeäksesi päälle monivaihetodennuksen, syötä koodi monivaihetodennussovellksesta:"
|
||||
}
|
||||
},
|
||||
"allow_following_move": "Salli automaattinen seuraaminen kun käyttäjä siirtää tilinsä",
|
||||
"block_export": "Estojen vienti",
|
||||
"block_export_button": "Vie estosi CSV-tiedostoon",
|
||||
"block_import": "Estojen tuonti",
|
||||
"block_import_error": "Virhe tuodessa estoja",
|
||||
"blocks_imported": "Estot tuotu! Käsittely vie hetken.",
|
||||
"blocks_tab": "Estot",
|
||||
"change_email": "Vaihda sähköpostiosoite",
|
||||
"change_email_error": "Virhe vaihtaessa sähköpostiosoitetta.",
|
||||
"changed_email": "Sähköpostiosoite vaihdettu!",
|
||||
"domain_mutes": "Sivut",
|
||||
"avatar_size_instruction": "Suositeltu vähimmäiskoko profiilikuville on 150x150 pikseliä.",
|
||||
"accent": "Korostus",
|
||||
"hide_muted_posts": "Piilota mykistettyjen käyttäjien viestit",
|
||||
"hide_filtered_statuses": "Piilota mykistetyt viestit",
|
||||
"import_blocks_from_a_csv_file": "Tuo estot CSV-tiedostosta",
|
||||
"no_blocks": "Ei estoja",
|
||||
"no_mutes": "Ei mykistyksiä",
|
||||
"notification_visibility_moves": "Käyttäjien siirtymiset",
|
||||
"hide_followers_description": "Älä näytä ketkä seuraavat minua",
|
||||
"hide_follows_count_description": "Älä näytä seurauksien määrää",
|
||||
"hide_followers_count_description": "Älä näytä seuraajien määrää",
|
||||
"show_admin_badge": "Näytä Ylläpitäjä-merkki proofilissani",
|
||||
"autohide_floating_post_button": "Piilota Uusi Viesti -nappi automaattisesti (mobiili)",
|
||||
"search_user_to_block": "Hae estettäviä käyttäjiä",
|
||||
"search_user_to_mute": "Hae mykistettäviä käyttäjiä",
|
||||
"minimal_scopes_mode": "Yksinkertaista näkyvyydenrajauksen vaihtoehdot",
|
||||
"post_status_content_type": "Uuden viestin sisällön muoto",
|
||||
"user_mutes": "Käyttäjät",
|
||||
"useStreamingApiWarning": "(Kokeellinen)",
|
||||
"type_domains_to_mute": "Syötä mykistettäviä sivustoja",
|
||||
"upload_a_photo": "Lataa kuva",
|
||||
"fun": "Hupi",
|
||||
"greentext": "Meeminuolet",
|
||||
"notifications": "Ilmoitukset",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"save_load_hint": "\"Säilytä\" asetukset säilyttävät tällä hetkellä asetetut asetukset valittaessa tai ladatessa teemaa, se myös tallentaa kyseiset asetukset viedessä teemaa. Kun kaikki laatikot ovat tyhjänä, viety teema tallentaa kaiken.",
|
||||
"help": {
|
||||
"older_version_imported": "Tuomasi tiedosto on luotu vanhemmalla versiolla.",
|
||||
"fe_upgraded": "PleromaFE:n teemaus päivitetty versiopäivityksen yhteydessä.",
|
||||
"migration_snapshot_ok": "Varmuuden vuoksi teeman kaappaus ladattu. Voit koittaa ladata teeman sisällön.",
|
||||
"migration_napshot_gone": "Jostain syystä teeman kaappaus puuttuu, kaikki asiat eivät välttämättä näytä oikealta.",
|
||||
"snapshot_source_mismatch": "Versiot eivät täsmää: todennäköisesti versio vaihdettu vanhempaan ja päivitetty uudestaan, jos vaihdoit teemaa vanhalla versiolla, sinun tulisi käyttää vanhaa versiota, muutoin uutta.",
|
||||
"upgraded_from_v2": "PleromaFE on päivitetty, teemasi saattaa näyttää erilaiselta kuin muistat.",
|
||||
"v2_imported": "Tuomasi tiedosto on luotu vanhemmalla versiolla. Yhteensopivuus ei välttämättä ole täydellinen.",
|
||||
"future_version_imported": "Tuomasi tiedosto on luotu uudemmalla versiolla.",
|
||||
"snapshot_present": "Teeman kaappaus ladattu, joten kaikki arvot ovat ylikirjoitettu. Voit sen sijaan ladata teeman sisällön.",
|
||||
"snapshot_missing": "Teeman kaappausta ei tiedostossa, joten se voi näyttää erilaiselta kuin suunniteltu.",
|
||||
"fe_downgraded": "PleromaFE:n versio vaihtunut vanhempaan."
|
||||
},
|
||||
"keep_color": "Säilytä värit",
|
||||
"keep_shadows": "Säilytä varjot",
|
||||
"keep_opacity": "Säilytä läpinäkyvyys",
|
||||
"keep_roundness": "Säilytä pyöristys",
|
||||
"keep_fonts": "Säilytä fontit",
|
||||
"reset": "Palauta",
|
||||
"clear_all": "Tyhjennä kaikki",
|
||||
"clear_opacity": "Tyhjennä läpinäkyvyys",
|
||||
"load_theme": "Lataa teema",
|
||||
"keep_as_is": "Pidä sellaisenaan",
|
||||
"use_snapshot": "Vanha",
|
||||
"use_source": "Uusi"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"selectedPost": "Valittu viesti",
|
||||
"_tab_label": "Edistynyt",
|
||||
"alert": "Varoituksen tausta",
|
||||
"alert_error": "Virhe",
|
||||
"alert_warning": "Varoitus",
|
||||
"alert_neutral": "Neutraali",
|
||||
"post": "Viestit/Käyttäjien kuvaukset",
|
||||
"badge": "Merkin tausta",
|
||||
"badge_notification": "Ilmoitus",
|
||||
"panel_header": "Ruudun otsikko",
|
||||
"top_bar": "Yläpalkki",
|
||||
"borders": "Reunat",
|
||||
"buttons": "Napit",
|
||||
"inputs": "Syöttökentät",
|
||||
"faint_text": "Häivytetty teksti",
|
||||
"underlay": "Taustapeite",
|
||||
"poll": "Äänestyksen kuvaaja",
|
||||
"icons": "Ikonit",
|
||||
"highlight": "Korostetut elementit",
|
||||
"pressed": "Painettu",
|
||||
"selectedMenu": "Valikon valinta",
|
||||
"disabled": "Pois käytöstä",
|
||||
"toggled": "Kytketty",
|
||||
"tabs": "Välilehdet",
|
||||
"popover": "Työkaluvinkit, valikot, ponnahdusviestit"
|
||||
},
|
||||
"common": {
|
||||
"color": "Väri",
|
||||
"opacity": "Läpinäkyvyys",
|
||||
"contrast": {
|
||||
"level": {
|
||||
"aaa": "saavuttaa AAA-tason (suositeltu)",
|
||||
"aa": "saavuttaa AA-tason (minimi)",
|
||||
"bad": "ei saavuta mitään helppokäyttöisyyssuosituksia"
|
||||
},
|
||||
"hint": "Kontrastisuhde on {ratio}, se {level} {context}",
|
||||
"context": {
|
||||
"18pt": "suurella (18pt+) tekstillä",
|
||||
"text": "tekstillä"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_colors": {
|
||||
"_tab_label": "Yleinen",
|
||||
"main": "Yleiset värit",
|
||||
"foreground_hint": "Löydät \"Edistynyt\"-välilehdeltä tarkemmat asetukset",
|
||||
"rgbo": "Ikonit, korostukset, merkit"
|
||||
},
|
||||
"shadows": {
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "Varoitus, tämä varjo käyttää aina {0} kun selain tukee sitä.",
|
||||
"avatar_inset": "Huom. sisennettyjen ja ei-sisennettyjen varjojen yhdistelmät saattavat luoda ei-odotettuja lopputuloksia läpinäkyvillä profiilikuvilla.",
|
||||
"drop_shadow_syntax": "{0} ei tue {1} parametria ja {2} avainsanaa.",
|
||||
"spread_zero": "Varjot joiden levitys > 0 näyttävät samalta kuin se olisi nolla",
|
||||
"inset_classic": "Sisennetyt varjot käyttävät {0}"
|
||||
},
|
||||
"components": {
|
||||
"buttonPressedHover": "Nappi (painettu ja kohdistettu)",
|
||||
"panel": "Ruutu",
|
||||
"panelHeader": "Ruudun otsikko",
|
||||
"topBar": "Yläpalkki",
|
||||
"avatar": "Profiilikuva (profiilinäkymässä)",
|
||||
"avatarStatus": "Profiilikuva (viestin yhtyedessä)",
|
||||
"popup": "Ponnahdusviestit ja työkaluvinkit",
|
||||
"button": "Nappi",
|
||||
"buttonHover": "Nappi (kohdistus)",
|
||||
"buttonPressed": "Nappi (painettu)",
|
||||
"input": "Syöttökenttä"
|
||||
},
|
||||
"hintV3": "Voit käyttää {0} merkintää varjoille käyttääksesi väriä toisesta asetuksesta.",
|
||||
"_tab_label": "Valo ja varjostus",
|
||||
"component": "Komponentti",
|
||||
"override": "Ylikirjoita",
|
||||
"shadow_id": "Varjo #{value}",
|
||||
"blur": "Sumennus",
|
||||
"spread": "Levitys",
|
||||
"inset": "Sisennys"
|
||||
},
|
||||
"fonts": {
|
||||
"help": "Valitse fontti käyttöliittymälle. \"Oma\"-vaihtohdolle on syötettävä fontin nimi tarkalleen samana kuin se on järjestelmässäsi.",
|
||||
"_tab_label": "Fontit",
|
||||
"components": {
|
||||
"interface": "Käyttöliittymä",
|
||||
"input": "Syöttökentät",
|
||||
"post": "Viestin teksti",
|
||||
"postCode": "Tasavälistetty teksti viestissä"
|
||||
},
|
||||
"family": "Fontin nimi",
|
||||
"size": "Koko (pikseleissä)",
|
||||
"weight": "Painostus (paksuus)",
|
||||
"custom": "Oma"
|
||||
},
|
||||
"preview": {
|
||||
"input": "Tulin juuri saunasta.",
|
||||
"header": "Esikatselu",
|
||||
"content": "Sisältö",
|
||||
"error": "Esimerkkivirhe",
|
||||
"button": "Nappi",
|
||||
"text": "Vähän lisää {0} ja {1}",
|
||||
"mono": "sisältöä",
|
||||
"faint_link": "manuaali",
|
||||
"fine_print": "Lue meidän {0} vaikka huvin vuoksi!",
|
||||
"header_faint": "Tämä on OK",
|
||||
"checkbox": "Olen silmäillyt käyttöehdot",
|
||||
"link": "kiva linkki"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Pyöristys"
|
||||
}
|
||||
},
|
||||
"enter_current_password_to_confirm": "Syötä nykyinen salasanasi todentaaksesi henkilöllisyytesi",
|
||||
"discoverable": "Salli tilisi näkyvyys hakukoneisiin ja muihin palveluihin",
|
||||
"pad_emoji": "Välistä emojit välilyönneillä lisätessäsi niitä valitsimesta",
|
||||
"mutes_tab": "Mykistykset",
|
||||
"new_email": "Uusi sähköpostiosoite",
|
||||
"notification_setting_follows": "Käyttäjät joita seuraat",
|
||||
"notification_setting_non_follows": "Käyttäjät joita et seuraa",
|
||||
"notification_setting_followers": "Käyttäjät jotka seuraavat sinua",
|
||||
"notification_setting_non_followers": "Käyttäjät jotka eivät seuraa sinua",
|
||||
"notification_setting_privacy": "Yksityisyys",
|
||||
"notification_mutes": "Jos et halua ilmoituksia joltain käyttäjältä, käytä mykistystä.",
|
||||
"notification_blocks": "Estäminen pysäyttää kaikki ilmoitukset käyttäjältä ja poistaa seurauksen.",
|
||||
"version": {
|
||||
"title": "Versio",
|
||||
"backend_version": "Palvelimen versio",
|
||||
"frontend_version": "Käyttöliittymän versio"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
@ -252,8 +511,8 @@
|
|||
"months": "{0} kuukautta",
|
||||
"month_short": "{0}kk",
|
||||
"months_short": "{0}kk",
|
||||
"now": "nyt",
|
||||
"now_short": "juuri nyt",
|
||||
"now": "juuri nyt",
|
||||
"now_short": "nyt",
|
||||
"second": "{0} sekunti",
|
||||
"seconds": "{0} sekuntia",
|
||||
"second_short": "{0}s",
|
||||
|
@ -276,7 +535,8 @@
|
|||
"repeated": "toisti",
|
||||
"show_new": "Näytä uudet",
|
||||
"up_to_date": "Ajantasalla",
|
||||
"no_more_statuses": "Ei enempää viestejä"
|
||||
"no_more_statuses": "Ei enempää viestejä",
|
||||
"no_statuses": "Ei viestejä"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "Tykkäykset",
|
||||
|
@ -288,8 +548,10 @@
|
|||
"delete_confirm": "Haluatko varmasti postaa viestin?",
|
||||
"reply_to": "Vastaus",
|
||||
"replies_list": "Vastaukset:",
|
||||
"mute_conversation": "Hiljennä keskustelu",
|
||||
"unmute_conversation": "Poista hiljennys"
|
||||
"mute_conversation": "Mykistä keskustelu",
|
||||
"unmute_conversation": "Poista mykistys",
|
||||
"status_unavailable": "Viesti ei saatavissa",
|
||||
"copy_link": "Kopioi linkki"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Hyväksy",
|
||||
|
@ -298,7 +560,7 @@
|
|||
"deny": "Älä hyväksy",
|
||||
"follow": "Seuraa",
|
||||
"follow_sent": "Pyyntö lähetetty!",
|
||||
"follow_progress": "Pyydetään...",
|
||||
"follow_progress": "Pyydetään…",
|
||||
"follow_again": "Lähetä pyyntö uudestaan",
|
||||
"follow_unfollow": "Älä seuraa",
|
||||
"followees": "Seuraa",
|
||||
|
@ -306,14 +568,50 @@
|
|||
"following": "Seuraat!",
|
||||
"follows_you": "Seuraa sinua!",
|
||||
"its_you": "Sinun tili!",
|
||||
"mute": "Hiljennä",
|
||||
"muted": "Hiljennetty",
|
||||
"mute": "Mykistä",
|
||||
"muted": "Mykistetty",
|
||||
"per_day": "päivässä",
|
||||
"remote_follow": "Seuraa muualta",
|
||||
"statuses": "Viestit"
|
||||
"statuses": "Viestit",
|
||||
"hidden": "Piilotettu",
|
||||
"media": "Media",
|
||||
"block_progress": "Estetään...",
|
||||
"admin_menu": {
|
||||
"grant_admin": "Anna Ylläpitöoikeudet",
|
||||
"force_nsfw": "Merkitse kaikki viestit NSFW:nä",
|
||||
"disable_any_subscription": "Estä käyttäjän seuraaminen",
|
||||
"moderation": "Moderaatio",
|
||||
"revoke_admin": "Poista Ylläpitöoikeudet",
|
||||
"grant_moderator": "Anna Moderaattorioikeudet",
|
||||
"revoke_moderator": "Poista Moderaattorioikeudet",
|
||||
"activate_account": "Aktivoi tili",
|
||||
"deactivate_account": "Deaktivoi tili",
|
||||
"delete_account": "Poista tili",
|
||||
"strip_media": "Poista media viesteistä",
|
||||
"force_unlisted": "Pakota viestit listaamattomiksi",
|
||||
"sandbox": "Pakota viestit vain seuraajille",
|
||||
"disable_remote_subscription": "Estä seuraaminen ulkopuolisilta sivuilta",
|
||||
"quarantine": "Estä käyttäjän viestin federoituminen",
|
||||
"delete_user": "Poista käyttäjä",
|
||||
"delete_user_confirmation": "Oletko aivan varma? Tätä ei voi kumota."
|
||||
},
|
||||
"favorites": "Tykkäykset",
|
||||
"mention": "Mainitse",
|
||||
"report": "Ilmianna",
|
||||
"subscribe": "Tilaa",
|
||||
"unsubscribe": "Poista tilaus",
|
||||
"unblock": "Poista esto",
|
||||
"unblock_progress": "Postetaan estoa...",
|
||||
"unmute": "Poista mykistys",
|
||||
"unmute_progress": "Poistetaan mykistystä...",
|
||||
"mute_progress": "Mykistetään...",
|
||||
"hide_repeats": "Piilota toistot",
|
||||
"show_repeats": "Näytä toistot"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Käyttäjän aikajana"
|
||||
"timeline_title": "Käyttäjän aikajana",
|
||||
"profile_does_not_exist": "Tätä profiilia ei ole.",
|
||||
"profile_loading_error": "Virhe ladatessa profiilia."
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "Lisää",
|
||||
|
@ -324,9 +622,12 @@
|
|||
"repeat": "Toista",
|
||||
"reply": "Vastaa",
|
||||
"favorite": "Tykkää",
|
||||
"user_settings": "Käyttäjäasetukset"
|
||||
"user_settings": "Käyttäjäasetukset",
|
||||
"add_reaction": "Lisää Reaktio",
|
||||
"accept_follow_request": "Hyväksy seurauspyyntö",
|
||||
"reject_follow_request": "Hylkää seurauspyyntö"
|
||||
},
|
||||
"upload":{
|
||||
"upload": {
|
||||
"error": {
|
||||
"base": "Lataus epäonnistui.",
|
||||
"file_too_big": "Tiedosto liian suuri [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
|
@ -339,5 +640,108 @@
|
|||
"GiB": "Gt",
|
||||
"TiB": "Tt"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"mrf": {
|
||||
"keyword": {
|
||||
"keyword_policies": "Avainsanasäännöt",
|
||||
"ftl_removal": "Poistettu \"Koko Tunnettu Verkosto\" -aikajanalta",
|
||||
"reject": "Hylkää",
|
||||
"replace": "Korvaa",
|
||||
"is_replaced_by": "→"
|
||||
},
|
||||
"simple": {
|
||||
"accept": "Hyväksy",
|
||||
"reject": "Hylkää",
|
||||
"quarantine": "Karanteeni",
|
||||
"ftl_removal": "Poisto \"Koko Tunnettu Verkosto\" -aikajanalta",
|
||||
"media_removal": "Media-tiedostojen poisto",
|
||||
"simple_policies": "Palvelinkohtaiset Säännöt",
|
||||
"accept_desc": "Tämä palvelin hyväksyy viestit vain seuraavilta palvelimilta:",
|
||||
"reject_desc": "Tämä palvelin ei hyväksy viestejä seuraavilta palvelimilta:",
|
||||
"quarantine_desc": "Tämä palvelin lähettää vain julkisia viestejä seuraaville palvelimille:",
|
||||
"ftl_removal_desc": "Tämä palvelin poistaa nämä palvelimet \"Koko Tunnettu Verkosto\"-aikajanalta:",
|
||||
"media_removal_desc": "Tämä palvelin postaa mediatiedostot viesteistä seuraavilta palvelimilta:",
|
||||
"media_nsfw": "Pakota Media Arkaluontoiseksi",
|
||||
"media_nsfw_desc": "Tämä palvelin pakottaa mediatiedostot arkaluonteisiksi seuraavilta palvelimilta:"
|
||||
},
|
||||
"federation": "Federaatio",
|
||||
"mrf_policies": "Aktivoidut MRF-säännöt",
|
||||
"mrf_policies_desc": "MRF-säännöt muuttavat federaation toimintaa sivulla. Seuraavat säännöt ovat kytketty päälle:"
|
||||
},
|
||||
"staff": "Henkilökunta"
|
||||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Mykistä",
|
||||
"unmute": "Poista mykistys",
|
||||
"mute_progress": "Mykistetään...",
|
||||
"unmute_progress": "Poistetaan mykistyst..."
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Vie",
|
||||
"processing": "Käsitellään, hetken päästä voit tallentaa tiedoston"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Rajaa kuva",
|
||||
"save": "Tallenna",
|
||||
"save_without_cropping": "Tallenna rajaamatta",
|
||||
"cancel": "Peruuta"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "Hyväksy",
|
||||
"error": "Virhe tapahtui tietoja tuodessa.",
|
||||
"success": "Tuonti onnistui."
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "Edellinen",
|
||||
"next": "Seuraava"
|
||||
},
|
||||
"emoji": {
|
||||
"stickers": "Tarrat",
|
||||
"emoji": "Emoji",
|
||||
"keep_open": "Pidä valitsin auki",
|
||||
"search_emoji": "Hae emojia",
|
||||
"add_emoji": "Lisää emoji",
|
||||
"custom": "Custom-emoji",
|
||||
"load_all": "Ladataan kaikkia {emojiAmount} emojia",
|
||||
"unicode": "Unicode-emoji",
|
||||
"load_all_hint": "Ensimmäiset {saneAmount} emojia ladattu, kaikkien emojien lataaminen voi aiheuttaa hidastelua."
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"remote_user_resolver": "Ulkopuolinen käyttäjä",
|
||||
"searching_for": "Etsitään käyttäjää",
|
||||
"error": "Ei löytynyt."
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Valitse kaikki"
|
||||
},
|
||||
"password_reset": {
|
||||
"check_email": "Tarkista sähköpostisi salasanannollausta varten.",
|
||||
"instruction": "Syötä sähköpostiosoite tai käyttäjänimi. Lähetämme linkin salasanan nollausta varten.",
|
||||
"password_reset_disabled": "Salasanan nollaus ei käytössä. Ota yhteyttä sivun ylläpitäjään.",
|
||||
"password_reset_required_but_mailer_is_disabled": "Sinun täytyy vaihtaa salasana, mutta salasanan nollaus on pois käytöstä. Ota yhteyttä sivun ylläpitäjään.",
|
||||
"forgot_password": "Unohditko salasanan?",
|
||||
"password_reset": "Salasanan nollaus",
|
||||
"placeholder": "Sähköpostiosoite tai käyttäjänimi",
|
||||
"return_home": "Palaa etusivulle",
|
||||
"not_found": "Sähköpostiosoitetta tai käyttäjänimeä ei löytynyt.",
|
||||
"too_many_requests": "Olet käyttänyt kaikki yritykset, yritä uudelleen myöhemmin.",
|
||||
"password_reset_required": "Sinun täytyy vaihtaa salasana kirjautuaksesi."
|
||||
},
|
||||
"user_reporting": {
|
||||
"add_comment_description": "Tämä raportti lähetetään sivun moderaattoreille. Voit antaa selityksen miksi ilmiannoit tilin:",
|
||||
"title": "Ilmiannetaan {0}",
|
||||
"additional_comments": "Lisäkommentit",
|
||||
"forward_description": "Tämä tili on toiselta palvelimelta. Lähetä kopio ilmiannosta sinnekin?",
|
||||
"forward_to": "Lähetä eteenpäin: {0}",
|
||||
"submit": "Lähetä",
|
||||
"generic_error": "Virhe käsitellessä pyyntöä."
|
||||
},
|
||||
"search": {
|
||||
"people": "Käyttäjät",
|
||||
"hashtags": "Aihetunnisteet",
|
||||
"people_talking": "{0} käyttäjää puhuvat",
|
||||
"person_talking": "{0} käyttäjä puhuu",
|
||||
"no_results": "Ei tuloksia"
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue