Merge branch 'settings-modal' into 'develop'
Settings modal See merge request pleroma/pleroma-fe!1118
This commit is contained in:
commit
a5de8db579
|
@ -29,11 +29,11 @@
|
|||
"portal-vue": "^2.1.4",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"vue": "^2.5.13",
|
||||
"vue": "^2.6.11",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.4",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuelidate": "^0.7.4",
|
||||
"vuex": "^3.0.1",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
|
|
|
@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
|||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
|
@ -29,6 +30,7 @@ export default {
|
|||
SideDrawer,
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
SettingsModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal
|
||||
},
|
||||
|
@ -117,6 +119,9 @@ export default {
|
|||
onSearchBarToggled (hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
updateMobileState () {
|
||||
const mobileLayout = windowWidth() <= 800
|
||||
const changed = mobileLayout !== this.isMobileLayout
|
||||
|
|
47
src/App.scss
47
src/App.scss
|
@ -566,7 +566,7 @@ main-router {
|
|||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
margin-left: .25em;
|
||||
margin-left: .5em;
|
||||
min-width: 1px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
@ -860,51 +860,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||
margin: 1em 1em 1.4em;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
> div {
|
||||
margin-bottom: .5em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unavailable,
|
||||
.unavailable i {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
}
|
||||
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
}
|
||||
}
|
||||
.select-multiple {
|
||||
display: flex;
|
||||
.option-list {
|
||||
|
|
|
@ -46,15 +46,16 @@
|
|||
@toggled="onSearchBarToggled"
|
||||
@click.stop.native
|
||||
/>
|
||||
<router-link
|
||||
<a
|
||||
href="#"
|
||||
class="mobile-hidden"
|
||||
:to="{ name: 'settings'}"
|
||||
@click.stop="openSettingsModal"
|
||||
>
|
||||
<i
|
||||
class="button-icon icon-cog nav-icon"
|
||||
:title="$t('nav.preferences')"
|
||||
/>
|
||||
</router-link>
|
||||
</a>
|
||||
<a
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
href="/pleroma/admin/#/login-pleroma"
|
||||
|
@ -125,6 +126,7 @@
|
|||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<SettingsModal />
|
||||
<portal-target name="modal" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -7,10 +7,8 @@ import Interactions from 'components/interactions/interactions.vue'
|
|||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import Settings from 'components/settings/settings.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||
import UserSettings from 'components/user_settings/user_settings.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
|
@ -56,12 +54,10 @@ export default (store) => {
|
|||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'settings', path: '/settings', component: Settings },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
|
|
|
@ -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>
|
||||
<div
|
||||
v-show="isOpen"
|
||||
v-body-scroll-lock="isOpen"
|
||||
v-body-scroll-lock="isOpen && !noBackground"
|
||||
class="modal-view"
|
||||
:class="classes"
|
||||
@click.self="$emit('backdropClicked')"
|
||||
>
|
||||
<slot />
|
||||
|
@ -15,6 +16,18 @@ export default {
|
|||
isOpen: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
noBackground: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return {
|
||||
'modal-background': !this.noBackground,
|
||||
'open': this.isOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,12 +45,22 @@ export default {
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
pointer-events: none;
|
||||
animation-duration: 0.2s;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation-name: modal-background-fadein;
|
||||
opacity: 0;
|
||||
|
||||
body:not(.scroll-locked) & {
|
||||
opacity: 0;
|
||||
> * {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
&.modal-background {
|
||||
pointer-events: initial;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.postStatus.modalActivated
|
||||
},
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<Modal
|
||||
v-if="isLoggedIn && !resettingForm"
|
||||
:is-open="modalActivated"
|
||||
class="post-form-modal-view"
|
||||
@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
|
|
@ -0,0 +1,42 @@
|
|||
import Modal from 'src/components/modal/modal.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
|
||||
const SettingsModal = {
|
||||
components: {
|
||||
Modal,
|
||||
SettingsModalContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_content.vue'),
|
||||
{
|
||||
loading: PanelLoading,
|
||||
error: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeSettingsModal')
|
||||
},
|
||||
peekModal () {
|
||||
this.$store.dispatch('togglePeekSettingsModal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentSaveStateNotice () {
|
||||
return this.$store.state.interface.settings.currentSaveStateNotice
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||
},
|
||||
modalOpenedOnce () {
|
||||
return this.$store.state.interface.settingsModalLoaded
|
||||
},
|
||||
modalPeeked () {
|
||||
return this.$store.state.interface.settingsModalState === 'minimized'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModal
|
|
@ -0,0 +1,44 @@
|
|||
@import 'src/_variables.scss';
|
||||
.settings-modal {
|
||||
overflow: hidden;
|
||||
|
||||
&.peek {
|
||||
.settings-modal-panel {
|
||||
/* Explanation:
|
||||
* Modal is positioned vertically centered.
|
||||
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||
* bottom of the screen
|
||||
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||
*/
|
||||
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||
}
|
||||
}
|
||||
|
||||
.settings-modal-panel {
|
||||
overflow: hidden;
|
||||
transition: transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 300ms;
|
||||
width: 1000px;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<Modal
|
||||
:is-open="modalActivated"
|
||||
class="settings-modal"
|
||||
:class="{ peek: modalPeeked }"
|
||||
:no-background="modalPeeked"
|
||||
>
|
||||
<div class="settings-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t('settings.settings') }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<template v-if="currentSaveStateNotice">
|
||||
<div
|
||||
v-if="currentSaveStateNotice.error"
|
||||
class="alert error"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_err') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!currentSaveStateNotice.error"
|
||||
class="alert transparent"
|
||||
@click.prevent
|
||||
>
|
||||
{{ $t('settings.saving_ok') }}
|
||||
</div>
|
||||
</template>
|
||||
</transition>
|
||||
<button
|
||||
class="btn"
|
||||
@click="peekModal"
|
||||
>
|
||||
{{ $t('general.peek') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('general.close') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal.js"></script>
|
||||
|
||||
<style src="./settings_modal.scss" lang="scss"></style>
|
|
@ -0,0 +1,34 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
|
||||
import DataImportExportTab from './tabs/data_import_export_tab.vue'
|
||||
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
|
||||
import NotificationsTab from './tabs/notifications_tab.vue'
|
||||
import FilteringTab from './tabs/filtering_tab.vue'
|
||||
import SecurityTab from './tabs/security_tab/security_tab.vue'
|
||||
import ProfileTab from './tabs/profile_tab.vue'
|
||||
import GeneralTab from './tabs/general_tab.vue'
|
||||
import VersionTab from './tabs/version_tab.vue'
|
||||
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
|
||||
|
||||
const SettingsModalContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
|
||||
DataImportExportTab,
|
||||
MutesAndBlocksTab,
|
||||
NotificationsTab,
|
||||
FilteringTab,
|
||||
SecurityTab,
|
||||
ProfileTab,
|
||||
GeneralTab,
|
||||
VersionTab,
|
||||
ThemeTab
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModalContent
|
|
@ -0,0 +1,43 @@
|
|||
@import 'src/_variables.scss';
|
||||
.settings_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||
margin: 1em 1em 1.4em;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
> div {
|
||||
margin-bottom: .5em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unavailable,
|
||||
.unavailable i {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="settings_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
>
|
||||
<div
|
||||
:label="$t('settings.general')"
|
||||
icon="wrench"
|
||||
>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.profile_tab')"
|
||||
icon="user"
|
||||
>
|
||||
<ProfileTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.security_tab')"
|
||||
icon="lock"
|
||||
>
|
||||
<SecurityTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.filtering')"
|
||||
icon="filter"
|
||||
>
|
||||
<FilteringTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.theme')"
|
||||
icon="brush"
|
||||
>
|
||||
<ThemeTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.notifications')"
|
||||
icon="bell-ringing-o"
|
||||
>
|
||||
<NotificationsTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
icon="download"
|
||||
>
|
||||
<DataImportExportTab />
|
||||
</div>
|
||||
<div
|
||||
v-if="isLoggedIn"
|
||||
:label="$t('settings.mutes_and_blocks')"
|
||||
:fullHeight="true"
|
||||
icon="eye-off"
|
||||
>
|
||||
<MutesAndBlocksTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('settings.version.title')"
|
||||
icon="info-circled"
|
||||
>
|
||||
<VersionTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./settings_modal_content.js"></script>
|
||||
|
||||
<style src="./settings_modal_content.scss" lang="scss"></style>
|
|
@ -0,0 +1,65 @@
|
|||
import Importer from 'src/components/importer/importer.vue'
|
||||
import Exporter from 'src/components/exporter/exporter.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const DataImportExportTab = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
Importer,
|
||||
Exporter,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFollowsContent () {
|
||||
return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
getBlocksContent () {
|
||||
return this.$store.state.api.backendInteractor.fetchBlocks()
|
||||
.then(this.generateExportableUsersContent)
|
||||
},
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent (users) {
|
||||
// Get addresses
|
||||
return users.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DataImportExportTab
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div
|
||||
:label="$t('settings.data_import_export_tab')"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_import') }}</h2>
|
||||
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importFollows"
|
||||
:success-message="$t('settings.follows_imported')"
|
||||
:error-message="$t('settings.follow_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.follow_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getFollowsContent"
|
||||
filename="friends.csv"
|
||||
:export-button-label="$t('settings.follow_export_button')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_import') }}</h2>
|
||||
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
||||
<Importer
|
||||
:submit-handler="importBlocks"
|
||||
:success-message="$t('settings.blocks_imported')"
|
||||
:error-message="$t('settings.block_import_error')"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.block_export') }}</h2>
|
||||
<Exporter
|
||||
:get-content="getBlocksContent"
|
||||
filename="blocks.csv"
|
||||
:export-button-label="$t('settings.block_export_button')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./data_import_export_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
|
@ -0,0 +1,44 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const FilteringTab = {
|
||||
data () {
|
||||
return {
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
muteWordsString: {
|
||||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
notificationVisibility: {
|
||||
handler (value) {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'notificationVisibility',
|
||||
value: this.$store.getters.mergedConfig.notificationVisibility
|
||||
})
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FilteringTab
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div :label="$t('settings.filtering')">
|
||||
<div class="setting-item">
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.likes">
|
||||
{{ $t('settings.notification_visibility_likes') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.repeats">
|
||||
{{ $t('settings.notification_visibility_repeats') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.follows">
|
||||
{{ $t('settings.notification_visibility_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.mentions">
|
||||
{{ $t('settings.notification_visibility_mentions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.moves">
|
||||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.emojiReactions">
|
||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
<label
|
||||
for="replyVisibility"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="replyVisibility"
|
||||
v-model="replyVisibility"
|
||||
>
|
||||
<option
|
||||
value="all"
|
||||
selected
|
||||
>{{ $t('settings.reply_visibility_all') }}</option>
|
||||
<option value="following">{{ $t('settings.reply_visibility_following') }}</option>
|
||||
<option value="self">{{ $t('settings.reply_visibility_self') }}</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<p>{{ $t('settings.filtering_explanation') }}</p>
|
||||
<textarea
|
||||
id="muteWords"
|
||||
v-model="muteWordsString"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox v-model="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./filtering_tab.js"></script>
|
|
@ -0,0 +1,31 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const GeneralTab = {
|
||||
data () {
|
||||
return {
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
// Chrome-likes
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
InterfaceLanguageSwitcher
|
||||
},
|
||||
computed: {
|
||||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
...SharedComputedObject()
|
||||
}
|
||||
}
|
||||
|
||||
export default GeneralTab
|
|
@ -0,0 +1,272 @@
|
|||
<template>
|
||||
<div :label="$t('settings.general')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher />
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<Checkbox v-model="hideISP">
|
||||
{{ $t('settings.hide_isp') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('nav.timeline') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideMutedPosts">
|
||||
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="collapseMessageWithSubject">
|
||||
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="streaming">
|
||||
{{ $t('settings.streaming') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="pauseOnUnfocused"
|
||||
:disabled="!streaming"
|
||||
>
|
||||
{{ $t('settings.pause_on_unfocused') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useStreamingApi">
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autoLoad">
|
||||
{{ $t('settings.autoload') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hoverPreview">
|
||||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.composing') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="scopeCopy">
|
||||
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="alwaysShowSubjectInput">
|
||||
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
{{ $t('settings.subject_line_behavior') }}
|
||||
<label
|
||||
for="subjectLineBehavior"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="subjectLineBehavior"
|
||||
v-model="subjectLineBehavior"
|
||||
>
|
||||
<option value="email">
|
||||
{{ $t('settings.subject_line_email') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="masto">
|
||||
{{ $t('settings.subject_line_mastodon') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
<option value="noop">
|
||||
{{ $t('settings.subject_line_noop') }}
|
||||
{{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="postFormats.length > 0">
|
||||
<div>
|
||||
{{ $t('settings.post_status_content_type') }}
|
||||
<label
|
||||
for="postContentType"
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="postContentType"
|
||||
v-model="postContentType"
|
||||
>
|
||||
<option
|
||||
v-for="postFormat in postFormats"
|
||||
:key="postFormat"
|
||||
:value="postFormat"
|
||||
>
|
||||
{{ $t(`post_status.content_type["${postFormat}"]`) }}
|
||||
{{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open" />
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="minimalScopesMode">
|
||||
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="autohideFloatingPostButton">
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="padEmoji">
|
||||
{{ $t('settings.pad_emoji') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.attachments') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<label for="maxThumbnails">
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</label>
|
||||
<input
|
||||
id="maxThumbnails"
|
||||
v-model.number="maxThumbnails"
|
||||
class="number-input"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="hideNsfw">
|
||||
{{ $t('settings.nsfw_clickthrough') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="preloadImage"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.preload_images') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="useOneClickNsfw"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.use_one_click_nsfw') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<Checkbox v-model="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="loopVideo">
|
||||
{{ $t('settings.loop_video') }}
|
||||
</Checkbox>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
:class="[{disabled: !streaming}]"
|
||||
>
|
||||
<li>
|
||||
<Checkbox
|
||||
v-model="loopVideoSilentOnly"
|
||||
:disabled="!loopVideo || !loopSilentAvailable"
|
||||
>
|
||||
{{ $t('settings.loop_video_silent_only') }}
|
||||
</Checkbox>
|
||||
<div
|
||||
v-if="!loopSilentAvailable"
|
||||
class="unavailable"
|
||||
>
|
||||
<i class="icon-globe" />! {{ $t('settings.limited_availability') }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="playVideosInModal">
|
||||
{{ $t('settings.play_videos_in_modal') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="useContainFit">
|
||||
{{ $t('settings.use_contain_fit') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notifications') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="webPushNotifications">
|
||||
{{ $t('settings.enable_web_push_notifications') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.fun') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<Checkbox v-model="greentext">
|
||||
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./general_tab.js"></script>
|
|
@ -0,0 +1,124 @@
|
|||
import get from 'lodash/get'
|
||||
import map from 'lodash/map'
|
||||
import reject from 'lodash/reject'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import BlockCard from 'src/components/block_card/block_card.vue'
|
||||
import MuteCard from 'src/components/mute_card/mute_card.vue'
|
||||
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
|
||||
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const DomainMuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MutesAndBlocks = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
TabSwitcher,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
ProgressButton,
|
||||
Autosuggest,
|
||||
Checkbox
|
||||
},
|
||||
methods: {
|
||||
importFollows (file) {
|
||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
importBlocks (file) {
|
||||
return this.$store.state.api.backendInteractor.importBlocks({ file })
|
||||
.then((status) => {
|
||||
if (!status) {
|
||||
throw new Error('failed')
|
||||
}
|
||||
})
|
||||
},
|
||||
generateExportableUsersContent (users) {
|
||||
// Get addresses
|
||||
return users.map((user) => {
|
||||
// check is it's a local user
|
||||
if (user && user.is_local) {
|
||||
// append the instance address
|
||||
// eslint-disable-next-line no-undef
|
||||
return user.screen_name + '@' + location.hostname
|
||||
}
|
||||
return user.screen_name
|
||||
}).join('\n')
|
||||
},
|
||||
activateTab (tabName) {
|
||||
this.activeTab = tabName
|
||||
},
|
||||
filterUnblockedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.blocking || userId === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
filterUnMutedUsers (userIds) {
|
||||
return reject(userIds, (userId) => {
|
||||
const relationship = this.$store.getters.relationship(this.userId)
|
||||
return relationship.muting || userId === this.$store.state.users.currentUser.id
|
||||
})
|
||||
},
|
||||
queryUserIds (query) {
|
||||
return this.$store.dispatch('searchUsers', { query })
|
||||
.then((users) => map(users, 'id'))
|
||||
},
|
||||
blockUsers (ids) {
|
||||
return this.$store.dispatch('blockUsers', ids)
|
||||
},
|
||||
unblockUsers (ids) {
|
||||
return this.$store.dispatch('unblockUsers', ids)
|
||||
},
|
||||
muteUsers (ids) {
|
||||
return this.$store.dispatch('muteUsers', ids)
|
||||
},
|
||||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
unmuteDomains (domains) {
|
||||
return this.$store.dispatch('unmuteDomains', domains)
|
||||
},
|
||||
muteDomain () {
|
||||
return this.$store.dispatch('muteDomain', this.newDomainToMute)
|
||||
.then(() => { this.newDomainToMute = '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MutesAndBlocks
|
|
@ -0,0 +1,29 @@
|
|||
.mutes-and-blocks-tab {
|
||||
height: 100%;
|
||||
|
||||
.usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.bulk-action-button {
|
||||
width: 10em
|
||||
}
|
||||
|
||||
.domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.domain-mute-button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
:scrollable-tabs="true"
|
||||
class="mutes-and-blocks-tab"
|
||||
>
|
||||
<div :label="$t('settings.blocks_tab')">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnblockedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_block')"
|
||||
>
|
||||
<BlockCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<BlockList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default bulk-action-button"
|
||||
:click="() => blockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unblockUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<BlockCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_blocks') }}
|
||||
</template>
|
||||
</BlockList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<tab-switcher>
|
||||
<div label="Users">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
<MuteCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.domain_mutes')">
|
||||
<div class="domain-mute-form">
|
||||
<input
|
||||
v-model="newDomainToMute"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
type="text"
|
||||
@keyup.enter="muteDomain"
|
||||
>
|
||||
<ProgressButton
|
||||
class="btn btn-default domain-mute-button"
|
||||
:click="muteDomain"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
<DomainMuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</DomainMuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./mutes_and_blocks_tab.js"></script>
|
||||
<style lang="scss" src="./mutes_and_blocks_tab.scss"></style>
|
|
@ -0,0 +1,27 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const NotificationsTab = {
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'profile',
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateNotificationSettings () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateNotificationSettings({ settings: this.notificationSettings })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationsTab
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div :label="$t('settings.notifications')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||
<div class="select-multiple">
|
||||
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.follows">
|
||||
{{ $t('settings.notification_setting_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.followers">
|
||||
{{ $t('settings.notification_setting_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_follows">
|
||||
{{ $t('settings.notification_setting_non_follows') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationSettings.non_followers">
|
||||
{{ $t('settings.notification_setting_non_followers') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
||||
<p>
|
||||
<Checkbox v-model="notificationSettings.privacy_option">
|
||||
{{ $t('settings.notification_setting_privacy_option') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="updateNotificationSettings"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./notifications_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
|
@ -0,0 +1,179 @@
|
|||
import unescape from 'lodash/unescape'
|
||||
import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
|
||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||
import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const ProfileTab = {
|
||||
data () {
|
||||
return {
|
||||
newName: this.$store.state.users.currentUser.name,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
newLocked: this.$store.state.users.currentUser.locked,
|
||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
||||
hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
pickAvatarBtnVisible: true,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
banner: null,
|
||||
bannerPreview: null,
|
||||
background: null,
|
||||
backgroundPreview: null,
|
||||
bannerUploadError: null,
|
||||
backgroundUploadError: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ScopeSelector,
|
||||
ImageCropper,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
ProgressButton,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
return suggestor({ emoji: [
|
||||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
] })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
params: {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
display_name: this.newName,
|
||||
default_scope: this.newDefaultScope,
|
||||
no_rich_text: this.newNoRichText,
|
||||
hide_follows: this.hideFollows,
|
||||
hide_followers: this.hideFollowers,
|
||||
discoverable: this.discoverable,
|
||||
allow_following_move: this.allowFollowingMove,
|
||||
hide_follows_count: this.hideFollowsCount,
|
||||
hide_followers_count: this.hideFollowersCount,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
} }).then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
})
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.newDefaultScope = visibility
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) { return }
|
||||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
this[slot + 'UploadError'] = [
|
||||
this.$t('upload.error.base'),
|
||||
this.$t(
|
||||
'upload.error.file_too_big',
|
||||
{
|
||||
filesize: filesize.num,
|
||||
filesizeunit: filesize.unit,
|
||||
allowedsize: allowedsize.num,
|
||||
allowedsizeunit: allowedsize.unit
|
||||
}
|
||||
)
|
||||
].join(' ')
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
const img = target.result
|
||||
this[slot + 'Preview'] = img
|
||||
this[slot] = file
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar (cropper, file) {
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar) {
|
||||
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
resolve()
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
|
||||
})
|
||||
}
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
||||
} else {
|
||||
updateAvatar(file)
|
||||
}
|
||||
})
|
||||
},
|
||||
submitBanner () {
|
||||
if (!this.bannerPreview) { return }
|
||||
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.bannerPreview = null
|
||||
})
|
||||
.catch((err) => {
|
||||
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
|
||||
})
|
||||
.then(() => { this.bannerUploading = false })
|
||||
},
|
||||
submitBg () {
|
||||
if (!this.backgroundPreview) { return }
|
||||
let background = this.background
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
||||
if (!data.error) {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.backgroundPreview = null
|
||||
} else {
|
||||
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||
}
|
||||
this.backgroundUploading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileTab
|
|
@ -0,0 +1,82 @@
|
|||
@import '../../../_variables.scss';
|
||||
.profile-tab {
|
||||
.bio {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.visibility-tray {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
input[type=file] {
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.banner {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.uploading {
|
||||
font-size: 1.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.name-changer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.current-avatar {
|
||||
display: block;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
}
|
||||
|
||||
.oauth-tokens {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&-usersearch-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
&-bulk-actions {
|
||||
text-align: right;
|
||||
padding: 0 1em;
|
||||
min-height: 28px;
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
&-domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-subitem {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
<template>
|
||||
<div class="profile-tab">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
||||
<p>{{ $t('settings.name') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newName"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiSuggestor"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="newName"
|
||||
classname="name-changer"
|
||||
>
|
||||
</EmojiInput>
|
||||
<p>{{ $t('settings.bio') }}</p>
|
||||
<EmojiInput
|
||||
v-model="newBio"
|
||||
enable-emoji-picker
|
||||
:suggest="emojiUserSuggestor"
|
||||
>
|
||||
<textarea
|
||||
v-model="newBio"
|
||||
classname="bio"
|
||||
/>
|
||||
</EmojiInput>
|
||||
<p>
|
||||
<Checkbox v-model="newLocked">
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<div>
|
||||
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
|
||||
<div
|
||||
id="default-vis"
|
||||
class="visibility-tray"
|
||||
>
|
||||
<scope-selector
|
||||
:show-all="true"
|
||||
:user-default="newDefaultScope"
|
||||
:initial-scope="newDefaultScope"
|
||||
:on-scope-change="changeVis"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<Checkbox v-model="newNoRichText">
|
||||
{{ $t('settings.no_rich_text_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="hideFollows">
|
||||
{{ $t('settings.hide_follows_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p class="setting-subitem">
|
||||
<Checkbox
|
||||
v-model="hideFollowsCount"
|
||||
:disabled="!hideFollows"
|
||||
>
|
||||
{{ $t('settings.hide_follows_count_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="hideFollowers">
|
||||
{{ $t('settings.hide_followers_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p class="setting-subitem">
|
||||
<Checkbox
|
||||
v-model="hideFollowersCount"
|
||||
:disabled="!hideFollowers"
|
||||
>
|
||||
{{ $t('settings.hide_followers_count_description') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="allowFollowingMove">
|
||||
{{ $t('settings.allow_following_move') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p v-if="role === 'admin' || role === 'moderator'">
|
||||
<Checkbox v-model="showRole">
|
||||
<template v-if="role === 'admin'">
|
||||
{{ $t('settings.show_admin_badge') }}
|
||||
</template>
|
||||
<template v-if="role === 'moderator'">
|
||||
{{ $t('settings.show_moderator_badge') }}
|
||||
</template>
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="discoverable">
|
||||
{{ $t('settings.discoverable') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<button
|
||||
:disabled="newName && newName.length === 0"
|
||||
class="btn btn-default"
|
||||
@click="updateProfile"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.avatar') }}</h2>
|
||||
<p class="visibility-notice">
|
||||
{{ $t('settings.avatar_size_instruction') }}
|
||||
</p>
|
||||
<p>{{ $t('settings.current_avatar') }}</p>
|
||||
<img
|
||||
:src="user.profile_image_url_original"
|
||||
class="current-avatar"
|
||||
>
|
||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
||||
<button
|
||||
v-show="pickAvatarBtnVisible"
|
||||
id="pick-avatar"
|
||||
class="btn"
|
||||
type="button"
|
||||
>
|
||||
{{ $t('settings.upload_a_photo') }}
|
||||
</button>
|
||||
<image-cropper
|
||||
trigger="#pick-avatar"
|
||||
:submit-handler="submitAvatar"
|
||||
@open="pickAvatarBtnVisible=false"
|
||||
@close="pickAvatarBtnVisible=true"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.profile_banner') }}</h2>
|
||||
<p>{{ $t('settings.current_profile_banner') }}</p>
|
||||
<img
|
||||
:src="user.cover_photo"
|
||||
class="banner"
|
||||
>
|
||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
||||
<img
|
||||
v-if="bannerPreview"
|
||||
class="banner"
|
||||
:src="bannerPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
@change="uploadFile('banner', $event)"
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
v-if="bannerUploading"
|
||||
class=" icon-spin4 animate-spin uploading"
|
||||
/>
|
||||
<button
|
||||
v-else-if="bannerPreview"
|
||||
class="btn btn-default"
|
||||
@click="submitBanner"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="bannerUploadError"
|
||||
class="alert error"
|
||||
>
|
||||
Error: {{ bannerUploadError }}
|
||||
<i
|
||||
class="button-icon icon-cancel"
|
||||
@click="clearUploadError('banner')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
||||
<img
|
||||
v-if="backgroundPreview"
|
||||
class="bg"
|
||||
:src="backgroundPreview"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
@change="uploadFile('background', $event)"
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
v-if="backgroundUploading"
|
||||
class=" icon-spin4 animate-spin uploading"
|
||||
/>
|
||||
<button
|
||||
v-else-if="backgroundPreview"
|
||||
class="btn btn-default"
|
||||
@click="submitBg"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="backgroundUploadError"
|
||||
class="alert error"
|
||||
>
|
||||
Error: {{ backgroundUploadError }}
|
||||
<i
|
||||
class="button-icon icon-cancel"
|
||||
@click="clearUploadError('background')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./profile_tab.js"></script>
|
||||
<style lang="scss" src="./profile_tab.scss"></style>
|
|
@ -137,11 +137,7 @@
|
|||
|
||||
<script src="./mfa.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
@import '../../../../_variables.scss';
|
||||
.mfa-settings {
|
||||
.mfa-heading, .method-item {
|
||||
overflow: hidden;
|
||||
|
@ -151,6 +147,11 @@
|
|||
align-items: baseline;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
|
||||
.setup-otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mfa-backup-codes">
|
||||
<h4 v-if="displayTitle">
|
||||
{{ $t('settings.mfa.recovery_codes') }}
|
||||
</h4>
|
||||
|
@ -21,13 +21,15 @@
|
|||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../../../../_variables.scss';
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
.mfa-backup-codes {
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,106 @@
|
|||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Mfa from './mfa.vue'
|
||||
|
||||
const SecurityTab = {
|
||||
data () {
|
||||
return {
|
||||
newEmail: '',
|
||||
changeEmailError: false,
|
||||
changeEmailPassword: '',
|
||||
changedEmail: false,
|
||||
deletingAccount: false,
|
||||
deleteAccountConfirmPasswordInput: '',
|
||||
deleteAccountError: false,
|
||||
changePasswordInputs: [ '', '', '' ],
|
||||
changedPassword: false,
|
||||
changePasswordError: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchTokens')
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Mfa,
|
||||
Checkbox
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
pleromaBackend () {
|
||||
return this.$store.state.instance.pleromaBackend
|
||||
},
|
||||
oauthTokens () {
|
||||
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
|
||||
return {
|
||||
id: oauthToken.id,
|
||||
appName: oauthToken.app_name,
|
||||
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
confirmDelete () {
|
||||
this.deletingAccount = true
|
||||
},
|
||||
deleteAccount () {
|
||||
this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push({ name: 'root' })
|
||||
} else {
|
||||
this.deleteAccountError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changePassword () {
|
||||
const params = {
|
||||
password: this.changePasswordInputs[0],
|
||||
newPassword: this.changePasswordInputs[1],
|
||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changePassword(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedPassword = true
|
||||
this.changePasswordError = false
|
||||
this.logout()
|
||||
} else {
|
||||
this.changedPassword = false
|
||||
this.changePasswordError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
changeEmail () {
|
||||
const params = {
|
||||
email: this.newEmail,
|
||||
password: this.changeEmailPassword
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changeEmail(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedEmail = true
|
||||
this.changeEmailError = false
|
||||
} else {
|
||||
this.changedEmail = false
|
||||
this.changeEmailError = res.error
|
||||
}
|
||||
})
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.replace('/')
|
||||
},
|
||||
revokeToken (id) {
|
||||
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
|
||||
this.$store.dispatch('revokeToken', id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SecurityTab
|
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<div :label="$t('settings.security_tab')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_email') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_email') }}</p>
|
||||
<input
|
||||
v-model="newEmail"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changeEmailPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="changeEmail"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<p v-if="changedEmail">
|
||||
{{ $t('settings.changed_email') }}
|
||||
</p>
|
||||
<template v-if="changeEmailError !== false">
|
||||
<p>{{ $t('settings.change_email_error') }}</p>
|
||||
<p>{{ changeEmailError }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.change_password') }}</h2>
|
||||
<div>
|
||||
<p>{{ $t('settings.current_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[0]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[1]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ $t('settings.confirm_new_password') }}</p>
|
||||
<input
|
||||
v-model="changePasswordInputs[2]"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="changePassword"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
<p v-if="changedPassword">
|
||||
{{ $t('settings.changed_password') }}
|
||||
</p>
|
||||
<p v-else-if="changePasswordError !== false">
|
||||
{{ $t('settings.change_password_error') }}
|
||||
</p>
|
||||
<p v-if="changePasswordError">
|
||||
{{ changePasswordError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.oauth_tokens') }}</h2>
|
||||
<table class="oauth-tokens">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('settings.app_name') }}</th>
|
||||
<th>{{ $t('settings.valid_until') }}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="oauthToken in oauthTokens"
|
||||
:key="oauthToken.id"
|
||||
>
|
||||
<td>{{ oauthToken.appName }}</td>
|
||||
<td>{{ oauthToken.validUntil }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="revokeToken(oauthToken.id)"
|
||||
>
|
||||
{{ $t('settings.revoke_token') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<mfa />
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.delete_account') }}</h2>
|
||||
<p v-if="!deletingAccount">
|
||||
{{ $t('settings.delete_account_description') }}
|
||||
</p>
|
||||
<div v-if="deletingAccount">
|
||||
<p>{{ $t('settings.delete_account_instructions') }}</p>
|
||||
<p>{{ $t('login.password') }}</p>
|
||||
<input
|
||||
v-model="deleteAccountConfirmPasswordInput"
|
||||
type="password"
|
||||
>
|
||||
<button
|
||||
class="btn btn-default"
|
||||
@click="deleteAccount"
|
||||
>
|
||||
{{ $t('settings.delete_account') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="deleteAccountError !== false">
|
||||
{{ $t('settings.delete_account_error') }}
|
||||
</p>
|
||||
<p v-if="deleteAccountError">
|
||||
{{ deleteAccountError }}
|
||||
</p>
|
||||
<button
|
||||
v-if="!deletingAccount"
|
||||
class="btn btn-default"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ $t('general.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./security_tab.js"></script>
|
||||
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
|
@ -3,7 +3,7 @@ import {
|
|||
rgb2hex,
|
||||
hex2rgb,
|
||||
getContrastRatioLayers
|
||||
} from '../../services/color_convert/color_convert.js'
|
||||
} from 'src/services/color_convert/color_convert.js'
|
||||
import {
|
||||
DEFAULT_SHADOWS,
|
||||
generateColors,
|
||||
|
@ -14,26 +14,27 @@ import {
|
|||
getThemes,
|
||||
shadows2to3,
|
||||
colors2to3
|
||||
} from '../../services/style_setter/style_setter.js'
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
import {
|
||||
SLOT_INHERITANCE
|
||||
} from '../../services/theme_data/pleromafe.js'
|
||||
} from 'src/services/theme_data/pleromafe.js'
|
||||
import {
|
||||
CURRENT_VERSION,
|
||||
OPACITIES,
|
||||
getLayers,
|
||||
getOpacitySlot
|
||||
} from '../../services/theme_data/theme_data.service.js'
|
||||
import ColorInput from '../color_input/color_input.vue'
|
||||
import RangeInput from '../range_input/range_input.vue'
|
||||
import OpacityInput from '../opacity_input/opacity_input.vue'
|
||||
import ShadowControl from '../shadow_control/shadow_control.vue'
|
||||
import FontControl from '../font_control/font_control.vue'
|
||||
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
} from 'src/services/theme_data/theme_data.service.js'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
import RangeInput from 'src/components/range_input/range_input.vue'
|
||||
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||
import FontControl from 'src/components/font_control/font_control.vue'
|
||||
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import ExportImport from 'src/components/export_import/export_import.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import Preview from './preview.vue'
|
||||
import ExportImport from '../export_import/export_import.vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
// List of color values used in v1
|
||||
const v1OnlyNames = [
|
|
@ -1,5 +1,6 @@
|
|||
@import '../../_variables.scss';
|
||||
.style-switcher {
|
||||
@import 'src/_variables.scss';
|
||||
.theme-tab {
|
||||
padding-bottom: 2em;
|
||||
.theme-warning {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
@ -54,10 +55,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tab-switcher {
|
||||
margin: 0 -1em;
|
||||
}
|
||||
|
||||
.reset-container {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
@ -98,20 +95,25 @@
|
|||
align-items: baseline;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
|
||||
.btn {
|
||||
min-width: 1px;
|
||||
flex: 0 auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
margin-bottom: 1em;
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
margin-bottom: 1em;
|
||||
.tab-header-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.btn {
|
||||
min-width: 1px;
|
||||
flex: 0 auto;
|
||||
padding: 0 1em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-selector {
|
||||
|
@ -161,7 +163,7 @@
|
|||
border-bottom: 1px dashed;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
margin: 1em -1em 0;
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: var(--body-background-image);
|
||||
background-size: cover;
|
||||
|
@ -328,6 +330,14 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: .25em;
|
||||
margin-right: .25em;
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="style-switcher">
|
||||
<div class="theme-tab">
|
||||
<div class="presets-container">
|
||||
<div class="save-load">
|
||||
<div
|
||||
|
@ -126,18 +126,20 @@
|
|||
>
|
||||
<div class="tab-header">
|
||||
<p>{{ $t('settings.theme_help') }}</p>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearOpacity"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_opacity') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearV1"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
<div class="tab-header-buttons">
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearOpacity"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_opacity') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="clearV1"
|
||||
>
|
||||
{{ $t('settings.style.switcher.clear_all') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ $t('settings.theme_help_v2_1') }}</p>
|
||||
<h4>{{ $t('settings.style.common_colors.main') }}</h4>
|
||||
|
@ -951,6 +953,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./style_switcher.js"></script>
|
||||
<script src="./theme_tab.js"></script>
|
||||
|
||||
<style src="./style_switcher.scss" lang="scss"></style>
|
||||
<style src="./theme_tab.scss" lang="scss"></style>
|
|
@ -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
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div :label="$t('settings.version.title')">
|
||||
<div class="setting-item">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p>{{ $t('settings.version.backend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="backendVersionLink"
|
||||
target="_blank"
|
||||
>{{ backendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>{{ $t('settings.version.frontend_version') }}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a
|
||||
:href="frontendVersionLink"
|
||||
target="_blank"
|
||||
>{{ frontendVersion }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./version_tab.js">
|
|
@ -62,6 +62,9 @@ const SideDrawer = {
|
|||
},
|
||||
touchMove (e) {
|
||||
GestureService.updateSwipe(e, this.closeGesture)
|
||||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,9 +122,12 @@
|
|||
</router-link>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'settings' }">
|
||||
<a
|
||||
href="#"
|
||||
@click="openSettingsModal"
|
||||
>
|
||||
<i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
|
||||
</router-link>
|
||||
</a>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'about'}">
|
||||
|
|
|
@ -24,6 +24,11 @@ export default Vue.component('tab-switcher', {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sideTabBar: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
@ -55,6 +60,9 @@ export default Vue.component('tab-switcher', {
|
|||
this.onSwitch.call(null, this.$slots.default[index].key)
|
||||
}
|
||||
this.active = index
|
||||
if (this.scrollableTabs) {
|
||||
this.$refs.contents.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -64,7 +72,6 @@ export default Vue.component('tab-switcher', {
|
|||
if (!slot.tag) return
|
||||
const classesTab = ['tab']
|
||||
const classesWrapper = ['tab-wrapper']
|
||||
|
||||
if (this.activeIndex === index) {
|
||||
classesTab.push('active')
|
||||
classesWrapper.push('active')
|
||||
|
@ -87,8 +94,14 @@ export default Vue.component('tab-switcher', {
|
|||
<button
|
||||
disabled={slot.data.attrs.disabled}
|
||||
onClick={this.activateTab(index)}
|
||||
class={classesTab.join(' ')}>
|
||||
{slot.data.attrs.label}</button>
|
||||
class={classesTab.join(' ')}
|
||||
type="button"
|
||||
>
|
||||
{!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)}
|
||||
<span class="text">
|
||||
{slot.data.attrs.label}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@ -96,20 +109,32 @@ export default Vue.component('tab-switcher', {
|
|||
const contents = this.$slots.default.map((slot, index) => {
|
||||
if (!slot.tag) return
|
||||
const active = this.activeIndex === index
|
||||
if (this.renderOnlyFocused) {
|
||||
return active
|
||||
? <div class="active">{slot}</div>
|
||||
: <div class="hidden"></div>
|
||||
const classes = [ active ? 'active' : 'hidden' ]
|
||||
if (slot.data.attrs.fullHeight) {
|
||||
classes.push('full-height')
|
||||
}
|
||||
return <div class={active ? 'active' : 'hidden' }>{slot}</div>
|
||||
const renderSlot = (!this.renderOnlyFocused || active)
|
||||
? slot
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div class={classes}>
|
||||
{
|
||||
this.sideTabBar
|
||||
? <h1 class="mobile-label">{slot.data.attrs.label}</h1>
|
||||
: ''
|
||||
}
|
||||
{renderSlot}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="tab-switcher">
|
||||
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
|
||||
<div class="tabs">
|
||||
{tabs}
|
||||
</div>
|
||||
<div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
|
||||
<div ref="contents" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
|
||||
{contents}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,144 @@
|
|||
|
||||
.tab-switcher {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tab-icon {
|
||||
font-size: 2em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.top-tabs {
|
||||
flex-direction: column;
|
||||
|
||||
> .tabs {
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding-top: 5px;
|
||||
flex-direction: row;
|
||||
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
flex: 1 1 auto;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
.tab-wrapper {
|
||||
height: 28px;
|
||||
|
||||
&:not(.active)::after {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
}
|
||||
.tab {
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-bottom: 99px;
|
||||
margin-bottom: 6px - 99px;
|
||||
}
|
||||
}
|
||||
.contents.scrollable-tabs {
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.side-tabs {
|
||||
flex-direction: row;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
> .contents {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
flex: 0 0 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
|
||||
&::after, &::before {
|
||||
flex-shrink: 0;
|
||||
flex-basis: .5em;
|
||||
content: '';
|
||||
border-right: 1px solid;
|
||||
border-right-color: $fallback--border;
|
||||
border-right-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
&::after {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&::before {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.tab-wrapper {
|
||||
min-width: 10em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
&:not(.active)::after {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-right: 1px solid;
|
||||
border-right-color: $fallback--border;
|
||||
border-right-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
&::before {
|
||||
flex: 0 0 6px;
|
||||
content: '';
|
||||
border-right: 1px solid;
|
||||
border-right-color: $fallback--border;
|
||||
border-right-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
&:last-child .tab {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
box-sizing: content-box;
|
||||
min-width: 10em;
|
||||
min-width: 1px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-left: 1em;
|
||||
padding-right: calc(1em + 200px);
|
||||
margin-right: -200px;
|
||||
margin-left: 1em;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
padding-left: .25em;
|
||||
padding-right: calc(.25em + 200px);
|
||||
margin-right: calc(.25em - 200px);
|
||||
margin-left: .25em;
|
||||
.text {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contents {
|
||||
flex: 1 0 auto;
|
||||
|
@ -11,88 +148,89 @@
|
|||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.full-height:not(.hidden) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> *:not(.mobile-label) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.scrollable-tabs {
|
||||
flex-basis: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
padding: 6px 1em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--tab, $fallback--fg);
|
||||
|
||||
&, &:active .tab-icon {
|
||||
color: $fallback--text;
|
||||
color: var(--tabText, $fallback--text);
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
z-index: 4;
|
||||
|
||||
&:hover {
|
||||
z-index: 6;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
color: $fallback--text;
|
||||
color: var(--tabActiveText, $fallback--text);
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 26px;
|
||||
vertical-align: top;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding-top: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after, &::before {
|
||||
display: block;
|
||||
content: '';
|
||||
flex: 1 1 auto;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-wrapper {
|
||||
height: 28px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
.tab-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.tab {
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
position: relative;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding: 6px 1em;
|
||||
padding-bottom: 99px;
|
||||
margin-bottom: 6px - 99px;
|
||||
white-space: nowrap;
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--tabText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--tab, $fallback--fg);
|
||||
|
||||
&:not(.active) {
|
||||
z-index: 4;
|
||||
|
||||
&:hover {
|
||||
z-index: 6;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
color: $fallback--text;
|
||||
color: var(--tabActiveText, $fallback--text);
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 26px;
|
||||
vertical-align: top;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 7;
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: $fallback--border;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
}
|
||||
&:not(.active) {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-label {
|
||||
padding-left: .3em;
|
||||
padding-bottom: .25em;
|
||||
margin-top: .5em;
|
||||
margin-left: .2em;
|
||||
margin-bottom: .25em;
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
|
||||
@media all and (min-width: 800px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,15 +50,6 @@
|
|||
>
|
||||
{{ user.name }}
|
||||
</div>
|
||||
<router-link
|
||||
v-if="!isOtherUser"
|
||||
:to="{ name: 'user-settings' }"
|
||||
>
|
||||
<i
|
||||
class="button-icon icon-wrench usersettings"
|
||||
:title="$t('tool_tip.user_settings')"
|
||||
/>
|
||||
</router-link>
|
||||
<a
|
||||
v-if="isOtherUser && !user.is_local"
|
||||
:href="user.statusnet_profile_url"
|
||||
|
@ -118,7 +109,7 @@
|
|||
type="color"
|
||||
>
|
||||
<label
|
||||
for="style-switcher"
|
||||
for="theme_tab"
|
||||
class="userHighlightSel select"
|
||||
>
|
||||
<select
|
||||
|
|
|
@ -3,6 +3,7 @@ import UserCard from '../user_card/user_card.vue'
|
|||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import Timeline from '../timeline/timeline.vue'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import List from '../list/list.vue'
|
||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||
|
||||
|
@ -146,6 +147,7 @@ const UserProfile = {
|
|||
FollowerList,
|
||||
FriendList,
|
||||
FollowCard,
|
||||
TabSwitcher,
|
||||
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",
|
||||
"submit": "Submit",
|
||||
"more": "More",
|
||||
"loading": "Loading…",
|
||||
"generic_error": "An error occured",
|
||||
"error_retry": "Please try again",
|
||||
"retry": "Try again",
|
||||
"optional": "optional",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
|
@ -68,7 +71,9 @@
|
|||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"confirm": "Confirm",
|
||||
"verify": "Verify"
|
||||
"verify": "Verify",
|
||||
"close": "Close",
|
||||
"peek": "Peek"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Crop picture",
|
||||
|
@ -278,6 +283,7 @@
|
|||
"current_avatar": "Your current avatar",
|
||||
"current_password": "Current password",
|
||||
"current_profile_banner": "Your current profile banner",
|
||||
"mutes_and_blocks": "Mutes and Blocks",
|
||||
"data_import_export_tab": "Data Import / Export",
|
||||
"default_vis": "Default visibility scope",
|
||||
"delete_account": "Delete Account",
|
||||
|
|
|
@ -3,6 +3,16 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
|||
|
||||
const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||
|
||||
/* TODO this is a bit messy.
|
||||
* We need to declare settings with their types and also deal with
|
||||
* instance-default settings in some way, hopefully try to avoid copy-pasta
|
||||
* in general.
|
||||
*/
|
||||
export const multiChoiceProperties = [
|
||||
'postContentType',
|
||||
'subjectLineBehavior'
|
||||
]
|
||||
|
||||
export const defaultState = {
|
||||
colors: {},
|
||||
theme: undefined,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { set, delete as del } from 'vue'
|
||||
|
||||
const defaultState = {
|
||||
settingsModalState: 'hidden',
|
||||
settingsModalLoaded: false,
|
||||
settings: {
|
||||
currentSaveStateNotice: null,
|
||||
noticeClearTimeout: null,
|
||||
|
@ -35,6 +37,27 @@ const interfaceMod = {
|
|||
},
|
||||
setMobileLayout (state, value) {
|
||||
state.mobileLayout = value
|
||||
},
|
||||
closeSettingsModal (state) {
|
||||
state.settingsModalState = 'hidden'
|
||||
},
|
||||
togglePeekSettingsModal (state) {
|
||||
switch (state.settingsModalState) {
|
||||
case 'minimized':
|
||||
state.settingsModalState = 'visible'
|
||||
return
|
||||
case 'visible':
|
||||
state.settingsModalState = 'minimized'
|
||||
return
|
||||
default:
|
||||
throw new Error('Illegal minimization state of settings modal')
|
||||
}
|
||||
},
|
||||
openSettingsModal (state) {
|
||||
state.settingsModalState = 'visible'
|
||||
if (!state.settingsModalLoaded) {
|
||||
state.settingsModalLoaded = true
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
@ -49,6 +72,15 @@ const interfaceMod = {
|
|||
},
|
||||
setMobileLayout ({ commit }, value) {
|
||||
commit('setMobileLayout', value)
|
||||
},
|
||||
closeSettingsModal ({ commit }) {
|
||||
commit('closeSettingsModal')
|
||||
},
|
||||
openSettingsModal ({ commit }) {
|
||||
commit('openSettingsModal')
|
||||
},
|
||||
togglePeekSettingsModal ({ commit }) {
|
||||
commit('togglePeekSettingsModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
"code": 59431,
|
||||
"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:
|
||||
version "1.0.2"
|
||||
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:
|
||||
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"
|
||||
loader-utils "^1.0.2"
|
||||
|
||||
vue-template-compiler@^2.3.4:
|
||||
version "2.5.21"
|
||||
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.21.tgz#a57ceb903177e8f643560a8d639a0f8db647054a"
|
||||
vue-template-compiler@^2.6.11:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
|
||||
integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
|
||||
dependencies:
|
||||
de-indent "^1.0.2"
|
||||
he "^1.1.0"
|
||||
|
@ -7914,9 +7916,10 @@ vue-template-es2015-compiler@^1.6.0:
|
|||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
|
||||
vue@^2.5.13:
|
||||
version "2.5.21"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85"
|
||||
vue@^2.6.11:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
||||
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
|
||||
|
||||
vuelidate@^0.7.4:
|
||||
version "0.7.4"
|
||||
|
|
Loading…
Reference in New Issue