forked from AkkomaGang/akkoma-fe
Merge branch 'settings-modal' into 'develop'
Settings modal See merge request pleroma/pleroma-fe!1118
This commit is contained in:
commit
a5de8db579
63 changed files with 2609 additions and 1889 deletions
|
@ -29,11 +29,11 @@
|
||||||
"portal-vue": "^2.1.4",
|
"portal-vue": "^2.1.4",
|
||||||
"sanitize-html": "^1.13.0",
|
"sanitize-html": "^1.13.0",
|
||||||
"v-click-outside": "^2.1.1",
|
"v-click-outside": "^2.1.1",
|
||||||
"vue": "^2.5.13",
|
"vue": "^2.6.11",
|
||||||
"vue-chat-scroll": "^1.2.1",
|
"vue-chat-scroll": "^1.2.1",
|
||||||
"vue-i18n": "^7.3.2",
|
"vue-i18n": "^7.3.2",
|
||||||
"vue-router": "^3.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vue-template-compiler": "^2.3.4",
|
"vue-template-compiler": "^2.6.11",
|
||||||
"vuelidate": "^0.7.4",
|
"vuelidate": "^0.7.4",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1",
|
||||||
"whatwg-fetch": "^2.0.3"
|
"whatwg-fetch": "^2.0.3"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
||||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
||||||
|
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MediaModal from './components/media_modal/media_modal.vue'
|
||||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||||
|
@ -29,6 +30,7 @@ export default {
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
MobilePostStatusButton,
|
MobilePostStatusButton,
|
||||||
MobileNav,
|
MobileNav,
|
||||||
|
SettingsModal,
|
||||||
UserReportingModal,
|
UserReportingModal,
|
||||||
PostStatusModal
|
PostStatusModal
|
||||||
},
|
},
|
||||||
|
@ -117,6 +119,9 @@ export default {
|
||||||
onSearchBarToggled (hidden) {
|
onSearchBarToggled (hidden) {
|
||||||
this.searchBarHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
|
openSettingsModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal')
|
||||||
|
},
|
||||||
updateMobileState () {
|
updateMobileState () {
|
||||||
const mobileLayout = windowWidth() <= 800
|
const mobileLayout = windowWidth() <= 800
|
||||||
const changed = mobileLayout !== this.isMobileLayout
|
const changed = mobileLayout !== this.isMobileLayout
|
||||||
|
|
47
src/App.scss
47
src/App.scss
|
@ -566,7 +566,7 @@ main-router {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-left: .25em;
|
margin-left: .5em;
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
.select-multiple {
|
||||||
display: flex;
|
display: flex;
|
||||||
.option-list {
|
.option-list {
|
||||||
|
|
|
@ -46,15 +46,16 @@
|
||||||
@toggled="onSearchBarToggled"
|
@toggled="onSearchBarToggled"
|
||||||
@click.stop.native
|
@click.stop.native
|
||||||
/>
|
/>
|
||||||
<router-link
|
<a
|
||||||
|
href="#"
|
||||||
class="mobile-hidden"
|
class="mobile-hidden"
|
||||||
:to="{ name: 'settings'}"
|
@click.stop="openSettingsModal"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="button-icon icon-cog nav-icon"
|
class="button-icon icon-cog nav-icon"
|
||||||
:title="$t('nav.preferences')"
|
:title="$t('nav.preferences')"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
href="/pleroma/admin/#/login-pleroma"
|
||||||
|
@ -125,6 +126,7 @@
|
||||||
<MobilePostStatusButton />
|
<MobilePostStatusButton />
|
||||||
<UserReportingModal />
|
<UserReportingModal />
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
|
<SettingsModal />
|
||||||
<portal-target name="modal" />
|
<portal-target name="modal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -7,10 +7,8 @@ import Interactions from 'components/interactions/interactions.vue'
|
||||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||||
import Search from 'components/search/search.vue'
|
import Search from 'components/search/search.vue'
|
||||||
import Settings from 'components/settings/settings.vue'
|
|
||||||
import Registration from 'components/registration/registration.vue'
|
import Registration from 'components/registration/registration.vue'
|
||||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||||
import UserSettings from 'components/user_settings/user_settings.vue'
|
|
||||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||||
import Notifications from 'components/notifications/notifications.vue'
|
import Notifications from 'components/notifications/notifications.vue'
|
||||||
|
@ -56,12 +54,10 @@ export default (store) => {
|
||||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'settings', path: '/settings', component: Settings },
|
|
||||||
{ name: 'registration', path: '/registration', component: Registration },
|
{ name: 'registration', path: '/registration', component: Registration },
|
||||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
|
|
||||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'login', path: '/login', component: AuthForm },
|
{ name: 'login', path: '/login', component: AuthForm },
|
||||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div class="async-component-error">
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
{{ $t('general.generic_error') }}
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
{{ $t('general.error_retry') }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="retry"
|
||||||
|
>
|
||||||
|
{{ $t('general.retry') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
retry () {
|
||||||
|
this.$emit('resetAsyncComponent')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.async-component-error {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
.btn {
|
||||||
|
margin: .5em;
|
||||||
|
padding: .5em 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-show="isOpen"
|
v-show="isOpen"
|
||||||
v-body-scroll-lock="isOpen"
|
v-body-scroll-lock="isOpen && !noBackground"
|
||||||
class="modal-view"
|
class="modal-view"
|
||||||
|
:class="classes"
|
||||||
@click.self="$emit('backdropClicked')"
|
@click.self="$emit('backdropClicked')"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -15,6 +16,18 @@ export default {
|
||||||
isOpen: {
|
isOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
noBackground: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
classes () {
|
||||||
|
return {
|
||||||
|
'modal-background': !this.noBackground,
|
||||||
|
'open': this.isOpen
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,12 +45,22 @@ export default {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
pointer-events: none;
|
||||||
animation-duration: 0.2s;
|
animation-duration: 0.2s;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
animation-name: modal-background-fadein;
|
animation-name: modal-background-fadein;
|
||||||
|
|
||||||
body:not(.scroll-locked) & {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.modal-background {
|
||||||
|
pointer-events: initial;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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>
|
|
@ -13,9 +13,6 @@ const PostStatusModal = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isLoggedIn () {
|
|
||||||
return !!this.$store.state.users.currentUser
|
|
||||||
},
|
|
||||||
modalActivated () {
|
modalActivated () {
|
||||||
return this.$store.state.postStatus.modalActivated
|
return this.$store.state.postStatus.modalActivated
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
v-if="isLoggedIn && !resettingForm"
|
|
||||||
:is-open="modalActivated"
|
:is-open="modalActivated"
|
||||||
class="post-form-modal-view"
|
class="post-form-modal-view"
|
||||||
@backdropClicked="closeModal"
|
@backdropClicked="closeModal"
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
/* eslint-env browser */
|
|
||||||
import { filter, trim } from 'lodash'
|
|
||||||
|
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
|
||||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
|
||||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
|
||||||
import { extractCommit } from '../../services/version/version.service'
|
|
||||||
import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
|
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
|
||||||
|
|
||||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/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>
|
124
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
124
src/components/settings_modal/tabs/mutes_and_blocks_tab.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import get from 'lodash/get'
|
||||||
|
import map from 'lodash/map'
|
||||||
|
import reject from 'lodash/reject'
|
||||||
|
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||||
|
import BlockCard from 'src/components/block_card/block_card.vue'
|
||||||
|
import MuteCard from 'src/components/mute_card/mute_card.vue'
|
||||||
|
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
|
||||||
|
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||||
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
|
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const BlockList = withSubscription({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||||
|
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||||
|
childPropName: 'items'
|
||||||
|
})(SelectableList)
|
||||||
|
|
||||||
|
const MuteList = withSubscription({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||||
|
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||||
|
childPropName: 'items'
|
||||||
|
})(SelectableList)
|
||||||
|
|
||||||
|
const DomainMuteList = withSubscription({
|
||||||
|
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||||
|
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
||||||
|
childPropName: 'items'
|
||||||
|
})(SelectableList)
|
||||||
|
|
||||||
|
const MutesAndBlocks = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
activeTab: 'profile',
|
||||||
|
newDomainToMute: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.dispatch('fetchTokens')
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
TabSwitcher,
|
||||||
|
BlockList,
|
||||||
|
MuteList,
|
||||||
|
DomainMuteList,
|
||||||
|
BlockCard,
|
||||||
|
MuteCard,
|
||||||
|
DomainMuteCard,
|
||||||
|
ProgressButton,
|
||||||
|
Autosuggest,
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
importFollows (file) {
|
||||||
|
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||||
|
.then((status) => {
|
||||||
|
if (!status) {
|
||||||
|
throw new Error('failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
importBlocks (file) {
|
||||||
|
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
||||||
|
.then((status) => {
|
||||||
|
if (!status) {
|
||||||
|
throw new Error('failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
generateExportableUsersContent (users) {
|
||||||
|
// Get addresses
|
||||||
|
return users.map((user) => {
|
||||||
|
// check is it's a local user
|
||||||
|
if (user && user.is_local) {
|
||||||
|
// append the instance address
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
return user.screen_name + '@' + location.hostname
|
||||||
|
}
|
||||||
|
return user.screen_name
|
||||||
|
}).join('\n')
|
||||||
|
},
|
||||||
|
activateTab (tabName) {
|
||||||
|
this.activeTab = tabName
|
||||||
|
},
|
||||||
|
filterUnblockedUsers (userIds) {
|
||||||
|
return reject(userIds, (userId) => {
|
||||||
|
const relationship = this.$store.getters.relationship(this.userId)
|
||||||
|
return relationship.blocking || userId === this.$store.state.users.currentUser.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
filterUnMutedUsers (userIds) {
|
||||||
|
return reject(userIds, (userId) => {
|
||||||
|
const relationship = this.$store.getters.relationship(this.userId)
|
||||||
|
return relationship.muting || userId === this.$store.state.users.currentUser.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
queryUserIds (query) {
|
||||||
|
return this.$store.dispatch('searchUsers', { query })
|
||||||
|
.then((users) => map(users, 'id'))
|
||||||
|
},
|
||||||
|
blockUsers (ids) {
|
||||||
|
return this.$store.dispatch('blockUsers', ids)
|
||||||
|
},
|
||||||
|
unblockUsers (ids) {
|
||||||
|
return this.$store.dispatch('unblockUsers', ids)
|
||||||
|
},
|
||||||
|
muteUsers (ids) {
|
||||||
|
return this.$store.dispatch('muteUsers', ids)
|
||||||
|
},
|
||||||
|
unmuteUsers (ids) {
|
||||||
|
return this.$store.dispatch('unmuteUsers', ids)
|
||||||
|
},
|
||||||
|
unmuteDomains (domains) {
|
||||||
|
return this.$store.dispatch('unmuteDomains', domains)
|
||||||
|
},
|
||||||
|
muteDomain () {
|
||||||
|
return this.$store.dispatch('muteDomain', this.newDomainToMute)
|
||||||
|
.then(() => { this.newDomainToMute = '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MutesAndBlocks
|
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
|
||||||
|
}
|
||||||
|
}
|
176
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
176
src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
<template>
|
||||||
|
<tab-switcher
|
||||||
|
:scrollable-tabs="true"
|
||||||
|
class="mutes-and-blocks-tab"
|
||||||
|
>
|
||||||
|
<div :label="$t('settings.blocks_tab')">
|
||||||
|
<div class="usersearch-wrapper">
|
||||||
|
<Autosuggest
|
||||||
|
:filter="filterUnblockedUsers"
|
||||||
|
:query="queryUserIds"
|
||||||
|
:placeholder="$t('settings.search_user_to_block')"
|
||||||
|
>
|
||||||
|
<BlockCard
|
||||||
|
slot-scope="row"
|
||||||
|
:user-id="row.item"
|
||||||
|
/>
|
||||||
|
</Autosuggest>
|
||||||
|
</div>
|
||||||
|
<BlockList
|
||||||
|
:refresh="true"
|
||||||
|
:get-key="i => i"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
slot="header"
|
||||||
|
slot-scope="{selected}"
|
||||||
|
>
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn btn-default bulk-action-button"
|
||||||
|
:click="() => blockUsers(selected)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.block') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('user_card.block_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn btn-default"
|
||||||
|
:click="() => unblockUsers(selected)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.unblock') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('user_card.unblock_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
slot="item"
|
||||||
|
slot-scope="{item}"
|
||||||
|
>
|
||||||
|
<BlockCard :user-id="item" />
|
||||||
|
</template>
|
||||||
|
<template slot="empty">
|
||||||
|
{{ $t('settings.no_blocks') }}
|
||||||
|
</template>
|
||||||
|
</BlockList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :label="$t('settings.mutes_tab')">
|
||||||
|
<tab-switcher>
|
||||||
|
<div label="Users">
|
||||||
|
<div class="usersearch-wrapper">
|
||||||
|
<Autosuggest
|
||||||
|
:filter="filterUnMutedUsers"
|
||||||
|
:query="queryUserIds"
|
||||||
|
:placeholder="$t('settings.search_user_to_mute')"
|
||||||
|
>
|
||||||
|
<MuteCard
|
||||||
|
slot-scope="row"
|
||||||
|
:user-id="row.item"
|
||||||
|
/>
|
||||||
|
</Autosuggest>
|
||||||
|
</div>
|
||||||
|
<MuteList
|
||||||
|
:refresh="true"
|
||||||
|
:get-key="i => i"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
slot="header"
|
||||||
|
slot-scope="{selected}"
|
||||||
|
>
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn btn-default"
|
||||||
|
:click="() => muteUsers(selected)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.mute') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('user_card.mute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn btn-default"
|
||||||
|
:click="() => unmuteUsers(selected)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.unmute') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('user_card.unmute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
slot="item"
|
||||||
|
slot-scope="{item}"
|
||||||
|
>
|
||||||
|
<MuteCard :user-id="item" />
|
||||||
|
</template>
|
||||||
|
<template slot="empty">
|
||||||
|
{{ $t('settings.no_mutes') }}
|
||||||
|
</template>
|
||||||
|
</MuteList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :label="$t('settings.domain_mutes')">
|
||||||
|
<div class="domain-mute-form">
|
||||||
|
<input
|
||||||
|
v-model="newDomainToMute"
|
||||||
|
:placeholder="$t('settings.type_domains_to_mute')"
|
||||||
|
type="text"
|
||||||
|
@keyup.enter="muteDomain"
|
||||||
|
>
|
||||||
|
<ProgressButton
|
||||||
|
class="btn btn-default domain-mute-button"
|
||||||
|
:click="muteDomain"
|
||||||
|
>
|
||||||
|
{{ $t('domain_mute_card.mute') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('domain_mute_card.mute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
</div>
|
||||||
|
<DomainMuteList
|
||||||
|
:refresh="true"
|
||||||
|
:get-key="i => i"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
slot="header"
|
||||||
|
slot-scope="{selected}"
|
||||||
|
>
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn btn-default"
|
||||||
|
:click="() => unmuteDomains(selected)"
|
||||||
|
>
|
||||||
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
|
<template slot="progress">
|
||||||
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
slot="item"
|
||||||
|
slot-scope="{item}"
|
||||||
|
>
|
||||||
|
<DomainMuteCard :domain="item" />
|
||||||
|
</template>
|
||||||
|
<template slot="empty">
|
||||||
|
{{ $t('settings.no_mutes') }}
|
||||||
|
</template>
|
||||||
|
</DomainMuteList>
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./mutes_and_blocks_tab.js"></script>
|
||||||
|
<style lang="scss" src="./mutes_and_blocks_tab.scss"></style>
|
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,11 +137,7 @@
|
||||||
|
|
||||||
<script src="./mfa.js"></script>
|
<script src="./mfa.js"></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../../../_variables.scss';
|
||||||
.warning {
|
|
||||||
color: $fallback--cOrange;
|
|
||||||
color: var(--cOrange, $fallback--cOrange);
|
|
||||||
}
|
|
||||||
.mfa-settings {
|
.mfa-settings {
|
||||||
.mfa-heading, .method-item {
|
.mfa-heading, .method-item {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -151,6 +147,11 @@
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: $fallback--cOrange;
|
||||||
|
color: var(--cOrange, $fallback--cOrange);
|
||||||
|
}
|
||||||
|
|
||||||
.setup-otp {
|
.setup-otp {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="mfa-backup-codes">
|
||||||
<h4 v-if="displayTitle">
|
<h4 v-if="displayTitle">
|
||||||
{{ $t('settings.mfa.recovery_codes') }}
|
{{ $t('settings.mfa.recovery_codes') }}
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -21,8 +21,9 @@
|
||||||
</template>
|
</template>
|
||||||
<script src="./mfa_backup_codes.js"></script>
|
<script src="./mfa_backup_codes.js"></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../../../_variables.scss';
|
||||||
|
|
||||||
|
.mfa-backup-codes {
|
||||||
.warning {
|
.warning {
|
||||||
color: $fallback--cOrange;
|
color: $fallback--cOrange;
|
||||||
color: var(--cOrange, $fallback--cOrange);
|
color: var(--cOrange, $fallback--cOrange);
|
||||||
|
@ -30,4 +31,5 @@
|
||||||
.backup-codes {
|
.backup-codes {
|
||||||
font-family: var(--postCodeFont, monospace);
|
font-family: var(--postCodeFont, monospace);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
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,
|
rgb2hex,
|
||||||
hex2rgb,
|
hex2rgb,
|
||||||
getContrastRatioLayers
|
getContrastRatioLayers
|
||||||
} from '../../services/color_convert/color_convert.js'
|
} from 'src/services/color_convert/color_convert.js'
|
||||||
import {
|
import {
|
||||||
DEFAULT_SHADOWS,
|
DEFAULT_SHADOWS,
|
||||||
generateColors,
|
generateColors,
|
||||||
|
@ -14,26 +14,27 @@ import {
|
||||||
getThemes,
|
getThemes,
|
||||||
shadows2to3,
|
shadows2to3,
|
||||||
colors2to3
|
colors2to3
|
||||||
} from '../../services/style_setter/style_setter.js'
|
} from 'src/services/style_setter/style_setter.js'
|
||||||
import {
|
import {
|
||||||
SLOT_INHERITANCE
|
SLOT_INHERITANCE
|
||||||
} from '../../services/theme_data/pleromafe.js'
|
} from 'src/services/theme_data/pleromafe.js'
|
||||||
import {
|
import {
|
||||||
CURRENT_VERSION,
|
CURRENT_VERSION,
|
||||||
OPACITIES,
|
OPACITIES,
|
||||||
getLayers,
|
getLayers,
|
||||||
getOpacitySlot
|
getOpacitySlot
|
||||||
} from '../../services/theme_data/theme_data.service.js'
|
} from 'src/services/theme_data/theme_data.service.js'
|
||||||
import ColorInput from '../color_input/color_input.vue'
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
import RangeInput from '../range_input/range_input.vue'
|
import RangeInput from 'src/components/range_input/range_input.vue'
|
||||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||||
import ShadowControl from '../shadow_control/shadow_control.vue'
|
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||||
import FontControl from '../font_control/font_control.vue'
|
import FontControl from 'src/components/font_control/font_control.vue'
|
||||||
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
|
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||||
|
import ExportImport from 'src/components/export_import/export_import.vue'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
import Preview from './preview.vue'
|
import Preview from './preview.vue'
|
||||||
import ExportImport from '../export_import/export_import.vue'
|
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
|
||||||
|
|
||||||
// List of color values used in v1
|
// List of color values used in v1
|
||||||
const v1OnlyNames = [
|
const v1OnlyNames = [
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../_variables.scss';
|
@import 'src/_variables.scss';
|
||||||
.style-switcher {
|
.theme-tab {
|
||||||
|
padding-bottom: 2em;
|
||||||
.theme-warning {
|
.theme-warning {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
@ -54,10 +55,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-switcher {
|
|
||||||
margin: 0 -1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-container {
|
.reset-container {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
@ -98,20 +95,25 @@
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
|
margin-bottom: 1em;
|
||||||
.btn {
|
|
||||||
min-width: 1px;
|
|
||||||
flex: 0 auto;
|
|
||||||
padding: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 1em;
|
.tab-header-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-width: 1px;
|
||||||
|
flex: 0 auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-selector {
|
.shadow-selector {
|
||||||
|
@ -161,7 +163,7 @@
|
||||||
border-bottom: 1px dashed;
|
border-bottom: 1px dashed;
|
||||||
border-color: $fallback--border;
|
border-color: $fallback--border;
|
||||||
border-color: var(--border, $fallback--border);
|
border-color: var(--border, $fallback--border);
|
||||||
margin: 1em -1em 0;
|
margin: 1em 0;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: var(--body-background-image);
|
background: var(--body-background-image);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
@ -328,6 +330,14 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.apply-container {
|
||||||
|
.btn {
|
||||||
|
min-height: 28px;
|
||||||
|
min-width: 10em;
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
margin-left: .25em;
|
margin-left: .25em;
|
||||||
margin-right: .25em;
|
margin-right: .25em;
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="style-switcher">
|
<div class="theme-tab">
|
||||||
<div class="presets-container">
|
<div class="presets-container">
|
||||||
<div class="save-load">
|
<div class="save-load">
|
||||||
<div
|
<div
|
||||||
|
@ -126,6 +126,7 @@
|
||||||
>
|
>
|
||||||
<div class="tab-header">
|
<div class="tab-header">
|
||||||
<p>{{ $t('settings.theme_help') }}</p>
|
<p>{{ $t('settings.theme_help') }}</p>
|
||||||
|
<div class="tab-header-buttons">
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
@click="clearOpacity"
|
@click="clearOpacity"
|
||||||
|
@ -139,6 +140,7 @@
|
||||||
{{ $t('settings.style.switcher.clear_all') }}
|
{{ $t('settings.style.switcher.clear_all') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<p>{{ $t('settings.theme_help_v2_1') }}</p>
|
<p>{{ $t('settings.theme_help_v2_1') }}</p>
|
||||||
<h4>{{ $t('settings.style.common_colors.main') }}</h4>
|
<h4>{{ $t('settings.style.common_colors.main') }}</h4>
|
||||||
<div class="color-item">
|
<div class="color-item">
|
||||||
|
@ -951,6 +953,6 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./style_switcher.js"></script>
|
<script src="./theme_tab.js"></script>
|
||||||
|
|
||||||
<style src="./style_switcher.scss" lang="scss"></style>
|
<style src="./theme_tab.scss" lang="scss"></style>
|
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) {
|
touchMove (e) {
|
||||||
GestureService.updateSwipe(e, this.closeGesture)
|
GestureService.updateSwipe(e, this.closeGesture)
|
||||||
|
},
|
||||||
|
openSettingsModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,9 +122,12 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li @click="toggleDrawer">
|
<li @click="toggleDrawer">
|
||||||
<router-link :to="{ name: 'settings' }">
|
<a
|
||||||
|
href="#"
|
||||||
|
@click="openSettingsModal"
|
||||||
|
>
|
||||||
<i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
|
<i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
|
||||||
</router-link>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li @click="toggleDrawer">
|
<li @click="toggleDrawer">
|
||||||
<router-link :to="{ name: 'about'}">
|
<router-link :to="{ name: 'about'}">
|
||||||
|
|
|
@ -24,6 +24,11 @@ export default Vue.component('tab-switcher', {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
sideTabBar: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -55,6 +60,9 @@ export default Vue.component('tab-switcher', {
|
||||||
this.onSwitch.call(null, this.$slots.default[index].key)
|
this.onSwitch.call(null, this.$slots.default[index].key)
|
||||||
}
|
}
|
||||||
this.active = index
|
this.active = index
|
||||||
|
if (this.scrollableTabs) {
|
||||||
|
this.$refs.contents.scrollTop = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -64,7 +72,6 @@ export default Vue.component('tab-switcher', {
|
||||||
if (!slot.tag) return
|
if (!slot.tag) return
|
||||||
const classesTab = ['tab']
|
const classesTab = ['tab']
|
||||||
const classesWrapper = ['tab-wrapper']
|
const classesWrapper = ['tab-wrapper']
|
||||||
|
|
||||||
if (this.activeIndex === index) {
|
if (this.activeIndex === index) {
|
||||||
classesTab.push('active')
|
classesTab.push('active')
|
||||||
classesWrapper.push('active')
|
classesWrapper.push('active')
|
||||||
|
@ -87,8 +94,14 @@ export default Vue.component('tab-switcher', {
|
||||||
<button
|
<button
|
||||||
disabled={slot.data.attrs.disabled}
|
disabled={slot.data.attrs.disabled}
|
||||||
onClick={this.activateTab(index)}
|
onClick={this.activateTab(index)}
|
||||||
class={classesTab.join(' ')}>
|
class={classesTab.join(' ')}
|
||||||
{slot.data.attrs.label}</button>
|
type="button"
|
||||||
|
>
|
||||||
|
{!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)}
|
||||||
|
<span class="text">
|
||||||
|
{slot.data.attrs.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -96,20 +109,32 @@ export default Vue.component('tab-switcher', {
|
||||||
const contents = this.$slots.default.map((slot, index) => {
|
const contents = this.$slots.default.map((slot, index) => {
|
||||||
if (!slot.tag) return
|
if (!slot.tag) return
|
||||||
const active = this.activeIndex === index
|
const active = this.activeIndex === index
|
||||||
if (this.renderOnlyFocused) {
|
const classes = [ active ? 'active' : 'hidden' ]
|
||||||
return active
|
if (slot.data.attrs.fullHeight) {
|
||||||
? <div class="active">{slot}</div>
|
classes.push('full-height')
|
||||||
: <div class="hidden"></div>
|
|
||||||
}
|
}
|
||||||
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 (
|
return (
|
||||||
<div class="tab-switcher">
|
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
{tabs}
|
{tabs}
|
||||||
</div>
|
</div>
|
||||||
<div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
|
<div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
|
||||||
{contents}
|
{contents}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,8 +2,145 @@
|
||||||
|
|
||||||
.tab-switcher {
|
.tab-switcher {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
font-size: 2em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top-tabs {
|
||||||
flex-direction: column;
|
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 {
|
.contents {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
|
@ -11,52 +148,32 @@
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.full-height:not(.hidden) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
> *:not(.mobile-label) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.scrollable-tabs {
|
&.scrollable-tabs {
|
||||||
flex-basis: 0;
|
|
||||||
overflow-y: auto;
|
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 {
|
.tab {
|
||||||
width: 100%;
|
|
||||||
min-width: 1px;
|
|
||||||
position: relative;
|
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;
|
white-space: nowrap;
|
||||||
|
padding: 6px 1em;
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--tabText, $fallback--text);
|
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--tab, $fallback--fg);
|
background-color: var(--tab, $fallback--fg);
|
||||||
|
|
||||||
|
&, &:active .tab-icon {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--tabText, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
|
||||||
|
@ -79,20 +196,41 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::after, &::before {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 7;
|
z-index: 7;
|
||||||
border-bottom: 1px solid;
|
|
||||||
border-bottom-color: $fallback--border;
|
|
||||||
border-bottom-color: var(--border, $fallback--border);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-label {
|
||||||
|
padding-left: .3em;
|
||||||
|
padding-bottom: .25em;
|
||||||
|
margin-top: .5em;
|
||||||
|
margin-left: .2em;
|
||||||
|
margin-bottom: .25em;
|
||||||
|
border-bottom: 1px solid var(--border, $fallback--border);
|
||||||
|
|
||||||
|
@media all and (min-width: 800px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,15 +50,6 @@
|
||||||
>
|
>
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
|
||||||
v-if="!isOtherUser"
|
|
||||||
:to="{ name: 'user-settings' }"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="button-icon icon-wrench usersettings"
|
|
||||||
:title="$t('tool_tip.user_settings')"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
<a
|
<a
|
||||||
v-if="isOtherUser && !user.is_local"
|
v-if="isOtherUser && !user.is_local"
|
||||||
:href="user.statusnet_profile_url"
|
:href="user.statusnet_profile_url"
|
||||||
|
@ -118,7 +109,7 @@
|
||||||
type="color"
|
type="color"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
for="style-switcher"
|
for="theme_tab"
|
||||||
class="userHighlightSel select"
|
class="userHighlightSel select"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
|
|
|
@ -3,6 +3,7 @@ import UserCard from '../user_card/user_card.vue'
|
||||||
import FollowCard from '../follow_card/follow_card.vue'
|
import FollowCard from '../follow_card/follow_card.vue'
|
||||||
import Timeline from '../timeline/timeline.vue'
|
import Timeline from '../timeline/timeline.vue'
|
||||||
import Conversation from '../conversation/conversation.vue'
|
import Conversation from '../conversation/conversation.vue'
|
||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||||
import List from '../list/list.vue'
|
import List from '../list/list.vue'
|
||||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||||
|
|
||||||
|
@ -146,6 +147,7 @@ const UserProfile = {
|
||||||
FollowerList,
|
FollowerList,
|
||||||
FriendList,
|
FriendList,
|
||||||
FollowCard,
|
FollowCard,
|
||||||
|
TabSwitcher,
|
||||||
Conversation
|
Conversation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,393 +0,0 @@
|
||||||
import unescape from 'lodash/unescape'
|
|
||||||
import get from 'lodash/get'
|
|
||||||
import map from 'lodash/map'
|
|
||||||
import reject from 'lodash/reject'
|
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
|
||||||
import ImageCropper from '../image_cropper/image_cropper.vue'
|
|
||||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
|
||||||
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
|
||||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
|
||||||
import BlockCard from '../block_card/block_card.vue'
|
|
||||||
import MuteCard from '../mute_card/mute_card.vue'
|
|
||||||
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
|
|
||||||
import SelectableList from '../selectable_list/selectable_list.vue'
|
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
|
||||||
import EmojiInput from '../emoji_input/emoji_input.vue'
|
|
||||||
import suggestor from '../emoji_input/suggestor.js'
|
|
||||||
import Autosuggest from '../autosuggest/autosuggest.vue'
|
|
||||||
import Importer from '../importer/importer.vue'
|
|
||||||
import Exporter from '../exporter/exporter.vue'
|
|
||||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
|
||||||
import Mfa from './mfa.vue'
|
|
||||||
|
|
||||||
const BlockList = withSubscription({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
|
||||||
childPropName: 'items'
|
|
||||||
})(SelectableList)
|
|
||||||
|
|
||||||
const MuteList = withSubscription({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
|
||||||
childPropName: 'items'
|
|
||||||
})(SelectableList)
|
|
||||||
|
|
||||||
const DomainMuteList = withSubscription({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
|
||||||
childPropName: 'items'
|
|
||||||
})(SelectableList)
|
|
||||||
|
|
||||||
const UserSettings = {
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
newEmail: '',
|
|
||||||
newName: this.$store.state.users.currentUser.name,
|
|
||||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
|
||||||
newLocked: this.$store.state.users.currentUser.locked,
|
|
||||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
|
||||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
|
||||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
|
||||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
|
||||||
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
|
||||||
hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
|
|
||||||
showRole: this.$store.state.users.currentUser.show_role,
|
|
||||||
role: this.$store.state.users.currentUser.role,
|
|
||||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
|
||||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
|
||||||
pickAvatarBtnVisible: true,
|
|
||||||
bannerUploading: false,
|
|
||||||
backgroundUploading: false,
|
|
||||||
banner: null,
|
|
||||||
bannerPreview: null,
|
|
||||||
background: null,
|
|
||||||
backgroundPreview: null,
|
|
||||||
bannerUploadError: null,
|
|
||||||
backgroundUploadError: null,
|
|
||||||
changeEmailError: false,
|
|
||||||
changeEmailPassword: '',
|
|
||||||
changedEmail: false,
|
|
||||||
deletingAccount: false,
|
|
||||||
deleteAccountConfirmPasswordInput: '',
|
|
||||||
deleteAccountError: false,
|
|
||||||
changePasswordInputs: [ '', '', '' ],
|
|
||||||
changedPassword: false,
|
|
||||||
changePasswordError: false,
|
|
||||||
activeTab: 'profile',
|
|
||||||
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
|
||||||
newDomainToMute: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.$store.dispatch('fetchTokens')
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
StyleSwitcher,
|
|
||||||
ScopeSelector,
|
|
||||||
TabSwitcher,
|
|
||||||
ImageCropper,
|
|
||||||
BlockList,
|
|
||||||
MuteList,
|
|
||||||
DomainMuteList,
|
|
||||||
EmojiInput,
|
|
||||||
Autosuggest,
|
|
||||||
BlockCard,
|
|
||||||
MuteCard,
|
|
||||||
DomainMuteCard,
|
|
||||||
ProgressButton,
|
|
||||||
Importer,
|
|
||||||
Exporter,
|
|
||||||
Mfa,
|
|
||||||
Checkbox
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
user () {
|
|
||||||
return this.$store.state.users.currentUser
|
|
||||||
},
|
|
||||||
emojiUserSuggestor () {
|
|
||||||
return suggestor({
|
|
||||||
emoji: [
|
|
||||||
...this.$store.state.instance.emoji,
|
|
||||||
...this.$store.state.instance.customEmoji
|
|
||||||
],
|
|
||||||
users: this.$store.state.users.users,
|
|
||||||
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
emojiSuggestor () {
|
|
||||||
return suggestor({ emoji: [
|
|
||||||
...this.$store.state.instance.emoji,
|
|
||||||
...this.$store.state.instance.customEmoji
|
|
||||||
] })
|
|
||||||
},
|
|
||||||
pleromaBackend () {
|
|
||||||
return this.$store.state.instance.pleromaBackend
|
|
||||||
},
|
|
||||||
minimalScopesMode () {
|
|
||||||
return this.$store.state.instance.minimalScopesMode
|
|
||||||
},
|
|
||||||
vis () {
|
|
||||||
return {
|
|
||||||
public: { selected: this.newDefaultScope === 'public' },
|
|
||||||
unlisted: { selected: this.newDefaultScope === 'unlisted' },
|
|
||||||
private: { selected: this.newDefaultScope === 'private' },
|
|
||||||
direct: { selected: this.newDefaultScope === 'direct' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentSaveStateNotice () {
|
|
||||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
|
||||||
},
|
|
||||||
oauthTokens () {
|
|
||||||
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
|
|
||||||
return {
|
|
||||||
id: oauthToken.id,
|
|
||||||
appName: oauthToken.app_name,
|
|
||||||
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateProfile () {
|
|
||||||
this.$store.state.api.backendInteractor
|
|
||||||
.updateProfile({
|
|
||||||
params: {
|
|
||||||
note: this.newBio,
|
|
||||||
locked: this.newLocked,
|
|
||||||
// Backend notation.
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
display_name: this.newName,
|
|
||||||
default_scope: this.newDefaultScope,
|
|
||||||
no_rich_text: this.newNoRichText,
|
|
||||||
hide_follows: this.hideFollows,
|
|
||||||
hide_followers: this.hideFollowers,
|
|
||||||
discoverable: this.discoverable,
|
|
||||||
allow_following_move: this.allowFollowingMove,
|
|
||||||
hide_follows_count: this.hideFollowsCount,
|
|
||||||
hide_followers_count: this.hideFollowersCount,
|
|
||||||
show_role: this.showRole
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
} }).then((user) => {
|
|
||||||
this.$store.commit('addNewUsers', [user])
|
|
||||||
this.$store.commit('setCurrentUser', user)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateNotificationSettings () {
|
|
||||||
this.$store.state.api.backendInteractor
|
|
||||||
.updateNotificationSettings({ settings: this.notificationSettings })
|
|
||||||
},
|
|
||||||
changeVis (visibility) {
|
|
||||||
this.newDefaultScope = visibility
|
|
||||||
},
|
|
||||||
uploadFile (slot, e) {
|
|
||||||
const file = e.target.files[0]
|
|
||||||
if (!file) { return }
|
|
||||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
|
||||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
|
||||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
|
||||||
this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const img = target.result
|
|
||||||
this[slot + 'Preview'] = img
|
|
||||||
this[slot] = file
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
},
|
|
||||||
submitAvatar (cropper, file) {
|
|
||||||
const that = this
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
function updateAvatar (avatar) {
|
|
||||||
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
|
||||||
.then((user) => {
|
|
||||||
that.$store.commit('addNewUsers', [user])
|
|
||||||
that.$store.commit('setCurrentUser', user)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cropper) {
|
|
||||||
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
|
||||||
} else {
|
|
||||||
updateAvatar(file)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
clearUploadError (slot) {
|
|
||||||
this[slot + 'UploadError'] = null
|
|
||||||
},
|
|
||||||
submitBanner () {
|
|
||||||
if (!this.bannerPreview) { return }
|
|
||||||
|
|
||||||
this.bannerUploading = true
|
|
||||||
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
|
|
||||||
.then((user) => {
|
|
||||||
this.$store.commit('addNewUsers', [user])
|
|
||||||
this.$store.commit('setCurrentUser', user)
|
|
||||||
this.bannerPreview = null
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
|
|
||||||
})
|
|
||||||
.then(() => { this.bannerUploading = false })
|
|
||||||
},
|
|
||||||
submitBg () {
|
|
||||||
if (!this.backgroundPreview) { return }
|
|
||||||
let background = this.background
|
|
||||||
this.backgroundUploading = true
|
|
||||||
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
|
||||||
if (!data.error) {
|
|
||||||
this.$store.commit('addNewUsers', [data])
|
|
||||||
this.$store.commit('setCurrentUser', data)
|
|
||||||
this.backgroundPreview = null
|
|
||||||
} else {
|
|
||||||
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
|
||||||
}
|
|
||||||
this.backgroundUploading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
importFollows (file) {
|
|
||||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
|
||||||
.then((status) => {
|
|
||||||
if (!status) {
|
|
||||||
throw new Error('failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
importBlocks (file) {
|
|
||||||
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
|
||||||
.then((status) => {
|
|
||||||
if (!status) {
|
|
||||||
throw new Error('failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
generateExportableUsersContent (users) {
|
|
||||||
// Get addresses
|
|
||||||
return users.map((user) => {
|
|
||||||
// check is it's a local user
|
|
||||||
if (user && user.is_local) {
|
|
||||||
// append the instance address
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
return user.screen_name + '@' + location.hostname
|
|
||||||
}
|
|
||||||
return user.screen_name
|
|
||||||
}).join('\n')
|
|
||||||
},
|
|
||||||
getFollowsContent () {
|
|
||||||
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
|
|
||||||
.then(this.generateExportableUsersContent)
|
|
||||||
},
|
|
||||||
getBlocksContent () {
|
|
||||||
return this.$store.state.api.backendInteractor.fetchBlocks()
|
|
||||||
.then(this.generateExportableUsersContent)
|
|
||||||
},
|
|
||||||
confirmDelete () {
|
|
||||||
this.deletingAccount = true
|
|
||||||
},
|
|
||||||
deleteAccount () {
|
|
||||||
this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
|
|
||||||
.then((res) => {
|
|
||||||
if (res.status === 'success') {
|
|
||||||
this.$store.dispatch('logout')
|
|
||||||
this.$router.push({ name: 'root' })
|
|
||||||
} else {
|
|
||||||
this.deleteAccountError = res.error
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
changePassword () {
|
|
||||||
const params = {
|
|
||||||
password: this.changePasswordInputs[0],
|
|
||||||
newPassword: this.changePasswordInputs[1],
|
|
||||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
|
||||||
}
|
|
||||||
this.$store.state.api.backendInteractor.changePassword(params)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.status === 'success') {
|
|
||||||
this.changedPassword = true
|
|
||||||
this.changePasswordError = false
|
|
||||||
this.logout()
|
|
||||||
} else {
|
|
||||||
this.changedPassword = false
|
|
||||||
this.changePasswordError = res.error
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
changeEmail () {
|
|
||||||
const params = {
|
|
||||||
email: this.newEmail,
|
|
||||||
password: this.changeEmailPassword
|
|
||||||
}
|
|
||||||
this.$store.state.api.backendInteractor.changeEmail(params)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.status === 'success') {
|
|
||||||
this.changedEmail = true
|
|
||||||
this.changeEmailError = false
|
|
||||||
} else {
|
|
||||||
this.changedEmail = false
|
|
||||||
this.changeEmailError = res.error
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
activateTab (tabName) {
|
|
||||||
this.activeTab = tabName
|
|
||||||
},
|
|
||||||
logout () {
|
|
||||||
this.$store.dispatch('logout')
|
|
||||||
this.$router.replace('/')
|
|
||||||
},
|
|
||||||
revokeToken (id) {
|
|
||||||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
|
||||||
this.$store.dispatch('revokeToken', id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filterUnblockedUsers (userIds) {
|
|
||||||
return reject(userIds, (userId) => {
|
|
||||||
const relationship = this.$store.getters.relationship(this.userId)
|
|
||||||
return relationship.blocking || userId === this.$store.state.users.currentUser.id
|
|
||||||
})
|
|
||||||
},
|
|
||||||
filterUnMutedUsers (userIds) {
|
|
||||||
return reject(userIds, (userId) => {
|
|
||||||
const relationship = this.$store.getters.relationship(this.userId)
|
|
||||||
return relationship.muting || userId === this.$store.state.users.currentUser.id
|
|
||||||
})
|
|
||||||
},
|
|
||||||
queryUserIds (query) {
|
|
||||||
return this.$store.dispatch('searchUsers', { query })
|
|
||||||
.then((users) => map(users, 'id'))
|
|
||||||
},
|
|
||||||
blockUsers (ids) {
|
|
||||||
return this.$store.dispatch('blockUsers', ids)
|
|
||||||
},
|
|
||||||
unblockUsers (ids) {
|
|
||||||
return this.$store.dispatch('unblockUsers', ids)
|
|
||||||
},
|
|
||||||
muteUsers (ids) {
|
|
||||||
return this.$store.dispatch('muteUsers', ids)
|
|
||||||
},
|
|
||||||
unmuteUsers (ids) {
|
|
||||||
return this.$store.dispatch('unmuteUsers', ids)
|
|
||||||
},
|
|
||||||
unmuteDomains (domains) {
|
|
||||||
return this.$store.dispatch('unmuteDomains', domains)
|
|
||||||
},
|
|
||||||
muteDomain () {
|
|
||||||
return this.$store.dispatch('muteDomain', this.newDomainToMute)
|
|
||||||
.then(() => { this.newDomainToMute = '' })
|
|
||||||
},
|
|
||||||
identity (value) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserSettings
|
|
|
@ -1,728 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="settings panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<div class="title">
|
|
||||||
{{ $t('settings.user_settings') }}
|
|
||||||
</div>
|
|
||||||
<transition name="fade">
|
|
||||||
<template v-if="currentSaveStateNotice">
|
|
||||||
<div
|
|
||||||
v-if="currentSaveStateNotice.error"
|
|
||||||
class="alert error"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
{{ $t('settings.saving_err') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="!currentSaveStateNotice.error"
|
|
||||||
class="alert transparent"
|
|
||||||
@click.prevent
|
|
||||||
>
|
|
||||||
{{ $t('settings.saving_ok') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body profile-edit">
|
|
||||||
<tab-switcher>
|
|
||||||
<div :label="$t('settings.profile_tab')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
|
||||||
<p>{{ $t('settings.name') }}</p>
|
|
||||||
<EmojiInput
|
|
||||||
v-model="newName"
|
|
||||||
enable-emoji-picker
|
|
||||||
:suggest="emojiSuggestor"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
v-model="newName"
|
|
||||||
classname="name-changer"
|
|
||||||
>
|
|
||||||
</EmojiInput>
|
|
||||||
<p>{{ $t('settings.bio') }}</p>
|
|
||||||
<EmojiInput
|
|
||||||
v-model="newBio"
|
|
||||||
enable-emoji-picker
|
|
||||||
:suggest="emojiUserSuggestor"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
v-model="newBio"
|
|
||||||
classname="bio"
|
|
||||||
/>
|
|
||||||
</EmojiInput>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="newLocked">
|
|
||||||
{{ $t('settings.lock_account_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
|
|
||||||
<div
|
|
||||||
id="default-vis"
|
|
||||||
class="visibility-tray"
|
|
||||||
>
|
|
||||||
<scope-selector
|
|
||||||
:show-all="true"
|
|
||||||
:user-default="newDefaultScope"
|
|
||||||
:initial-scope="newDefaultScope"
|
|
||||||
:on-scope-change="changeVis"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="newNoRichText">
|
|
||||||
{{ $t('settings.no_rich_text_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="hideFollows">
|
|
||||||
{{ $t('settings.hide_follows_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p class="setting-subitem">
|
|
||||||
<Checkbox
|
|
||||||
v-model="hideFollowsCount"
|
|
||||||
:disabled="!hideFollows"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_follows_count_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="hideFollowers">
|
|
||||||
{{ $t('settings.hide_followers_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p class="setting-subitem">
|
|
||||||
<Checkbox
|
|
||||||
v-model="hideFollowersCount"
|
|
||||||
:disabled="!hideFollowers"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_followers_count_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="allowFollowingMove">
|
|
||||||
{{ $t('settings.allow_following_move') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p v-if="role === 'admin' || role === 'moderator'">
|
|
||||||
<Checkbox v-model="showRole">
|
|
||||||
<template v-if="role === 'admin'">
|
|
||||||
{{ $t('settings.show_admin_badge') }}
|
|
||||||
</template>
|
|
||||||
<template v-if="role === 'moderator'">
|
|
||||||
{{ $t('settings.show_moderator_badge') }}
|
|
||||||
</template>
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="discoverable">
|
|
||||||
{{ $t('settings.discoverable') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
:disabled="newName && newName.length === 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="updateProfile"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.avatar') }}</h2>
|
|
||||||
<p class="visibility-notice">
|
|
||||||
{{ $t('settings.avatar_size_instruction') }}
|
|
||||||
</p>
|
|
||||||
<p>{{ $t('settings.current_avatar') }}</p>
|
|
||||||
<img
|
|
||||||
:src="user.profile_image_url_original"
|
|
||||||
class="current-avatar"
|
|
||||||
>
|
|
||||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
|
||||||
<button
|
|
||||||
v-show="pickAvatarBtnVisible"
|
|
||||||
id="pick-avatar"
|
|
||||||
class="btn"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{{ $t('settings.upload_a_photo') }}
|
|
||||||
</button>
|
|
||||||
<image-cropper
|
|
||||||
trigger="#pick-avatar"
|
|
||||||
:submit-handler="submitAvatar"
|
|
||||||
@open="pickAvatarBtnVisible=false"
|
|
||||||
@close="pickAvatarBtnVisible=true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.profile_banner') }}</h2>
|
|
||||||
<p>{{ $t('settings.current_profile_banner') }}</p>
|
|
||||||
<img
|
|
||||||
:src="user.cover_photo"
|
|
||||||
class="banner"
|
|
||||||
>
|
|
||||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
|
||||||
<img
|
|
||||||
v-if="bannerPreview"
|
|
||||||
class="banner"
|
|
||||||
:src="bannerPreview"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
@change="uploadFile('banner', $event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
v-if="bannerUploading"
|
|
||||||
class=" icon-spin4 animate-spin uploading"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-else-if="bannerPreview"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="submitBanner"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="bannerUploadError"
|
|
||||||
class="alert error"
|
|
||||||
>
|
|
||||||
Error: {{ bannerUploadError }}
|
|
||||||
<i
|
|
||||||
class="button-icon icon-cancel"
|
|
||||||
@click="clearUploadError('banner')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
|
||||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
|
||||||
<img
|
|
||||||
v-if="backgroundPreview"
|
|
||||||
class="bg"
|
|
||||||
:src="backgroundPreview"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
@change="uploadFile('background', $event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
v-if="backgroundUploading"
|
|
||||||
class=" icon-spin4 animate-spin uploading"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-else-if="backgroundPreview"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="submitBg"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="backgroundUploadError"
|
|
||||||
class="alert error"
|
|
||||||
>
|
|
||||||
Error: {{ backgroundUploadError }}
|
|
||||||
<i
|
|
||||||
class="button-icon icon-cancel"
|
|
||||||
@click="clearUploadError('background')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.security_tab')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.change_email') }}</h2>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.new_email') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="newEmail"
|
|
||||||
type="email"
|
|
||||||
autocomplete="email"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.current_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changeEmailPassword"
|
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="changeEmail"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<p v-if="changedEmail">
|
|
||||||
{{ $t('settings.changed_email') }}
|
|
||||||
</p>
|
|
||||||
<template v-if="changeEmailError !== false">
|
|
||||||
<p>{{ $t('settings.change_email_error') }}</p>
|
|
||||||
<p>{{ changeEmailError }}</p>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.change_password') }}</h2>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.current_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changePasswordInputs[0]"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.new_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changePasswordInputs[1]"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.confirm_new_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changePasswordInputs[2]"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="changePassword"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<p v-if="changedPassword">
|
|
||||||
{{ $t('settings.changed_password') }}
|
|
||||||
</p>
|
|
||||||
<p v-else-if="changePasswordError !== false">
|
|
||||||
{{ $t('settings.change_password_error') }}
|
|
||||||
</p>
|
|
||||||
<p v-if="changePasswordError">
|
|
||||||
{{ changePasswordError }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.oauth_tokens') }}</h2>
|
|
||||||
<table class="oauth-tokens">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ $t('settings.app_name') }}</th>
|
|
||||||
<th>{{ $t('settings.valid_until') }}</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="oauthToken in oauthTokens"
|
|
||||||
:key="oauthToken.id"
|
|
||||||
>
|
|
||||||
<td>{{ oauthToken.appName }}</td>
|
|
||||||
<td>{{ oauthToken.validUntil }}</td>
|
|
||||||
<td class="actions">
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="revokeToken(oauthToken.id)"
|
|
||||||
>
|
|
||||||
{{ $t('settings.revoke_token') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<mfa />
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.delete_account') }}</h2>
|
|
||||||
<p v-if="!deletingAccount">
|
|
||||||
{{ $t('settings.delete_account_description') }}
|
|
||||||
</p>
|
|
||||||
<div v-if="deletingAccount">
|
|
||||||
<p>{{ $t('settings.delete_account_instructions') }}</p>
|
|
||||||
<p>{{ $t('login.password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="deleteAccountConfirmPasswordInput"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="deleteAccount"
|
|
||||||
>
|
|
||||||
{{ $t('settings.delete_account') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p v-if="deleteAccountError !== false">
|
|
||||||
{{ $t('settings.delete_account_error') }}
|
|
||||||
</p>
|
|
||||||
<p v-if="deleteAccountError">
|
|
||||||
{{ deleteAccountError }}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
v-if="!deletingAccount"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="confirmDelete"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="pleromaBackend"
|
|
||||||
:label="$t('settings.notifications')"
|
|
||||||
>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
|
||||||
<div class="select-multiple">
|
|
||||||
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
|
||||||
<ul class="option-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.follows">
|
|
||||||
{{ $t('settings.notification_setting_follows') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.followers">
|
|
||||||
{{ $t('settings.notification_setting_followers') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.non_follows">
|
|
||||||
{{ $t('settings.notification_setting_non_follows') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.non_followers">
|
|
||||||
{{ $t('settings.notification_setting_non_followers') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="notificationSettings.privacy_option">
|
|
||||||
{{ $t('settings.notification_setting_privacy_option') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
|
||||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="updateNotificationSettings"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="pleromaBackend"
|
|
||||||
:label="$t('settings.data_import_export_tab')"
|
|
||||||
>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.follow_import') }}</h2>
|
|
||||||
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
|
||||||
<Importer
|
|
||||||
:submit-handler="importFollows"
|
|
||||||
:success-message="$t('settings.follows_imported')"
|
|
||||||
:error-message="$t('settings.follow_import_error')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.follow_export') }}</h2>
|
|
||||||
<Exporter
|
|
||||||
:get-content="getFollowsContent"
|
|
||||||
filename="friends.csv"
|
|
||||||
:export-button-label="$t('settings.follow_export_button')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.block_import') }}</h2>
|
|
||||||
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
|
||||||
<Importer
|
|
||||||
:submit-handler="importBlocks"
|
|
||||||
:success-message="$t('settings.blocks_imported')"
|
|
||||||
:error-message="$t('settings.block_import_error')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.block_export') }}</h2>
|
|
||||||
<Exporter
|
|
||||||
:get-content="getBlocksContent"
|
|
||||||
filename="blocks.csv"
|
|
||||||
:export-button-label="$t('settings.block_export_button')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.blocks_tab')">
|
|
||||||
<div class="profile-edit-usersearch-wrapper">
|
|
||||||
<Autosuggest
|
|
||||||
:filter="filterUnblockedUsers"
|
|
||||||
:query="queryUserIds"
|
|
||||||
:placeholder="$t('settings.search_user_to_block')"
|
|
||||||
>
|
|
||||||
<BlockCard
|
|
||||||
slot-scope="row"
|
|
||||||
:user-id="row.item"
|
|
||||||
/>
|
|
||||||
</Autosuggest>
|
|
||||||
</div>
|
|
||||||
<BlockList
|
|
||||||
:refresh="true"
|
|
||||||
:get-key="identity"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
slot="header"
|
|
||||||
slot-scope="{selected}"
|
|
||||||
>
|
|
||||||
<div class="profile-edit-bulk-actions">
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => blockUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.block') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.block_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => unblockUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.unblock') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.unblock_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
slot="item"
|
|
||||||
slot-scope="{item}"
|
|
||||||
>
|
|
||||||
<BlockCard :user-id="item" />
|
|
||||||
</template>
|
|
||||||
<template slot="empty">
|
|
||||||
{{ $t('settings.no_blocks') }}
|
|
||||||
</template>
|
|
||||||
</BlockList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.mutes_tab')">
|
|
||||||
<tab-switcher>
|
|
||||||
<div label="Users">
|
|
||||||
<div class="profile-edit-usersearch-wrapper">
|
|
||||||
<Autosuggest
|
|
||||||
:filter="filterUnMutedUsers"
|
|
||||||
:query="queryUserIds"
|
|
||||||
:placeholder="$t('settings.search_user_to_mute')"
|
|
||||||
>
|
|
||||||
<MuteCard
|
|
||||||
slot-scope="row"
|
|
||||||
:user-id="row.item"
|
|
||||||
/>
|
|
||||||
</Autosuggest>
|
|
||||||
</div>
|
|
||||||
<MuteList
|
|
||||||
:refresh="true"
|
|
||||||
:get-key="identity"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
slot="header"
|
|
||||||
slot-scope="{selected}"
|
|
||||||
>
|
|
||||||
<div class="profile-edit-bulk-actions">
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => muteUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.mute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.mute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => unmuteUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.unmute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.unmute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
slot="item"
|
|
||||||
slot-scope="{item}"
|
|
||||||
>
|
|
||||||
<MuteCard :user-id="item" />
|
|
||||||
</template>
|
|
||||||
<template slot="empty">
|
|
||||||
{{ $t('settings.no_mutes') }}
|
|
||||||
</template>
|
|
||||||
</MuteList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.domain_mutes')">
|
|
||||||
<div class="profile-edit-domain-mute-form">
|
|
||||||
<input
|
|
||||||
v-model="newDomainToMute"
|
|
||||||
:placeholder="$t('settings.type_domains_to_mute')"
|
|
||||||
type="text"
|
|
||||||
@keyup.enter="muteDomain"
|
|
||||||
>
|
|
||||||
<ProgressButton
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="muteDomain"
|
|
||||||
>
|
|
||||||
{{ $t('domain_mute_card.mute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('domain_mute_card.mute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
<DomainMuteList
|
|
||||||
:refresh="true"
|
|
||||||
:get-key="identity"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
slot="header"
|
|
||||||
slot-scope="{selected}"
|
|
||||||
>
|
|
||||||
<div class="profile-edit-bulk-actions">
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => unmuteDomains(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('domain_mute_card.unmute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('domain_mute_card.unmute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
slot="item"
|
|
||||||
slot-scope="{item}"
|
|
||||||
>
|
|
||||||
<DomainMuteCard :domain="item" />
|
|
||||||
</template>
|
|
||||||
<template slot="empty">
|
|
||||||
{{ $t('settings.no_mutes') }}
|
|
||||||
</template>
|
|
||||||
</DomainMuteList>
|
|
||||||
</div>
|
|
||||||
</tab-switcher>
|
|
||||||
</div>
|
|
||||||
</tab-switcher>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./user_settings.js">
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.profile-edit {
|
|
||||||
.bio {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visibility-tray {
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=file] {
|
|
||||||
padding: 5px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploading {
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-changer {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-avatar {
|
|
||||||
display: block;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-tokens {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-usersearch-wrapper {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-bulk-actions {
|
|
||||||
text-align: right;
|
|
||||||
padding: 0 1em;
|
|
||||||
min-height: 28px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-domain-mute-form {
|
|
||||||
padding: 1em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
button {
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-top: 1em;
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-subitem {
|
|
||||||
margin-left: 1.75em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -59,7 +59,10 @@
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
|
"loading": "Loading…",
|
||||||
"generic_error": "An error occured",
|
"generic_error": "An error occured",
|
||||||
|
"error_retry": "Please try again",
|
||||||
|
"retry": "Try again",
|
||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"show_more": "Show more",
|
"show_more": "Show more",
|
||||||
"show_less": "Show less",
|
"show_less": "Show less",
|
||||||
|
@ -68,7 +71,9 @@
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"verify": "Verify"
|
"verify": "Verify",
|
||||||
|
"close": "Close",
|
||||||
|
"peek": "Peek"
|
||||||
},
|
},
|
||||||
"image_cropper": {
|
"image_cropper": {
|
||||||
"crop_picture": "Crop picture",
|
"crop_picture": "Crop picture",
|
||||||
|
@ -278,6 +283,7 @@
|
||||||
"current_avatar": "Your current avatar",
|
"current_avatar": "Your current avatar",
|
||||||
"current_password": "Current password",
|
"current_password": "Current password",
|
||||||
"current_profile_banner": "Your current profile banner",
|
"current_profile_banner": "Your current profile banner",
|
||||||
|
"mutes_and_blocks": "Mutes and Blocks",
|
||||||
"data_import_export_tab": "Data Import / Export",
|
"data_import_export_tab": "Data Import / Export",
|
||||||
"default_vis": "Default visibility scope",
|
"default_vis": "Default visibility scope",
|
||||||
"delete_account": "Delete Account",
|
"delete_account": "Delete Account",
|
||||||
|
|
|
@ -3,6 +3,16 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
||||||
|
|
||||||
const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||||
|
|
||||||
|
/* TODO this is a bit messy.
|
||||||
|
* We need to declare settings with their types and also deal with
|
||||||
|
* instance-default settings in some way, hopefully try to avoid copy-pasta
|
||||||
|
* in general.
|
||||||
|
*/
|
||||||
|
export const multiChoiceProperties = [
|
||||||
|
'postContentType',
|
||||||
|
'subjectLineBehavior'
|
||||||
|
]
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
colors: {},
|
colors: {},
|
||||||
theme: undefined,
|
theme: undefined,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { set, delete as del } from 'vue'
|
import { set, delete as del } from 'vue'
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
settingsModalState: 'hidden',
|
||||||
|
settingsModalLoaded: false,
|
||||||
settings: {
|
settings: {
|
||||||
currentSaveStateNotice: null,
|
currentSaveStateNotice: null,
|
||||||
noticeClearTimeout: null,
|
noticeClearTimeout: null,
|
||||||
|
@ -35,6 +37,27 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
setMobileLayout (state, value) {
|
setMobileLayout (state, value) {
|
||||||
state.mobileLayout = value
|
state.mobileLayout = value
|
||||||
|
},
|
||||||
|
closeSettingsModal (state) {
|
||||||
|
state.settingsModalState = 'hidden'
|
||||||
|
},
|
||||||
|
togglePeekSettingsModal (state) {
|
||||||
|
switch (state.settingsModalState) {
|
||||||
|
case 'minimized':
|
||||||
|
state.settingsModalState = 'visible'
|
||||||
|
return
|
||||||
|
case 'visible':
|
||||||
|
state.settingsModalState = 'minimized'
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
throw new Error('Illegal minimization state of settings modal')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openSettingsModal (state) {
|
||||||
|
state.settingsModalState = 'visible'
|
||||||
|
if (!state.settingsModalLoaded) {
|
||||||
|
state.settingsModalLoaded = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -49,6 +72,15 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
setMobileLayout ({ commit }, value) {
|
setMobileLayout ({ commit }, value) {
|
||||||
commit('setMobileLayout', value)
|
commit('setMobileLayout', value)
|
||||||
|
},
|
||||||
|
closeSettingsModal ({ commit }) {
|
||||||
|
commit('closeSettingsModal')
|
||||||
|
},
|
||||||
|
openSettingsModal ({ commit }) {
|
||||||
|
commit('openSettingsModal')
|
||||||
|
},
|
||||||
|
togglePeekSettingsModal ({ commit }) {
|
||||||
|
commit('togglePeekSettingsModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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
|
|
@ -363,6 +363,18 @@
|
||||||
"css": "ok",
|
"css": "ok",
|
||||||
"code": 59431,
|
"code": 59431,
|
||||||
"src": "fontawesome"
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "4109c474ff99cad28fd5a2c38af2ec6f",
|
||||||
|
"css": "filter",
|
||||||
|
"code": 61616,
|
||||||
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
|
||||||
|
"css": "download",
|
||||||
|
"code": 59429,
|
||||||
|
"src": "fontawesome"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
15
yarn.lock
15
yarn.lock
|
@ -2327,6 +2327,7 @@ dateformat@^1.0.6:
|
||||||
de-indent@^1.0.2:
|
de-indent@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||||
|
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
|
||||||
|
|
||||||
debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
|
@ -7903,9 +7904,10 @@ vue-style-loader@^4.0.0, vue-style-loader@^4.0.1:
|
||||||
hash-sum "^1.0.2"
|
hash-sum "^1.0.2"
|
||||||
loader-utils "^1.0.2"
|
loader-utils "^1.0.2"
|
||||||
|
|
||||||
vue-template-compiler@^2.3.4:
|
vue-template-compiler@^2.6.11:
|
||||||
version "2.5.21"
|
version "2.6.11"
|
||||||
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.21.tgz#a57ceb903177e8f643560a8d639a0f8db647054a"
|
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
|
||||||
|
integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
|
||||||
dependencies:
|
dependencies:
|
||||||
de-indent "^1.0.2"
|
de-indent "^1.0.2"
|
||||||
he "^1.1.0"
|
he "^1.1.0"
|
||||||
|
@ -7914,9 +7916,10 @@ vue-template-es2015-compiler@^1.6.0:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||||
|
|
||||||
vue@^2.5.13:
|
vue@^2.6.11:
|
||||||
version "2.5.21"
|
version "2.6.11"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
||||||
|
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
|
||||||
|
|
||||||
vuelidate@^0.7.4:
|
vuelidate@^0.7.4:
|
||||||
version "0.7.4"
|
version "0.7.4"
|
||||||
|
|
Loading…
Reference in a new issue