Merge branch 'develop' of git.pleroma.social:pleroma/pleroma-fe into develop
This commit is contained in:
commit
77bd79100b
110 changed files with 9373 additions and 7855 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -4,24 +4,36 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## [Unreleased]
|
||||
### Changed
|
||||
- Greentext now has separate color slot for it
|
||||
- Removed the use of with_move parameters when fetching notifications
|
||||
|
||||
### Fixed
|
||||
- Weird bug related to post being sent seemingly after pasting with keyboard (hopefully)
|
||||
- Multiple issues with muted statuses/notifications
|
||||
|
||||
## [Unreleased patch]
|
||||
### Add
|
||||
- Added private notifications option for push notifications
|
||||
- 'Copy link' button for statuses (in the ellipsis menu)
|
||||
- Autocomplete domains from list of known instances
|
||||
|
||||
### Changed
|
||||
- Registration page no longer requires email if the server is configured not to require it
|
||||
- Change heart to thumbs up in reaction picker
|
||||
- Close the media modal on navigation events
|
||||
- Add colons to the emoji alt text, to make them copyable
|
||||
- Add better visual indication for drag-and-drop for files
|
||||
|
||||
### Fixed
|
||||
- Custom Emoji will display in poll options now.
|
||||
- Status ellipsis menu closes properly when selecting certain options
|
||||
- Cropped images look correct in Chrome
|
||||
- Newlines in the muted words settings work again
|
||||
- Clicking on non-latin hashtags won't open a new window
|
||||
- Uploading and drag-dropping multiple files works correctly now.
|
||||
- Subject field now appears disabled when posting
|
||||
- Fix status ellipsis menu being cut off in notifications column
|
||||
- Fixed autocomplete sometimes not returning the right user when there's already some results
|
||||
|
||||
## [2.0.3] - 2020-05-02
|
||||
### Fixed
|
||||
|
|
11
package.json
11
package.json
|
@ -22,23 +22,20 @@
|
|||
"cropperjs": "^1.4.3",
|
||||
"diff": "^3.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"localforage": "^1.5.0",
|
||||
"object-path": "^0.11.3",
|
||||
"phoenix": "^1.3.0",
|
||||
"portal-vue": "^2.1.4",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"vue": "^2.5.13",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.4",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuelidate": "^0.7.4",
|
||||
"vuex": "^3.0.1",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
|
|
|
@ -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 () {
|
||||
|
@ -117,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 {
|
||||
|
|
|
@ -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"
|
||||
|
@ -125,6 +126,7 @@
|
|||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<SettingsModal />
|
||||
<portal-target name="modal" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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,6 +3,7 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { debounce } from 'lodash'
|
|||
|
||||
const debounceUserSearch = debounce((data, input) => {
|
||||
data.updateUsersList(input)
|
||||
}, 500, { leading: true, trailing: false })
|
||||
}, 500)
|
||||
|
||||
export default data => input => {
|
||||
const firstChar = input[0]
|
||||
|
@ -97,8 +97,8 @@ export const suggestUsers = data => input => {
|
|||
replacement: '@' + screen_name + ' '
|
||||
}))
|
||||
|
||||
// BE search users if there are no matches
|
||||
if (newUsers.length === 0 && data.updateUsersList) {
|
||||
// BE search users to get more comprehensive results
|
||||
if (data.updateUsersList) {
|
||||
debounceUserSearch(data, noPrefix)
|
||||
}
|
||||
return newUsers
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
trigger="click"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
:bound-to="{ x: 'container' }"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
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'
|
||||
|
@ -16,10 +17,11 @@ const Notification = {
|
|||
},
|
||||
props: [ 'notification' ],
|
||||
components: {
|
||||
Status,
|
||||
StatusContent,
|
||||
UserAvatar,
|
||||
UserCard,
|
||||
Timeago
|
||||
Timeago,
|
||||
Status
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -157,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,23 +48,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.status-body {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
a {
|
||||
|
@ -72,11 +70,6 @@
|
|||
color: var(--postFaintLink);
|
||||
}
|
||||
}
|
||||
padding: 0;
|
||||
.media-body {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follow-request-accept {
|
||||
|
|
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>
|
|
@ -17,7 +17,7 @@
|
|||
<span class="result-percentage">
|
||||
{{ percentageForOption(option.votes_count) }}%
|
||||
</span>
|
||||
<span>{{ option.title }}</span>
|
||||
<span v-html="option.title_html"></span>
|
||||
</div>
|
||||
<div
|
||||
class="result-fill"
|
||||
|
|
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
/* eslint-env browser */
|
||||
import { filter, trim } from 'lodash'
|
||||
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
||||
import { extractCommit } from '../../services/version/version.service'
|
||||
import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/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,
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TabSwitcher,
|
||||
StyleSwitcher,
|
||||
InterfaceLanguageSwitcher,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
},
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
frontendVersionLink () {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
},
|
||||
// Getting localized values for instance-default properties
|
||||
...instanceDefaultProperties
|
||||
.filter(key => multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'DefaultValue',
|
||||
function () {
|
||||
return this.$store.getters.instanceDefaultConfig[key]
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
...instanceDefaultProperties
|
||||
.filter(key => !multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'LocalizedValue',
|
||||
function () {
|
||||
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Generating computed values for vuex properties
|
||||
...Object.keys(configDefaultState)
|
||||
.map(key => [key, {
|
||||
get () { return this.$store.getters.mergedConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Special cases (need to transform values or perform actions first)
|
||||
muteWordsString: {
|
||||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}
|
||||
},
|
||||
useStreamingApi: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
set (value) {
|
||||
const promise = value
|
||||
? this.$store.dispatch('enableMastoSockets')
|
||||
: this.$store.dispatch('disableMastoSockets')
|
||||
|
||||
promise.then(() => {
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
||||
}).catch((e) => {
|
||||
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||
this.$store.dispatch('disableMastoSockets')
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
notificationVisibility: {
|
||||
handler (value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'notificationVisibility',
|
||||
value: this.$store.getters.mergedConfig.notificationVisibility
|
||||
})
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default settings
|
|
@ -1,424 +0,0 @@
|
|||
<template>
|
||||
<div class="settings panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('settings.settings') }}
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<template v-if="currentSaveStateNotice">
|
||||
<div
|
||||
v-if="currentSaveStateNotice.error"
|
||||
class="alert error"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_err') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!currentSaveStateNotice.error"
|
||||
class="alert transparent"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_ok') }}
|
||||
</div>
|
||||
</template>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<keep-alive>
|
||||
<tab-switcher>
|
||||
<div :label="$t('settings.general')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher />
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<Checkbox v-model="hideISP">
|
||||
{{ $t('settings.hide_isp') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('nav.timeline') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideMutedPosts">
|
||||
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="collapseMessageWithSubject">
|
||||
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="streaming">
|
||||
{{ $t('settings.streaming') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="pauseOnUnfocused"
|
||||
:disabled="!streaming"
|
||||
>
|
||||
{{ $t('settings.pause_on_unfocused') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useStreamingApi">
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autoLoad">
|
||||
{{ $t('settings.autoload') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hoverPreview">
|
||||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.composing') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="scopeCopy">
|
||||
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="alwaysShowSubjectInput">
|
||||
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
{{ $t('settings.subject_line_behavior') }}
|
||||
<label
|
||||
for="subjectLineBehavior"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="subjectLineBehavior"
|
||||
v-model="subjectLineBehavior"
|
||||
>
|
||||
<option value="email">
|
||||
{{ $t('settings.subject_line_email') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="masto">
|
||||
{{ $t('settings.subject_line_mastodon') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="noop">
|
||||
{{ $t('settings.subject_line_noop') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="postFormats.length > 0">
|
||||
<div>
|
||||
{{ $t('settings.post_status_content_type') }}
|
||||
<label
|
||||
for="postContentType"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="postContentType"
|
||||
v-model="postContentType"
|
||||
>
|
||||
<option
|
||||
v-for="postFormat in postFormats"
|
||||
:key="postFormat"
|
||||
:value="postFormat"
|
||||
>
|
||||
{{ $t(`post_status.content_type["${postFormat}"]`) }}
|
||||
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="minimalScopesMode">
|
||||
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autohideFloatingPostButton">
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="padEmoji">
|
||||
{{ $t('settings.pad_emoji') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.attachments') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<label for="maxThumbnails">
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</label>
|
||||
<input
|
||||
id="maxThumbnails"
|
||||
v-model.number="maxThumbnails"
|
||||
class="number-input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideNsfw">
|
||||
{{ $t('settings.nsfw_clickthrough') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="preloadImage"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.preload_images') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="useOneClickNsfw"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.use_one_click_nsfw') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<Checkbox v-model="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="loopVideo">
|
||||
{{ $t('settings.loop_video') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="loopVideoSilentOnly"
|
||||
:disabled="!loopVideo || !loopSilentAvailable"
|
||||
>
|
||||
{{ $t('settings.loop_video_silent_only') }}
|
||||
</Checkbox>
|
||||
<div
|
||||
v-if="!loopSilentAvailable"
|
||||
class="unavailable"
|
||||
>
|
||||
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="playVideosInModal">
|
||||
{{ $t('settings.play_videos_in_modal') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useContainFit">
|
||||
{{ $t('settings.use_contain_fit') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notifications') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="webPushNotifications">
|
||||
{{ $t('settings.enable_web_push_notifications') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.fun') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="greentext">
|
||||
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.theme')">
|
||||
<div class="setting-item">
|
||||
<style-switcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.filtering')">
|
||||
<div class="setting-item">
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.likes">
|
||||
{{ $t('settings.notification_visibility_likes') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.repeats">
|
||||
{{ $t('settings.notification_visibility_repeats') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.follows">
|
||||
{{ $t('settings.notification_visibility_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.mentions">
|
||||
{{ $t('settings.notification_visibility_mentions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.moves">
|
||||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
<label
|
||||
for="replyVisibility"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="replyVisibility"
|
||||
v-model="replyVisibility"
|
||||
>
|
||||
<option
|
||||
value="all"
|
||||
selected
|
||||
>{{ $t('settings.reply_visibility_all') }}</option>
|
||||
<option value="following">{{ $t('settings.reply_visibility_following') }}</option>
|
||||
<option value="self">{{ $t('settings.reply_visibility_self') }}</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<p>{{ $t('settings.filtering_explanation') }}</p>
|
||||
<textarea
|
||||
id="muteWords"
|
||||
v-model="muteWordsString"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :label="$t('settings.version.title')">
|
||||
<div class="setting-item">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p>{{ $t('settings.version.backend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="backendVersionLink"
|
||||
target="_blank"
|
||||
>{{ backendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>{{ $t('settings.version.frontend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="frontendVersionLink"
|
||||
target="_blank"
|
||||
>{{ frontendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./settings.js">
|
||||
</script>
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
instanceDefaultProperties,
|
||||
multiChoiceProperties,
|
||||
defaultState as configDefaultState
|
||||
} from 'src/modules/config.js'
|
||||
|
||||
const SharedComputedObject = () => ({
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
// Getting localized values for instance-default properties
|
||||
...instanceDefaultProperties
|
||||
.filter(key => multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'DefaultValue',
|
||||
function () {
|
||||
return this.$store.getters.instanceDefaultConfig[key]
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
...instanceDefaultProperties
|
||||
.filter(key => !multiChoiceProperties.includes(key))
|
||||
.map(key => [
|
||||
key + 'LocalizedValue',
|
||||
function () {
|
||||
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
|
||||
}
|
||||
])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Generating computed values for vuex properties
|
||||
...Object.keys(configDefaultState)
|
||||
.map(key => [key, {
|
||||
get () { return this.$store.getters.mergedConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Special cases (need to transform values or perform actions first)
|
||||
useStreamingApi: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
set (value) {
|
||||
const promise = value
|
||||
? this.$store.dispatch('enableMastoSockets')
|
||||
: this.$store.dispatch('disableMastoSockets')
|
||||
|
||||
promise.then(() => {
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
||||
}).catch((e) => {
|
||||
console.error('Failed starting MastoAPI Streaming socket', e)
|
||||
this.$store.dispatch('disableMastoSockets')
|
||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default SharedComputedObject
|
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,8 +21,9 @@
|
|||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../../../_variables.scss';
|
||||
|
||||
.mfa-backup-codes {
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
|
@ -30,4 +31,5 @@
|
|||
.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,6 +126,7 @@
|
|||
>
|
||||
<div class="tab-header">
|
||||
<p>{{ $t('settings.theme_help') }}</p>
|
||||
<div class="tab-header-buttons">
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearOpacity"
|
||||
|
@ -139,6 +140,7 @@
|
|||
{{ $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>
|
||||
<div class="color-item">
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'}">
|
||||
|
|
|
@ -12,7 +12,8 @@ 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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.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 = {
|
||||
|
@ -44,6 +45,12 @@ const Status = {
|
|||
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)
|
||||
|
@ -93,20 +100,42 @@ 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 relationship = this.$store.getters.relationship(this.status.user.id)
|
||||
return !this.unmuted && (
|
||||
(!(this.inProfile && this.status.user.id === this.profileUserId) && relationship.muting) ||
|
||||
(!this.inConversation && this.status.thread_muted) ||
|
||||
this.muteWordHits.length > 0)
|
||||
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
|
||||
},
|
||||
hideFilteredStatuses () {
|
||||
return this.mergedConfig.hideFilteredStatuses
|
||||
|
|
|
@ -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"
|
||||
|
@ -637,20 +658,49 @@ $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;
|
||||
}
|
||||
|
||||
.muteWords {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
a.unmute {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-body {
|
||||
flex: 1;
|
||||
|
|
|
@ -164,11 +164,6 @@ $status-margin: 0.75em;
|
|||
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;
|
||||
|
@ -181,6 +176,11 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.status-content {
|
||||
font-family: var(--postFont, sans-serif);
|
||||
line-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
|
||||
blockquote {
|
||||
margin: 0.2em 0 0.2em 2em;
|
||||
font-style: italic;
|
||||
|
@ -226,7 +226,7 @@ $status-margin: 0.75em;
|
|||
|
||||
.greentext {
|
||||
color: $fallback--cGreen;
|
||||
color: var(--cGreen, $fallback--cGreen);
|
||||
color: var(--postGreentext, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.timeline :not(.panel-disabled) > {
|
||||
|
|
|
@ -23,13 +23,6 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.contain-fit {
|
||||
.still-image {
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.still-image {
|
||||
position: relative;
|
||||
|
@ -38,6 +31,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover canvas {
|
||||
display: none;
|
||||
|
@ -45,8 +39,8 @@
|
|||
|
||||
img {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
object-fit: contain;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&.animated {
|
||||
|
|
|
@ -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,8 +2,145 @@
|
|||
|
||||
.tab-switcher {
|
||||
display: flex;
|
||||
|
||||
.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;
|
||||
min-height: 0px;
|
||||
|
@ -11,52 +148,32 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
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);
|
||||
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;
|
||||
|
||||
|
@ -79,20 +196,41 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after, &::before {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&:not(.active) {
|
||||
&::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-label {
|
||||
padding-left: .3em;
|
||||
padding-bottom: .25em;
|
||||
margin-top: .5em;
|
||||
margin-left: .2em;
|
||||
margin-bottom: .25em;
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
|
||||
@media all and (min-width: 800px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,15 +50,6 @@
|
|||
>
|
||||
{{ user.name }}
|
||||
</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"
|
||||
|
@ -118,7 +109,7 @@
|
|||
type="color"
|
||||
>
|
||||
<label
|
||||
for="style-switcher"
|
||||
for="theme_tab"
|
||||
class="userHighlightSel select"
|
||||
>
|
||||
<select
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
@ -123,6 +124,14 @@ const UserProfile = {
|
|||
onTabSwitch (tab) {
|
||||
this.tab = tab
|
||||
this.$router.replace({ query: { tab } })
|
||||
},
|
||||
linkClicked ({ target }) {
|
||||
if (target.tagName === 'SPAN') {
|
||||
target = target.parentNode
|
||||
}
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -146,6 +155,7 @@ const UserProfile = {
|
|||
FollowerList,
|
||||
FriendList,
|
||||
FollowCard,
|
||||
TabSwitcher,
|
||||
Conversation
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,31 @@
|
|||
:allow-zooming-avatar="true"
|
||||
rounded="top"
|
||||
/>
|
||||
<div
|
||||
v-if="user.fields_html && user.fields_html.length > 0"
|
||||
class="user-profile-fields"
|
||||
>
|
||||
<dl
|
||||
v-for="(field, index) in user.fields_html"
|
||||
:key="index"
|
||||
class="user-profile-field"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<dt
|
||||
:title="user.fields_text[index].name"
|
||||
class="user-profile-field-name"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="field.name"
|
||||
/>
|
||||
<dd
|
||||
:title="user.fields_text[index].value"
|
||||
class="user-profile-field-value"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="field.value"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</dl>
|
||||
</div>
|
||||
<tab-switcher
|
||||
:active-tab="tab"
|
||||
:render-only-focused="true"
|
||||
|
@ -108,11 +133,60 @@
|
|||
<script src="./user_profile.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.user-profile {
|
||||
flex: 2;
|
||||
flex-basis: 500px;
|
||||
|
||||
.user-profile-fields {
|
||||
margin: 0 0.5em;
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
&.emoji {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-profile-field {
|
||||
display: flex;
|
||||
margin: 0.25em auto;
|
||||
max-width: 32em;
|
||||
border: 1px solid var(--border, $fallback--border);
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
|
||||
.user-profile-field-name {
|
||||
flex: 0 1 30%;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
color: var(--lightText);
|
||||
min-width: 120px;
|
||||
border-right: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.user-profile-field-value {
|
||||
flex: 1 1 70%;
|
||||
color: var(--text);
|
||||
margin: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
.user-profile-field-name, .user-profile-field-value {
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 0.5em 1.5em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userlist-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
@ -1,411 +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,
|
||||
mascot: this.$store.state.users.currentUser.mascot,
|
||||
mascotPreview: null,
|
||||
mascotUploadError: 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')
|
||||
this.$store.dispatch('fetchMascot')
|
||||
},
|
||||
components: {
|
||||
StyleSwitcher,
|
||||
ScopeSelector,
|
||||
TabSwitcher,
|
||||
ImageCropper,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
ProgressButton,
|
||||
Importer,
|
||||
Exporter,
|
||||
Mfa,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
return suggestor({ emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
] })
|
||||
},
|
||||
pleromaBackend () {
|
||||
return this.$store.state.instance.pleromaBackend
|
||||
},
|
||||
minimalScopesMode () {
|
||||
return this.$store.state.instance.minimalScopesMode
|
||||
},
|
||||
vis () {
|
||||
return {
|
||||
public: { selected: this.newDefaultScope === 'public' },
|
||||
unlisted: { selected: this.newDefaultScope === 'unlisted' },
|
||||
private: { selected: this.newDefaultScope === 'private' },
|
||||
direct: { selected: this.newDefaultScope === 'direct' }
|
||||
}
|
||||
},
|
||||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
},
|
||||
oauthTokens () {
|
||||
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
|
||||
return {
|
||||
id: oauthToken.id,
|
||||
appName: oauthToken.app_name,
|
||||
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
params: {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
display_name: this.newName,
|
||||
default_scope: this.newDefaultScope,
|
||||
no_rich_text: this.newNoRichText,
|
||||
hide_follows: this.hideFollows,
|
||||
hide_followers: this.hideFollowers,
|
||||
discoverable: this.discoverable,
|
||||
allow_following_move: this.allowFollowingMove,
|
||||
hide_follows_count: this.hideFollowsCount,
|
||||
hide_followers_count: this.hideFollowersCount,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
} }).then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
updateNotificationSettings () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateNotificationSettings({ settings: this.notificationSettings })
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
const img = target.result
|
||||
this[slot + 'Preview'] = img
|
||||
this[slot] = file
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar (cropper, file) {
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar) {
|
||||
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
resolve()
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
|
||||
})
|
||||
}
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
||||
} else {
|
||||
updateAvatar(file)
|
||||
}
|
||||
})
|
||||
},
|
||||
clearUploadError (slot) {
|
||||
this[slot + 'UploadError'] = null
|
||||
},
|
||||
submitBanner () {
|
||||
if (!this.bannerPreview) { return }
|
||||
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.bannerPreview = null
|
||||
})
|
||||
.catch((err) => {
|
||||
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
|
||||
})
|
||||
.then(() => { this.bannerUploading = false })
|
||||
},
|
||||
submitBg () {
|
||||
if (!this.backgroundPreview) { return }
|
||||
let background = this.background
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
||||
if (!data.error) {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.backgroundPreview = null
|
||||
} else {
|
||||
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||
}
|
||||
this.backgroundUploading = false
|
||||
})
|
||||
},
|
||||
submitMascot () {
|
||||
if (!this.mascotPreview) { return }
|
||||
let mascot = this.mascot
|
||||
this.mascotUploading = true
|
||||
this.$store.state.api.backendInteractor.updateMascot({ mascot }).then((data) => {
|
||||
if (!data.error) {
|
||||
this.mascotPreview = null
|
||||
this.$store.commit('updateMascot', data.url)
|
||||
} else {
|
||||
this.mascotUploadError = this.$t('upload.error.base') + ' ' + data.error
|
||||
}
|
||||
this.mascotUploading = false
|
||||
})
|
||||
},
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent (users) {
|
||||
// Get addresses
|
||||
return users.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
},
|
||||
getFollowsContent () {
|
||||
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
getBlocksContent () {
|
||||
return this.$store.state.api.backendInteractor.fetchBlocks()
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
confirmDelete () {
|
||||
this.deletingAccount = true
|
||||
},
|
||||
deleteAccount () {
|
||||
this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push({ name: 'root' })
|
||||
} else {
|
||||
this.deleteAccountError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changePassword () {
|
||||
const params = {
|
||||
password: this.changePasswordInputs[0],
|
||||
newPassword: this.changePasswordInputs[1],
|
||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changePassword(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedPassword = true
|
||||
this.changePasswordError = false
|
||||
this.logout()
|
||||
} else {
|
||||
this.changedPassword = false
|
||||
this.changePasswordError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changeEmail () {
|
||||
const params = {
|
||||
email: this.newEmail,
|
||||
password: this.changeEmailPassword
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changeEmail(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedEmail = true
|
||||
this.changeEmailError = false
|
||||
} else {
|
||||
this.changedEmail = false
|
||||
this.changeEmailError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
activateTab (tabName) {
|
||||
this.activeTab = tabName
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.replace('/')
|
||||
},
|
||||
revokeToken (id) {
|
||||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
}
|
||||
},
|
||||
filterUnblockedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.blocking || userId === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.muting || userId === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
queryUserIds (query) {
|
||||
return this.$store.dispatch('searchUsers', { query })
|
||||
.then((users) => map(users, 'id'))
|
||||
},
|
||||
blockUsers (ids) {
|
||||
return this.$store.dispatch('blockUsers', ids)
|
||||
},
|
||||
unblockUsers (ids) {
|
||||
return this.$store.dispatch('unblockUsers', ids)
|
||||
},
|
||||
muteUsers (ids) {
|
||||
return this.$store.dispatch('muteUsers', ids)
|
||||
},
|
||||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
unmuteDomains (domains) {
|
||||
return this.$store.dispatch('unmuteDomains', domains)
|
||||
},
|
||||
muteDomain () {
|
||||
return this.$store.dispatch('muteDomain', this.newDomainToMute)
|
||||
.then(() => { this.newDomainToMute = '' })
|
||||
},
|
||||
identity (value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserSettings
|
|
@ -1,778 +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 class="setting-item">
|
||||
<h2>{{ $t('settings.mascot') }}</h2>
|
||||
<p>{{ $t('settings.current_mascot') }}</p>
|
||||
<img
|
||||
:src="user.mascot"
|
||||
class="current-mascot"
|
||||
>
|
||||
<p>{{ $t('settings.set_new_mascot') }}</p>
|
||||
<img
|
||||
v-if="mascotPreview"
|
||||
class="mascot"
|
||||
:src="mascotPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
@change="uploadFile('mascot', $event)"
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
v-if="mascotUploading"
|
||||
class=" icon-spin4 animate-spin uploading"
|
||||
/>
|
||||
<button
|
||||
v-else-if="mascotPreview"
|
||||
class="btn btn-default"
|
||||
@click="submitMascot"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="mascotUploadError"
|
||||
class="alert error"
|
||||
>
|
||||
Error: {{ mascotUploadError }}
|
||||
<i
|
||||
class="button-icon icon-cancel"
|
||||
@click="clearUploadError('mascot')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.security_tab')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_email') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_email') }}</p>
|
||||
<input
|
||||
v-model="newEmail"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changeEmailPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="changeEmail"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<p v-if="changedEmail">
|
||||
{{ $t('settings.changed_email') }}
|
||||
</p>
|
||||
<template v-if="changeEmailError !== false">
|
||||
<p>{{ $t('settings.change_email_error') }}</p>
|
||||
<p>{{ changeEmailError }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_password') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[0]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[1]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.confirm_new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[2]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="changePassword"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<p v-if="changedPassword">
|
||||
{{ $t('settings.changed_password') }}
|
||||
</p>
|
||||
<p v-else-if="changePasswordError !== false">
|
||||
{{ $t('settings.change_password_error') }}
|
||||
</p>
|
||||
<p v-if="changePasswordError">
|
||||
{{ changePasswordError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.oauth_tokens') }}</h2>
|
||||
<table class="oauth-tokens">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('settings.app_name') }}</th>
|
||||
<th>{{ $t('settings.valid_until') }}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="oauthToken in oauthTokens"
|
||||
:key="oauthToken.id"
|
||||
>
|
||||
<td>{{ oauthToken.appName }}</td>
|
||||
<td>{{ oauthToken.validUntil }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="revokeToken(oauthToken.id)"
|
||||
>
|
||||
{{ $t('settings.revoke_token') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<mfa />
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.delete_account') }}</h2>
|
||||
<p v-if="!deletingAccount">
|
||||
{{ $t('settings.delete_account_description') }}
|
||||
</p>
|
||||
<div v-if="deletingAccount">
|
||||
<p>{{ $t('settings.delete_account_instructions') }}</p>
|
||||
<p>{{ $t('login.password') }}</p>
|
||||
<input
|
||||
v-model="deleteAccountConfirmPasswordInput"
|
||||
type="password"
|
||||
>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="deleteAccount"
|
||||
>
|
||||
{{ $t('settings.delete_account') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="deleteAccountError !== false">
|
||||
{{ $t('settings.delete_account_error') }}
|
||||
</p>
|
||||
<p v-if="deleteAccountError">
|
||||
{{ deleteAccountError }}
|
||||
</p>
|
||||
<button
|
||||
v-if="!deletingAccount"
|
||||
class="btn btn-default"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="pleromaBackend"
|
||||
:label="$t('settings.notifications')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.follows">
|
||||
{{ $t('settings.notification_setting_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.followers">
|
||||
{{ $t('settings.notification_setting_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_follows">
|
||||
{{ $t('settings.notification_setting_non_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_followers">
|
||||
{{ $t('settings.notification_setting_non_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
||||
<p>
|
||||
<Checkbox v-model="notificationSettings.privacy_option">
|
||||
{{ $t('settings.notification_setting_privacy_option') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="updateNotificationSettings"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="pleromaBackend"
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_import') }}</h2>
|
||||
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importFollows"
|
||||
:success-message="$t('settings.follows_imported')"
|
||||
:error-message="$t('settings.follow_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getFollowsContent"
|
||||
filename="friends.csv"
|
||||
:export-button-label="$t('settings.follow_export_button')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_import') }}</h2>
|
||||
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importBlocks"
|
||||
:success-message="$t('settings.blocks_imported')"
|
||||
:error-message="$t('settings.block_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getBlocksContent"
|
||||
filename="blocks.csv"
|
||||
:export-button-label="$t('settings.block_export_button')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.blocks_tab')">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnblockedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_block')"
|
||||
>
|
||||
<BlockCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<BlockList
|
||||
:refresh="true"
|
||||
:get-key="identity"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => blockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unblockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<BlockCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_blocks') }}
|
||||
</template>
|
||||
</BlockList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<tab-switcher>
|
||||
<div label="Users">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
<MuteCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList
|
||||
:refresh="true"
|
||||
:get-key="identity"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.domain_mutes')">
|
||||
<div class="profile-edit-domain-mute-form">
|
||||
<input
|
||||
v-model="newDomainToMute"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
type="text"
|
||||
@keyup.enter="muteDomain"
|
||||
>
|
||||
<ProgressButton
|
||||
class="btn btn-default"
|
||||
:click="muteDomain"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
<DomainMuteList
|
||||
:refresh="true"
|
||||
:get-key="identity"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</DomainMuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_settings.js">
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.profile-edit {
|
||||
.bio {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
input[type=file] {
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.banner {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.uploading {
|
||||
font-size: 1.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.name-changer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.current-avatar {
|
||||
display: block;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
}
|
||||
|
||||
.current-mascot {
|
||||
display: block;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.mascot {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.oauth-tokens {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&-usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&-bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
&-domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-subitem {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -34,9 +34,9 @@
|
|||
},
|
||||
"domain_mute_card": {
|
||||
"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",
|
||||
|
@ -121,7 +126,7 @@
|
|||
"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",
|
||||
|
@ -234,7 +239,7 @@
|
|||
"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...",
|
||||
"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": {
|
||||
|
@ -279,6 +284,7 @@
|
|||
"current_mascot": "Your current mascot",
|
||||
"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",
|
||||
|
@ -398,7 +404,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": {
|
||||
|
@ -624,7 +630,9 @@
|
|||
"mute_conversation": "Mute conversation",
|
||||
"unmute_conversation": "Unmute conversation",
|
||||
"status_unavailable": "Status unavailable",
|
||||
"copy_link": "Copy link to status"
|
||||
"copy_link": "Copy link to status",
|
||||
"thread_muted": "Thread muted",
|
||||
"thread_muted_and_words": ", has words:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Approve",
|
||||
|
@ -654,11 +662,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": {
|
||||
|
|
|
@ -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"
|
||||
|
@ -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",
|
||||
|
|
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
"disable": "Désactiver",
|
||||
"enable": "Activer",
|
||||
"confirm": "Confirmer",
|
||||
"verify": "Vérifier"
|
||||
"verify": "Vérifier",
|
||||
"dismiss": "Rejeter"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Rogner l'image",
|
||||
|
@ -167,7 +168,7 @@
|
|||
"generate_new_recovery_codes": "Générer de nouveaux codes de récupération",
|
||||
"warning_of_generate_new_codes": "Quand vous générez de nouveauc codes de récupération, vos anciens codes ne fonctionnerons plus.",
|
||||
"recovery_codes": "Codes de récupération.",
|
||||
"waiting_a_recovery_codes": "Récéption des codes de récupération…",
|
||||
"waiting_a_recovery_codes": "Réception des codes de récupération…",
|
||||
"recovery_codes_warning": "Écrivez les codes ou sauvez les quelquepart sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez vérouillé en dehors de votre compte.",
|
||||
"authentication_methods": "Methodes d'authentification",
|
||||
"scan": {
|
||||
|
@ -210,7 +211,7 @@
|
|||
"data_import_export_tab": "Import / Export des Données",
|
||||
"default_vis": "Visibilité par défaut",
|
||||
"delete_account": "Supprimer le compte",
|
||||
"delete_account_description": "Supprimer définitivement votre compte et tous vos statuts.",
|
||||
"delete_account_description": "Supprimer définitivement vos données et désactiver votre compte.",
|
||||
"delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateur⋅ice de cette instance.",
|
||||
"delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.",
|
||||
"avatar_size_instruction": "La taille minimale recommandée pour l'image de l'avatar est de 150x150 pixels.",
|
||||
|
@ -343,7 +344,13 @@
|
|||
"upgraded_from_v2": "PleromaFE à été mis à jour, le thème peut être un peu différent que dans vos souvenirs.",
|
||||
"v2_imported": "Le fichier que vous avez importé vient d'un version antérieure. Nous essayons de maximizer la compatibilité mais il peu y avoir quelques incohérences.",
|
||||
"future_version_imported": "Le fichier importé viens d'une version postérieure de PleromaFE.",
|
||||
"older_version_imported": "Le fichier importé viens d'une version antérieure de PleromaFE."
|
||||
"older_version_imported": "Le fichier importé viens d'une version antérieure de PleromaFE.",
|
||||
"snapshot_source_mismatch": "Conflict de version : Probablement due à un retour arrière puis remise à jour de la version de PleromaFE, si vous avez charger le thème en utilisant une version antérieure vous voulez probablement utiliser la version antérieure, autrement utiliser la version postérieure.",
|
||||
"migration_napshot_gone": "Pour une raison inconnue l'instantané est manquant, des parties peuvent rendre différentes que dans vos souvenirs.",
|
||||
"migration_snapshot_ok": "Pour être sûr un instantanée du thème à été chargé. Vos pouvez essayer de charger ses données.",
|
||||
"fe_downgraded": "Retour en arrière de la version de PleromaFE.",
|
||||
"fe_upgraded": "Le moteur de thème PleromaFE à été mis à jour après un changement de version.",
|
||||
"snapshot_missing": "Aucun instantané du thème à été trouvé dans le fichier, il peut y avoir un rendu différent à la vision originelle."
|
||||
},
|
||||
"keep_as_is": "Garder tel-quel",
|
||||
"use_source": "Nouvelle version"
|
||||
|
@ -392,7 +399,10 @@
|
|||
"selectedPost": "Message sélectionné",
|
||||
"selectedMenu": "Objet sélectionné du menu",
|
||||
"disabled": "Désactivé",
|
||||
"tabs": "Onglets"
|
||||
"tabs": "Onglets",
|
||||
"toggled": "(Dés)activé",
|
||||
"highlight": "Éléments mis en valeur",
|
||||
"popover": "Infobulles, menus"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Rondeur"
|
||||
|
@ -482,7 +492,9 @@
|
|||
"useStreamingApi": "Recevoir les messages et notifications en temps réel",
|
||||
"notification_setting_filters": "Filtres",
|
||||
"notification_setting_privacy_option": "Masquer l'expéditeur et le contenu des notifications push",
|
||||
"notification_setting_privacy": "Intimité"
|
||||
"notification_setting_privacy": "Intimité",
|
||||
"hide_followers_count_description": "Masquer le nombre d'abonnés",
|
||||
"accent": "Accent"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Fermer",
|
||||
|
@ -666,10 +678,13 @@
|
|||
"unicode": "émoji unicode",
|
||||
"load_all": "Charger tout les {emojiAmount} émojis",
|
||||
"load_all_hint": "{saneAmount} émojis chargé, charger tout les émojis peuvent causer des problèmes de performances.",
|
||||
"stickers": "Stickers"
|
||||
"stickers": "Stickers",
|
||||
"keep_open": "Garder le sélecteur ouvert"
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"error": "Non trouvé."
|
||||
"error": "Non trouvé.",
|
||||
"searching_for": "Rechercher",
|
||||
"remote_user_resolver": "Résolution de compte distant"
|
||||
},
|
||||
"time": {
|
||||
"minutes_short": "{0}min",
|
||||
|
|
250
src/i18n/it.json
250
src/i18n/it.json
|
@ -12,7 +12,12 @@
|
|||
"disable": "Disabilita",
|
||||
"enable": "Abilita",
|
||||
"confirm": "Conferma",
|
||||
"verify": "Verifica"
|
||||
"verify": "Verifica",
|
||||
"peek": "Anteprima",
|
||||
"close": "Chiudi",
|
||||
"retry": "Riprova",
|
||||
"error_retry": "Per favore, riprova",
|
||||
"loading": "Carico…"
|
||||
},
|
||||
"nav": {
|
||||
"mentions": "Menzioni",
|
||||
|
@ -35,14 +40,14 @@
|
|||
"followed_you": "ti segue",
|
||||
"notifications": "Notifiche",
|
||||
"read": "Letto!",
|
||||
"broken_favorite": "Stato sconosciuto, lo sto cercando...",
|
||||
"broken_favorite": "Stato sconosciuto, lo sto cercando…",
|
||||
"favorited_you": "ha gradito il tuo messaggio",
|
||||
"load_older": "Carica notifiche precedenti",
|
||||
"repeated_you": "ha condiviso il tuo messaggio",
|
||||
"follow_request": "vuole seguirti",
|
||||
"no_more_notifications": "Fine delle notifiche",
|
||||
"migrated_to": "è migrato verso",
|
||||
"reacted_with": "ha reagito con"
|
||||
"reacted_with": "ha reagito con {0}"
|
||||
},
|
||||
"settings": {
|
||||
"attachments": "Allegati",
|
||||
|
@ -57,7 +62,7 @@
|
|||
"hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze",
|
||||
"name": "Nome",
|
||||
"name_bio": "Nome ed introduzione",
|
||||
"nsfw_clickthrough": "Fai click per visualizzare gli allegati nascosti",
|
||||
"nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati",
|
||||
"profile_background": "Sfondo della tua pagina",
|
||||
"profile_banner": "Stendardo del tuo profilo",
|
||||
"reply_link_preview": "Visualizza le risposte al passaggio del cursore",
|
||||
|
@ -138,12 +143,200 @@
|
|||
"streaming": "Mostra automaticamente i nuovi messaggi quando sei in cima alla pagina",
|
||||
"text": "Testo",
|
||||
"theme_help": "Usa codici colore esadecimali (#rrggbb) per personalizzare il tuo schema di colori.",
|
||||
"tooltipRadius": "Descrizioni/avvisi",
|
||||
"tooltipRadius": "Suggerimenti/avvisi",
|
||||
"values": {
|
||||
"false": "no",
|
||||
"true": "sì"
|
||||
},
|
||||
"avatar_size_instruction": "La taglia minima per l'icona personale è 150x150 pixel.",
|
||||
"domain_mutes": "Domini",
|
||||
"discoverable": "Permetti la scoperta di questo profilo da servizi di ricerca ed altro",
|
||||
"composing": "Composizione",
|
||||
"changed_email": "Email cambiata con successo!",
|
||||
"change_email_error": "C'è stato un problema nel cambiare la tua email.",
|
||||
"change_email": "Cambia email",
|
||||
"blocks_tab": "Bloccati",
|
||||
"blocks_imported": "Blocchi importati! Saranno elaborati a breve.",
|
||||
"block_import_error": "Errore nell'importazione",
|
||||
"block_import": "Importa blocchi",
|
||||
"block_export_button": "Esporta i tuoi blocchi in un file CSV",
|
||||
"block_export": "Esporta blocchi",
|
||||
"allow_following_move": "Consenti",
|
||||
"mfa": {
|
||||
"verify": {
|
||||
"desc": "Per abilitare l'autenticazione bifattoriale, inserisci il codice fornito dalla tua applicazione:"
|
||||
},
|
||||
"scan": {
|
||||
"secret_code": "Codice",
|
||||
"desc": "Con la tua applicazione bifattoriale, acquisisci questo QR o inserisci il codice manualmente:",
|
||||
"title": "Acquisisci"
|
||||
},
|
||||
"authentication_methods": "Metodi di accesso",
|
||||
"recovery_codes_warning": "Appuntati i codici o salvali in un posto sicuro, altrimenti rischi di non rivederli mai più. Se perderai l'accesso sia alla tua applicazione bifattoriale che ai codici di recupero non potrai più accedere al tuo profilo.",
|
||||
"waiting_a_recovery_codes": "Ricevo codici di recupero…",
|
||||
"recovery_codes": "Codici di recupero.",
|
||||
"warning_of_generate_new_codes": "Alla generazione di nuovi codici di recupero, quelli vecchi saranno disattivati.",
|
||||
"generate_new_recovery_codes": "Genera nuovi codici di recupero",
|
||||
"title": "Accesso bifattoriale",
|
||||
"confirm_and_enable": "Conferma ed abilita OTP",
|
||||
"wait_pre_setup_otp": "preimposto OTP",
|
||||
"setup_otp": "Imposta OTP",
|
||||
"otp": "OTP"
|
||||
},
|
||||
"enter_current_password_to_confirm": "Inserisci la tua password per identificarti",
|
||||
"security": "Sicurezza",
|
||||
"app_name": "Nome applicazione",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"help": {
|
||||
"older_version_imported": "Il tema importato è stato creato per una versione precedente dell'interfaccia.",
|
||||
"future_version_imported": "Il tema importato è stato creato per una versione più recente dell'interfaccia.",
|
||||
"v2_imported": "Il tema importato è stato creato per una vecchia interfaccia. Non tutto potrebbe essere come prima.",
|
||||
"upgraded_from_v2": "L'interfaccia è stata aggiornata, il tema potrebbe essere diverso da come lo intendevi.",
|
||||
"migration_snapshot_ok": "Ho caricato l'anteprima del tema. Puoi provare a caricarne i contenuti.",
|
||||
"fe_downgraded": "L'interfaccia è stata portata ad una versione precedente.",
|
||||
"fe_upgraded": "Lo schema dei temi è stato aggiornato insieme all'interfaccia.",
|
||||
"snapshot_missing": "Il tema non è provvisto di anteprima, quindi potrebbe essere diverso da come appare.",
|
||||
"snapshot_present": "Tutti i valori sono sostituiti dall'anteprima del tema. Puoi invece caricare i suoi contenuti.",
|
||||
"snapshot_source_mismatch": "Conflitto di versione: probabilmente l'interfaccia è stata portata ad una versione precedente e poi aggiornata di nuovo. Se hai modificato il tema con una versione precedente dell'interfaccia, usa la vecchia versione del tema, altrimenti puoi usare la nuova.",
|
||||
"migration_napshot_gone": "Anteprima del tema non trovata, non tutto potrebbe essere come ricordi."
|
||||
},
|
||||
"use_source": "Nuova versione",
|
||||
"use_snapshot": "Versione precedente",
|
||||
"keep_as_is": "Mantieni tal quale",
|
||||
"load_theme": "Carica tema",
|
||||
"clear_opacity": "Rimuovi opacità",
|
||||
"clear_all": "Azzera tutto",
|
||||
"reset": "Reimposta",
|
||||
"save_load_hint": "Le opzioni \"mantieni\" conservano le impostazioni correnti quando selezioni o carichi un tema, e le salvano quando ne esporti uno. Quando nessuna casella è selezionata, tutte le impostazioni correnti saranno salvate nel tema.",
|
||||
"keep_fonts": "Mantieni font",
|
||||
"keep_roundness": "Mantieni vertici",
|
||||
"keep_opacity": "Mantieni opacità",
|
||||
"keep_shadows": "Mantieni ombre",
|
||||
"keep_color": "Mantieni colori"
|
||||
},
|
||||
"common": {
|
||||
"opacity": "Opacità",
|
||||
"color": "Colore",
|
||||
"contrast": {
|
||||
"context": {
|
||||
"text": "per il testo",
|
||||
"18pt": "per il testo grande (oltre 17pt)"
|
||||
},
|
||||
"level": {
|
||||
"bad": "non soddisfa le linee guida di alcun livello",
|
||||
"aaa": "soddisfa le linee guida di livello AAA (ottimo)",
|
||||
"aa": "soddisfa le linee guida di livello AA (sufficiente)"
|
||||
},
|
||||
"hint": "Il rapporto di contrasto è {ratio}, e {level} {context}"
|
||||
}
|
||||
},
|
||||
"advanced_colors": {
|
||||
"badge": "Sfondo medaglie",
|
||||
"post": "Messaggi / Biografie",
|
||||
"alert_neutral": "Neutro",
|
||||
"alert_warning": "Attenzione",
|
||||
"alert_error": "Errore",
|
||||
"alert": "Sfondo degli avvertimenti",
|
||||
"_tab_label": "Avanzate",
|
||||
"tabs": "Etichette",
|
||||
"disabled": "Disabilitato",
|
||||
"selectedMenu": "Voce menù selezionata",
|
||||
"selectedPost": "Messaggio selezionato",
|
||||
"pressed": "Premuto",
|
||||
"highlight": "Elementi evidenziati",
|
||||
"icons": "Icone",
|
||||
"poll": "Grafico sondaggi",
|
||||
"underlay": "Sottostante",
|
||||
"faint_text": "Testo sbiadito",
|
||||
"inputs": "Campi d'immissione",
|
||||
"buttons": "Pulsanti",
|
||||
"borders": "Bordi",
|
||||
"top_bar": "Barra superiore",
|
||||
"panel_header": "Titolo pannello",
|
||||
"badge_notification": "Notifica",
|
||||
"popover": "Suggerimenti, menù, sbalzi"
|
||||
},
|
||||
"common_colors": {
|
||||
"rgbo": "Icone, accenti, medaglie",
|
||||
"foreground_hint": "Seleziona l'etichetta \"Avanzate\" per controlli più fini",
|
||||
"main": "Colori comuni",
|
||||
"_tab_label": "Comuni"
|
||||
},
|
||||
"shadows": {
|
||||
"inset": "Includi",
|
||||
"spread": "Spandi",
|
||||
"blur": "Sfoca",
|
||||
"shadow_id": "Ombra numero {value}",
|
||||
"override": "Sostituisci",
|
||||
"component": "Componente",
|
||||
"_tab_label": "Luci ed ombre"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Raggio"
|
||||
}
|
||||
},
|
||||
"enable_web_push_notifications": "Abilita notifiche web push",
|
||||
"fun": "Divertimento",
|
||||
"notification_mutes": "Per non ricevere notifiche da uno specifico utente, zittiscilo.",
|
||||
"notification_setting_privacy_option": "Nascondi mittente e contenuti delle notifiche push",
|
||||
"notification_setting_privacy": "Privacy",
|
||||
"notification_setting_followers": "Utenti che ti seguono",
|
||||
"notification_setting_non_followers": "Utenti che non ti seguono",
|
||||
"notification_setting_non_follows": "Utenti che non segui",
|
||||
"notification_setting_follows": "Utenti che segui",
|
||||
"notification_setting": "Ricevi notifiche da:",
|
||||
"notification_setting_filters": "Filtri",
|
||||
"notifications": "Notifiche",
|
||||
"greentext": "Frecce da meme",
|
||||
"upload_a_photo": "Carica un'immagine",
|
||||
"type_domains_to_mute": "Cerca domini da zittire",
|
||||
"theme_help_v2_2": "Le icone dietro alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se si usano delle trasparenze, questi indicatori mostrano il peggior caso possibile.",
|
||||
"theme_help_v2_1": "Puoi anche forzare colore ed opacità di alcuni elementi selezionando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le forzature.",
|
||||
"useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)",
|
||||
"useStreamingApi": "Ricevi messaggi e notifiche in tempo reale",
|
||||
"user_mutes": "Utenti",
|
||||
"post_status_content_type": "Tipo di contenuto dei messaggi",
|
||||
"subject_line_noop": "Non copiare",
|
||||
"subject_line_mastodon": "Come in Mastodon: copia tal quale",
|
||||
"subject_line_email": "Come nelle email: \"re: oggetto\"",
|
||||
"subject_line_behavior": "Copia oggetto quando rispondi",
|
||||
"subject_input_always_show": "Mostra sempre il campo Oggetto",
|
||||
"minimal_scopes_mode": "Riduci opzioni di visibilità",
|
||||
"scope_copy": "Risposte ereditano la visibilità (messaggi privati lo fanno sempre)",
|
||||
"search_user_to_mute": "Cerca utente da zittire",
|
||||
"search_user_to_block": "Cerca utente da bloccare",
|
||||
"autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)",
|
||||
"show_moderator_badge": "Mostra l'insegna di moderatore sulla mia pagina",
|
||||
"show_admin_badge": "Mostra l'insegna di amministratore sulla mia pagina",
|
||||
"hide_followers_count_description": "Non mostrare quanti seguaci ho",
|
||||
"hide_follows_count_description": "Non mostrare quanti utenti seguo",
|
||||
"hide_followers_description": "Non mostrare i miei seguaci",
|
||||
"hide_follows_description": "Non mostrare chi seguo",
|
||||
"no_mutes": "Nessun utente zittito",
|
||||
"no_blocks": "Nessun utente bloccato",
|
||||
"notification_visibility_emoji_reactions": "Reazioni",
|
||||
"notification_visibility_moves": "Migrazioni utenti",
|
||||
"new_email": "Nuova email",
|
||||
"use_contain_fit": "Non ritagliare le anteprime degli allegati",
|
||||
"play_videos_in_modal": "Riproduci video in un riquadro a sbalzo",
|
||||
"mutes_tab": "Zittiti",
|
||||
"interface": "Interfaccia",
|
||||
"instance_default_simple": "(predefinito)",
|
||||
"checkboxRadius": "Caselle di selezione",
|
||||
"import_blocks_from_a_csv_file": "Importa blocchi da un file CSV",
|
||||
"hide_filtered_statuses": "Nascondi messaggi filtrati",
|
||||
"use_one_click_nsfw": "Apri media offuscati con un solo click",
|
||||
"preload_images": "Precarica immagini",
|
||||
"hide_isp": "Nascondi pannello della stanza",
|
||||
"max_thumbnails": "Numero massimo di anteprime per messaggio",
|
||||
"hide_muted_posts": "Nascondi messaggi degli utenti zittiti",
|
||||
"accent": "Accento",
|
||||
"emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze",
|
||||
"pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore",
|
||||
"notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più.",
|
||||
"mutes_and_blocks": "Zittiti e bloccati"
|
||||
},
|
||||
"timeline": {
|
||||
"error_fetching": "Errore nell'aggiornamento",
|
||||
"load_older": "Carica messaggi più vecchi",
|
||||
|
@ -209,7 +402,10 @@
|
|||
"account_not_locked_warning_link": "protetto",
|
||||
"attachments_sensitive": "Nascondi gli allegati",
|
||||
"content_type": {
|
||||
"text/plain": "Testo normale"
|
||||
"text/plain": "Testo normale",
|
||||
"text/bbcode": "BBCode",
|
||||
"text/markdown": "Markdown",
|
||||
"text/html": "HTML"
|
||||
},
|
||||
"content_warning": "Oggetto (facoltativo)",
|
||||
"default": "Sono appena atterrato a Fiumicino.",
|
||||
|
@ -220,7 +416,15 @@
|
|||
"private": "Solo per seguaci - Visibile solo dai tuoi seguaci",
|
||||
"public": "Pubblico - Visibile sulla sequenza pubblica",
|
||||
"unlisted": "Non elencato - Non visibile sulla sequenza pubblica"
|
||||
}
|
||||
},
|
||||
"scope_notice": {
|
||||
"unlisted": "Questo messaggio non sarà visibile sulla sequenza locale né su quella pubblica",
|
||||
"private": "Questo messaggio sarà visibile solo ai tuoi seguaci",
|
||||
"public": "Questo messaggio sarà visibile a tutti"
|
||||
},
|
||||
"direct_warning_to_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati all'inizio.",
|
||||
"direct_warning_to_all": "Questo messaggio sarà visibile a tutti i menzionati.",
|
||||
"new_status": "Nuovo messaggio"
|
||||
},
|
||||
"registration": {
|
||||
"bio": "Introduzione",
|
||||
|
@ -228,7 +432,20 @@
|
|||
"fullname": "Nome visualizzato",
|
||||
"password_confirm": "Conferma password",
|
||||
"registration": "Registrazione",
|
||||
"token": "Codice d'invito"
|
||||
"token": "Codice d'invito",
|
||||
"validations": {
|
||||
"password_confirmation_match": "dovrebbe essere uguale alla password",
|
||||
"password_confirmation_required": "non può essere vuoto",
|
||||
"password_required": "non può essere vuoto",
|
||||
"email_required": "non può essere vuoto",
|
||||
"fullname_required": "non può essere vuoto",
|
||||
"username_required": "non può essere vuoto"
|
||||
},
|
||||
"bio_placeholder": "es.\nCiao, sono Lupo Lucio.\nSono un lupo fantastico che vive nel Fantabosco. Forse mi hai visto alla Melevisione.",
|
||||
"fullname_placeholder": "es. Lupo Lucio",
|
||||
"username_placeholder": "es. mister_wolf",
|
||||
"new_captcha": "Clicca l'immagine per avere un altro captcha",
|
||||
"captcha": "CAPTCHA"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Sequenza dell'Utente"
|
||||
|
@ -269,9 +486,9 @@
|
|||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Zittisci",
|
||||
"mute_progress": "Zittisco...",
|
||||
"mute_progress": "Zittisco…",
|
||||
"unmute": "Ascolta",
|
||||
"unmute_progress": "Procedo..."
|
||||
"unmute_progress": "Procedo…"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Esporta",
|
||||
|
@ -307,7 +524,10 @@
|
|||
"not_enough_options": "Aggiungi altre risposte"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Condivisi e preferiti"
|
||||
"favs_repeats": "Condivisi e preferiti",
|
||||
"load_older": "Carica vecchie interazioni",
|
||||
"moves": "Utenti migrati",
|
||||
"follows": "Nuovi seguìti"
|
||||
},
|
||||
"emoji": {
|
||||
"load_all": "Carico tutti i {emojiAmount} emoji",
|
||||
|
@ -319,5 +539,13 @@
|
|||
"keep_open": "Tieni aperto il menù",
|
||||
"emoji": "Emoji",
|
||||
"stickers": "Adesivi"
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Seleziona tutto"
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"error": "Non trovato.",
|
||||
"searching_for": "Cerco",
|
||||
"remote_user_resolver": "Cerca utenti remoti"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,34 +7,47 @@
|
|||
// sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json
|
||||
// There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry.
|
||||
|
||||
const loaders = {
|
||||
ar: () => import('./ar.json'),
|
||||
ca: () => import('./ca.json'),
|
||||
cs: () => import('./cs.json'),
|
||||
de: () => import('./de.json'),
|
||||
eo: () => import('./eo.json'),
|
||||
es: () => import('./es.json'),
|
||||
et: () => import('./et.json'),
|
||||
eu: () => import('./eu.json'),
|
||||
fi: () => import('./fi.json'),
|
||||
fr: () => import('./fr.json'),
|
||||
ga: () => import('./ga.json'),
|
||||
he: () => import('./he.json'),
|
||||
hu: () => import('./hu.json'),
|
||||
it: () => import('./it.json'),
|
||||
ja: () => import('./ja_pedantic.json'),
|
||||
ja_easy: () => import('./ja_easy.json'),
|
||||
ko: () => import('./ko.json'),
|
||||
nb: () => import('./nb.json'),
|
||||
nl: () => import('./nl.json'),
|
||||
oc: () => import('./oc.json'),
|
||||
pl: () => import('./pl.json'),
|
||||
pt: () => import('./pt.json'),
|
||||
ro: () => import('./ro.json'),
|
||||
ru: () => import('./ru.json'),
|
||||
te: () => import('./te.json'),
|
||||
zh: () => import('./zh.json')
|
||||
}
|
||||
|
||||
const messages = {
|
||||
ar: require('./ar.json'),
|
||||
ca: require('./ca.json'),
|
||||
cs: require('./cs.json'),
|
||||
de: require('./de.json'),
|
||||
en: require('./en.json'),
|
||||
eo: require('./eo.json'),
|
||||
es: require('./es.json'),
|
||||
et: require('./et.json'),
|
||||
eu: require('./eu.json'),
|
||||
fi: require('./fi.json'),
|
||||
fr: require('./fr.json'),
|
||||
ga: require('./ga.json'),
|
||||
he: require('./he.json'),
|
||||
hu: require('./hu.json'),
|
||||
it: require('./it.json'),
|
||||
ja: require('./ja_pedantic.json'),
|
||||
ja_easy: require('./ja_easy.json'),
|
||||
ko: require('./ko.json'),
|
||||
nb: require('./nb.json'),
|
||||
nl: require('./nl.json'),
|
||||
oc: require('./oc.json'),
|
||||
pl: require('./pl.json'),
|
||||
pt: require('./pt.json'),
|
||||
ro: require('./ro.json'),
|
||||
ru: require('./ru.json'),
|
||||
te: require('./te.json'),
|
||||
zh: require('./zh.json')
|
||||
languages: ['en', ...Object.keys(loaders)],
|
||||
default: {
|
||||
en: require('./en.json')
|
||||
},
|
||||
setLanguage: async (i18n, language) => {
|
||||
if (loaders[language]) {
|
||||
let messages = await loaders[language]()
|
||||
i18n.setLocaleMessage(language, messages)
|
||||
}
|
||||
i18n.locale = language
|
||||
}
|
||||
}
|
||||
|
||||
export default messages
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
"interactions": "Interacties"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Onbekende status, aan het zoeken...",
|
||||
"broken_favorite": "Onbekende status, aan het zoeken…",
|
||||
"favorited_you": "vond je status leuk",
|
||||
"followed_you": "volgt jou",
|
||||
"load_older": "Laad oudere meldingen",
|
||||
|
@ -402,7 +402,7 @@
|
|||
"title": "Twee-factor Authenticatie",
|
||||
"generate_new_recovery_codes": "Genereer nieuwe herstelcodes",
|
||||
"recovery_codes": "Herstelcodes.",
|
||||
"waiting_a_recovery_codes": "Backup codes ontvangen...",
|
||||
"waiting_a_recovery_codes": "Backup codes ontvangen…",
|
||||
"authentication_methods": "Authenticatie methodes",
|
||||
"scan": {
|
||||
"title": "Scannen",
|
||||
|
@ -526,11 +526,11 @@
|
|||
},
|
||||
"show_repeats": "Herhalingen tonen",
|
||||
"hide_repeats": "Herhalingen verbergen",
|
||||
"mute_progress": "Negeren...",
|
||||
"unmute_progress": "Negering opheffen...",
|
||||
"mute_progress": "Negeren…",
|
||||
"unmute_progress": "Negering opheffen…",
|
||||
"unmute": "Negering opheffen",
|
||||
"block_progress": "Blokkeren...",
|
||||
"unblock_progress": "Blokkade opheffen...",
|
||||
"block_progress": "Blokkeren…",
|
||||
"unblock_progress": "Blokkade opheffen…",
|
||||
"unblock": "Blokkade opheffen",
|
||||
"unsubscribe": "Abonnement opzeggen",
|
||||
"subscribe": "Abonneren",
|
||||
|
@ -604,9 +604,9 @@
|
|||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Negeren",
|
||||
"mute_progress": "Negeren...",
|
||||
"mute_progress": "Negeren…",
|
||||
"unmute": "Negering opheffen",
|
||||
"unmute_progress": "Negering wordt opgeheven..."
|
||||
"unmute_progress": "Negering wordt opgeheven…"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Exporteren",
|
||||
|
|
|
@ -456,9 +456,9 @@
|
|||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Игнорировать",
|
||||
"mute_progress": "В процессе...",
|
||||
"mute_progress": "В процессе…",
|
||||
"unmute": "Прекратить игнорирование",
|
||||
"unmute_progress": "В процессе..."
|
||||
"unmute_progress": "В процессе…"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Экспорт",
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import merge from 'lodash.merge'
|
||||
import objectPath from 'object-path'
|
||||
import localforage from 'localforage'
|
||||
import { each } from 'lodash'
|
||||
import { each, get, set } from 'lodash'
|
||||
|
||||
let loaded = false
|
||||
|
||||
const defaultReducer = (state, paths) => (
|
||||
paths.length === 0 ? state : paths.reduce((substate, path) => {
|
||||
objectPath.set(substate, path, objectPath.get(state, path))
|
||||
set(substate, path, get(state, path))
|
||||
return substate
|
||||
}, {})
|
||||
)
|
||||
|
|
|
@ -46,11 +46,13 @@ Vue.use(VBodyScrollLock)
|
|||
|
||||
const i18n = new VueI18n({
|
||||
// By default, use the browser locale, we will update it if neccessary
|
||||
locale: currentLocale,
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages
|
||||
messages: messages.default
|
||||
})
|
||||
|
||||
messages.setLanguage(i18n, currentLocale)
|
||||
|
||||
const persistedStateOptions = {
|
||||
paths: [
|
||||
'config',
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
import { set, delete as del } from 'vue'
|
||||
import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
||||
import messages from '../i18n/messages'
|
||||
|
||||
const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||
|
||||
/* TODO this is a bit messy.
|
||||
* We need to declare settings with their types and also deal with
|
||||
* instance-default settings in some way, hopefully try to avoid copy-pasta
|
||||
* in general.
|
||||
*/
|
||||
export const multiChoiceProperties = [
|
||||
'postContentType',
|
||||
'subjectLineBehavior'
|
||||
]
|
||||
|
||||
export const defaultState = {
|
||||
colors: {},
|
||||
theme: undefined,
|
||||
|
@ -105,6 +116,10 @@ const config = {
|
|||
case 'customTheme':
|
||||
case 'customThemeSource':
|
||||
applyTheme(value)
|
||||
break
|
||||
case 'interfaceLanguage':
|
||||
messages.setLanguage(this.getters.i18n, value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { set } from 'vue'
|
||||
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
import apiService from '../services/api/api.service.js'
|
||||
import { instanceDefaultProperties } from './config.js'
|
||||
|
||||
const defaultState = {
|
||||
|
@ -48,6 +49,7 @@ const defaultState = {
|
|||
postFormats: [],
|
||||
restrictedNicknames: [],
|
||||
safeDM: true,
|
||||
knownDomains: [],
|
||||
|
||||
// Feature-set, apparently, not everything here is reported...
|
||||
chatAvailable: false,
|
||||
|
@ -80,6 +82,9 @@ const instance = {
|
|||
if (typeof value !== 'undefined') {
|
||||
set(state, name, value)
|
||||
}
|
||||
},
|
||||
setKnownDomains (state, domains) {
|
||||
state.knownDomains = domains
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
@ -182,6 +187,18 @@ const instance = {
|
|||
state.emojiFetched = true
|
||||
dispatch('getStaticEmoji')
|
||||
}
|
||||
},
|
||||
|
||||
async getKnownDomains ({ commit, rootState }) {
|
||||
try {
|
||||
const result = await apiService.fetchKnownDomains({
|
||||
credentials: rootState.users.currentUser.credentials
|
||||
})
|
||||
commit('setKnownDomains', result)
|
||||
} catch (e) {
|
||||
console.warn("Can't load known domains")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { set, delete as del } from 'vue'
|
||||
|
||||
const defaultState = {
|
||||
settingsModalState: 'hidden',
|
||||
settingsModalLoaded: false,
|
||||
settings: {
|
||||
currentSaveStateNotice: null,
|
||||
noticeClearTimeout: null,
|
||||
|
@ -35,6 +37,27 @@ const interfaceMod = {
|
|||
},
|
||||
setMobileLayout (state, value) {
|
||||
state.mobileLayout = value
|
||||
},
|
||||
closeSettingsModal (state) {
|
||||
state.settingsModalState = 'hidden'
|
||||
},
|
||||
togglePeekSettingsModal (state) {
|
||||
switch (state.settingsModalState) {
|
||||
case 'minimized':
|
||||
state.settingsModalState = 'visible'
|
||||
return
|
||||
case 'visible':
|
||||
state.settingsModalState = 'minimized'
|
||||
return
|
||||
default:
|
||||
throw new Error('Illegal minimization state of settings modal')
|
||||
}
|
||||
},
|
||||
openSettingsModal (state) {
|
||||
state.settingsModalState = 'visible'
|
||||
if (!state.settingsModalLoaded) {
|
||||
state.settingsModalLoaded = true
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
@ -49,6 +72,15 @@ const interfaceMod = {
|
|||
},
|
||||
setMobileLayout ({ commit }, value) {
|
||||
commit('setMobileLayout', value)
|
||||
},
|
||||
closeSettingsModal ({ commit }) {
|
||||
commit('closeSettingsModal')
|
||||
},
|
||||
openSettingsModal ({ commit }) {
|
||||
commit('openSettingsModal')
|
||||
},
|
||||
togglePeekSettingsModal ({ commit }) {
|
||||
commit('togglePeekSettingsModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { set } from 'vue'
|
||||
import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
|
||||
import apiService from '../services/api/api.service.js'
|
||||
// import parse from '../services/status_parser/status_parser.js'
|
||||
import { muteWordHits } from '../services/status_parser/status_parser.js'
|
||||
|
||||
const emptyTl = (userId = 0) => ({
|
||||
statuses: [],
|
||||
|
@ -381,7 +381,18 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
notifObj.image = status.attachments[0].url
|
||||
}
|
||||
|
||||
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
|
||||
const reasonsToMuteNotif = (
|
||||
notification.seen ||
|
||||
state.notifications.desktopNotificationSilence ||
|
||||
!visibleNotificationTypes.includes(notification.type) ||
|
||||
(
|
||||
notification.type === 'mention' && status && (
|
||||
status.muted ||
|
||||
muteWordHits(status, rootGetters.mergedConfig.muteWords).length === 0
|
||||
)
|
||||
)
|
||||
)
|
||||
if (!reasonsToMuteNotif) {
|
||||
let desktopNotification = new window.Notification(title, notifObj)
|
||||
// Chrome is known for not closing notifications automatically
|
||||
// according to MDN, anyway.
|
||||
|
|
|
@ -438,10 +438,10 @@ const users = {
|
|||
store.commit('setUserForNotification', notification)
|
||||
})
|
||||
},
|
||||
searchUsers (store, { query }) {
|
||||
return store.rootState.api.backendInteractor.searchUsers({ query })
|
||||
searchUsers ({ rootState, commit }, { query }) {
|
||||
return rootState.api.backendInteractor.searchUsers({ query })
|
||||
.then((users) => {
|
||||
store.commit('addNewUsers', users)
|
||||
commit('addNewUsers', users)
|
||||
return users
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { each, map, concat, last, get } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import 'whatwg-fetch'
|
||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||
|
||||
/* eslint-env browser */
|
||||
|
@ -76,6 +75,7 @@ const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
|
|||
const MASTODON_MASCOT_URL = '/api/v1/pleroma/mascot'
|
||||
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
|
||||
const MASTODON_STREAMING = '/api/v1/streaming'
|
||||
const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
|
||||
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
||||
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
|
@ -1010,6 +1010,10 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const fetchKnownDomains = ({ credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials })
|
||||
}
|
||||
|
||||
const fetchDomainMutes = ({ credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
|
||||
}
|
||||
|
@ -1210,6 +1214,7 @@ const apiService = {
|
|||
updateNotificationSettings,
|
||||
search2,
|
||||
searchUsers,
|
||||
fetchKnownDomains,
|
||||
fetchDomainMutes,
|
||||
muteDomain,
|
||||
unmuteDomain
|
||||
|
|
|
@ -56,6 +56,12 @@ export const parseUser = (data) => {
|
|||
value: addEmojis(field.value, data.emojis)
|
||||
}
|
||||
})
|
||||
output.fields_text = data.fields.map(field => {
|
||||
return {
|
||||
name: unescape(field.name.replace(/<[^>]*>/g, '')),
|
||||
value: unescape(field.value.replace(/<[^>]*>/g, ''))
|
||||
}
|
||||
})
|
||||
|
||||
// Utilize avatar_static for gif avatars?
|
||||
output.profile_image_url = data.avatar
|
||||
|
@ -258,6 +264,12 @@ export const parseStatus = (data) => {
|
|||
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
|
||||
output.external_url = data.url
|
||||
output.poll = data.poll
|
||||
if (output.poll) {
|
||||
output.poll.options = (output.poll.options || []).map(field => ({
|
||||
...field,
|
||||
title_html: addEmojis(field.title, data.emojis)
|
||||
}))
|
||||
}
|
||||
output.pinned = data.pinned
|
||||
output.muted = data.muted
|
||||
} else {
|
||||
|
|
32
src/services/resettable_async_component.js
Normal file
32
src/services/resettable_async_component.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
/* By default async components don't have any way to recover, if component is
|
||||
* failed, it is failed forever. This helper tries to remedy that by recreating
|
||||
* async component when retry is requested (by user). You need to emit the
|
||||
* `resetAsyncComponent` event from child to reset the component. Generally,
|
||||
* this should be done from error component but could be done from loading or
|
||||
* actual target component itself if needs to be.
|
||||
*/
|
||||
function getResettableAsyncComponent (asyncComponent, options) {
|
||||
const asyncComponentFactory = () => () => ({
|
||||
component: asyncComponent(),
|
||||
...options
|
||||
})
|
||||
|
||||
const observe = Vue.observable({ c: asyncComponentFactory() })
|
||||
|
||||
return {
|
||||
functional: true,
|
||||
render (createElement, { data, children }) {
|
||||
// emit event resetAsyncComponent to reloading
|
||||
data.on = {}
|
||||
data.on.resetAsyncComponent = () => {
|
||||
observe.c = asyncComponentFactory()
|
||||
// parent.$forceUpdate()
|
||||
}
|
||||
return createElement(observe.c, data, children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default getResettableAsyncComponent
|
|
@ -1,15 +1,11 @@
|
|||
import sanitize from 'sanitize-html'
|
||||
import { filter } from 'lodash'
|
||||
|
||||
export const removeAttachmentLinks = (html) => {
|
||||
return sanitize(html, {
|
||||
allowedTags: false,
|
||||
allowedAttributes: false,
|
||||
exclusiveFilter: ({ tag, attribs }) => tag === 'a' && typeof attribs.class === 'string' && attribs.class.match(/attachment/)
|
||||
export const muteWordHits = (status, muteWords) => {
|
||||
const statusText = status.text.toLowerCase()
|
||||
const statusSummary = status.summary.toLowerCase()
|
||||
const hits = filter(muteWords, (muteWord) => {
|
||||
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
|
||||
})
|
||||
}
|
||||
|
||||
export const parse = (html) => {
|
||||
return removeAttachmentLinks(html)
|
||||
return hits
|
||||
}
|
||||
|
||||
export default parse
|
||||
|
|
|
@ -356,6 +356,12 @@ export const SLOT_INHERITANCE = {
|
|||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
postGreentext: {
|
||||
depends: ['cGreen'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
border: {
|
||||
depends: ['fg'],
|
||||
opacity: 'border',
|
||||
|
|
|
@ -363,6 +363,18 @@
|
|||
"css": "ok",
|
||||
"code": 59431,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "4109c474ff99cad28fd5a2c38af2ec6f",
|
||||
"css": "filter",
|
||||
"code": 61616,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
|
||||
"css": "download",
|
||||
"code": 59429,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -286,7 +286,9 @@
|
|||
"cGreen": "#008000",
|
||||
"cOrange": "#808000",
|
||||
"highlight": "--accent",
|
||||
"selectedPost": "--bg,-10"
|
||||
"selectedPost": "--bg,-10",
|
||||
"selectedMenu": "--accent",
|
||||
"selectedMenuPopover": "--accent"
|
||||
},
|
||||
"radii": {
|
||||
"btn": "0",
|
||||
|
|
|
@ -277,7 +277,9 @@
|
|||
"cGreen": "#008000",
|
||||
"cOrange": "#808000",
|
||||
"highlight": "--accent",
|
||||
"selectedPost": "--bg,-10"
|
||||
"selectedPost": "--bg,-10",
|
||||
"selectedMenu": "--accent",
|
||||
"selectedMenuPopover": "--accent"
|
||||
},
|
||||
"radii": {
|
||||
"btn": "0",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue