forked from AkkomaGang/akkoma-fe
initial work on settings modal
This commit is contained in:
parent
9b349b4019
commit
2e35289c33
26 changed files with 1530 additions and 927 deletions
|
@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
||||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
||||||
|
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MediaModal from './components/media_modal/media_modal.vue'
|
||||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||||
|
@ -29,6 +30,7 @@ export default {
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
MobilePostStatusButton,
|
MobilePostStatusButton,
|
||||||
MobileNav,
|
MobileNav,
|
||||||
|
SettingsModal,
|
||||||
UserReportingModal,
|
UserReportingModal,
|
||||||
PostStatusModal
|
PostStatusModal
|
||||||
},
|
},
|
||||||
|
@ -112,6 +114,9 @@ export default {
|
||||||
onSearchBarToggled (hidden) {
|
onSearchBarToggled (hidden) {
|
||||||
this.searchBarHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
|
openSettingsModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal')
|
||||||
|
},
|
||||||
updateMobileState () {
|
updateMobileState () {
|
||||||
const mobileLayout = windowWidth() <= 800
|
const mobileLayout = windowWidth() <= 800
|
||||||
const changed = mobileLayout !== this.isMobileLayout
|
const changed = mobileLayout !== this.isMobileLayout
|
||||||
|
|
|
@ -860,6 +860,7 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
.setting-item {
|
.setting-item {
|
||||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||||
margin: 1em 1em 1.4em;
|
margin: 1em 1em 1.4em;
|
||||||
|
@ -905,6 +906,8 @@ nav {
|
||||||
max-width: 6em;
|
max-width: 6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// DELETE
|
||||||
|
|
||||||
.select-multiple {
|
.select-multiple {
|
||||||
display: flex;
|
display: flex;
|
||||||
.option-list {
|
.option-list {
|
||||||
|
|
|
@ -46,15 +46,15 @@
|
||||||
@toggled="onSearchBarToggled"
|
@toggled="onSearchBarToggled"
|
||||||
@click.stop.native
|
@click.stop.native
|
||||||
/>
|
/>
|
||||||
<router-link
|
<a
|
||||||
class="mobile-hidden"
|
class="mobile-hidden"
|
||||||
:to="{ name: 'settings'}"
|
@click.stop="openSettingsModal"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="button-icon icon-cog nav-icon"
|
class="button-icon icon-cog nav-icon"
|
||||||
:title="$t('nav.preferences')"
|
:title="$t('nav.preferences')"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
href="/pleroma/admin/#/login-pleroma"
|
||||||
|
@ -122,6 +122,7 @@
|
||||||
<MobilePostStatusButton />
|
<MobilePostStatusButton />
|
||||||
<UserReportingModal />
|
<UserReportingModal />
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
|
<SettingsModal />
|
||||||
<portal-target name="modal" />
|
<portal-target name="modal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -52,7 +52,7 @@ const MobilePostStatusButton = {
|
||||||
window.removeEventListener('scroll', this.handleScrollEnd)
|
window.removeEventListener('scroll', this.handleScrollEnd)
|
||||||
},
|
},
|
||||||
openPostForm () {
|
openPostForm () {
|
||||||
this.$store.dispatch('openPostStatusModal')
|
this.$store.dispatch('openSettingsModal')
|
||||||
},
|
},
|
||||||
handleOSK () {
|
handleOSK () {
|
||||||
// This is a big hack: we're guessing from changed window sizes if the
|
// This is a big hack: we're guessing from changed window sizes if the
|
||||||
|
|
|
@ -13,9 +13,6 @@ const PostStatusModal = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isLoggedIn () {
|
|
||||||
return !!this.$store.state.users.currentUser
|
|
||||||
},
|
|
||||||
modalActivated () {
|
modalActivated () {
|
||||||
return this.$store.state.postStatus.modalActivated
|
return this.$store.state.postStatus.modalActivated
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
v-if="isLoggedIn && !resettingForm"
|
|
||||||
:is-open="modalActivated"
|
:is-open="modalActivated"
|
||||||
class="post-form-modal-view"
|
class="post-form-modal-view"
|
||||||
@backdropClicked="closeModal"
|
@backdropClicked="closeModal"
|
||||||
|
|
39
src/components/settings_modal/settings_modal.js
Normal file
39
src/components/settings_modal/settings_modal.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import Modal from '../modal/modal.vue'
|
||||||
|
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||||
|
|
||||||
|
import Profile from './tabs/profile.vue'
|
||||||
|
import Security from './tabs/security.vue'
|
||||||
|
import Notifications from './tabs/notifications.vue'
|
||||||
|
import DataImportExport from './tabs/data_import_export.vue'
|
||||||
|
import MutesAndBlocks from './tabs/mutes_and_blocks.vue'
|
||||||
|
|
||||||
|
const SettingsModal = {
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
TabSwitcher,
|
||||||
|
Profile,
|
||||||
|
Security,
|
||||||
|
Notifications,
|
||||||
|
DataImportExport,
|
||||||
|
MutesAndBlocks
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
resettingForm: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
modalActivated () {
|
||||||
|
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsModal
|
59
src/components/settings_modal/settings_modal.scss
Normal file
59
src/components/settings_modal/settings_modal.scss
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
.settings-modal {
|
||||||
|
.settings_tab-switcher {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.settings-modal-panel {
|
||||||
|
width: 1000px;
|
||||||
|
max-width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
}
|
||||||
|
.panel-body {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/components/settings_modal/settings_modal.vue
Normal file
31
src/components/settings_modal/settings_modal.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-if="isLoggedIn && !resettingForm"
|
||||||
|
:is-open="modalActivated"
|
||||||
|
class="settings-modal"
|
||||||
|
>
|
||||||
|
<div class="settings-modal-panel panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
{{ $t('settings.settings') }}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<tab-switcher
|
||||||
|
class="settings_tab-switcher"
|
||||||
|
:sideTabBar="true"
|
||||||
|
:scrollableTabs="true"
|
||||||
|
ref="tabSwitcher"
|
||||||
|
>
|
||||||
|
<div :label="$t('settings.profile_tab')"><Profile /></div>
|
||||||
|
<div :label="$t('settings.security_tab')"><Security /></div>
|
||||||
|
<div :label="$t('settings.notifications')"><Notifications /></div>
|
||||||
|
<div :label="$t('settings.data_import_export_tab')"><DataImportExport /></div>
|
||||||
|
<div :label="$t('settings.mutes_and_blocks')"><MutesAndBlocks /></div>
|
||||||
|
</tab-switcher>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./settings_modal.js"></script>
|
||||||
|
|
||||||
|
<style src="./settings_modal.scss" lang="scss"></style>
|
65
src/components/settings_modal/tabs/data_import_export.js
Normal file
65
src/components/settings_modal/tabs/data_import_export.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import Importer from '../../importer/importer.vue'
|
||||||
|
import Exporter from '../../exporter/exporter.vue'
|
||||||
|
import Checkbox from '../../checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const DataImportExport = {
|
||||||
|
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 DataImportExport
|
43
src/components/settings_modal/tabs/data_import_export.vue
Normal file
43
src/components/settings_modal/tabs/data_import_export.vue
Normal file
|
@ -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.js"></script>
|
||||||
|
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
124
src/components/settings_modal/tabs/mutes_and_blocks.js
Normal file
124
src/components/settings_modal/tabs/mutes_and_blocks.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import get from 'lodash/get'
|
||||||
|
import map from 'lodash/map'
|
||||||
|
import reject from 'lodash/reject'
|
||||||
|
import Autosuggest from '../../autosuggest/autosuggest.vue'
|
||||||
|
import TabSwitcher from '../../tab_switcher/tab_switcher.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 withSubscription from '../../../hocs/with_subscription/with_subscription'
|
||||||
|
import Checkbox from '../../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 user = this.$store.getters.findUser(userId)
|
||||||
|
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
filterUnMutedUsers (userIds) {
|
||||||
|
return reject(userIds, (userId) => {
|
||||||
|
const user = this.$store.getters.findUser(userId)
|
||||||
|
return !user || user.muted || user.id === this.$store.state.users.currentUser.id
|
||||||
|
})
|
||||||
|
},
|
||||||
|
queryUserIds (query) {
|
||||||
|
return this.$store.dispatch('searchUsers', query)
|
||||||
|
.then((users) => map(users, 'id'))
|
||||||
|
},
|
||||||
|
blockUsers (ids) {
|
||||||
|
return this.$store.dispatch('blockUsers', ids)
|
||||||
|
},
|
||||||
|
unblockUsers (ids) {
|
||||||
|
return this.$store.dispatch('unblockUsers', ids)
|
||||||
|
},
|
||||||
|
muteUsers (ids) {
|
||||||
|
return this.$store.dispatch('muteUsers', ids)
|
||||||
|
},
|
||||||
|
unmuteUsers (ids) {
|
||||||
|
return this.$store.dispatch('unmuteUsers', ids)
|
||||||
|
},
|
||||||
|
unmuteDomains (domains) {
|
||||||
|
return this.$store.dispatch('unmuteDomains', domains)
|
||||||
|
},
|
||||||
|
muteDomain () {
|
||||||
|
return this.$store.dispatch('muteDomain', this.newDomainToMute)
|
||||||
|
.then(() => { this.newDomainToMute = '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MutesAndBlocks
|
173
src/components/settings_modal/tabs/mutes_and_blocks.vue
Normal file
173
src/components/settings_modal/tabs/mutes_and_blocks.vue
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
<template>
|
||||||
|
<tab-switcher>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./mutes_and_blocks.js"></script>
|
||||||
|
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
27
src/components/settings_modal/tabs/notifications.js
Normal file
27
src/components/settings_modal/tabs/notifications.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import Checkbox from '../../checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const Notifications = {
|
||||||
|
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 Notifications
|
42
src/components/settings_modal/tabs/notifications.vue
Normal file
42
src/components/settings_modal/tabs/notifications.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div :label="$t('settings.notifications')">
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="select-multiple">
|
||||||
|
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
||||||
|
<ul class="option-list">
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationSettings.follows">
|
||||||
|
{{ $t('settings.notification_setting_follows') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationSettings.followers">
|
||||||
|
{{ $t('settings.notification_setting_followers') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationSettings.non_follows">
|
||||||
|
{{ $t('settings.notification_setting_non_follows') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Checkbox v-model="notificationSettings.non_followers">
|
||||||
|
{{ $t('settings.notification_setting_non_followers') }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||||
|
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||||
|
<button
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="updateNotificationSettings"
|
||||||
|
>
|
||||||
|
{{ $t('general.submit') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./notifications.js"></script>
|
||||||
|
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
179
src/components/settings_modal/tabs/profile.js
Normal file
179
src/components/settings_modal/tabs/profile.js
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
import unescape from 'lodash/unescape'
|
||||||
|
import ImageCropper from '../../image_cropper/image_cropper.vue'
|
||||||
|
import ScopeSelector from '../../scope_selector/scope_selector.vue'
|
||||||
|
import fileSizeFormatService from '../../../services/file_size_format/file_size_format.js'
|
||||||
|
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 Checkbox from '../../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: (input) => this.$store.dispatch('searchUsers', input)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
emojiSuggestor () {
|
||||||
|
return suggestor({ emoji: [
|
||||||
|
...this.$store.state.instance.emoji,
|
||||||
|
...this.$store.state.instance.customEmoji
|
||||||
|
] })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateProfile () {
|
||||||
|
this.$store.state.api.backendInteractor
|
||||||
|
.updateProfile({
|
||||||
|
params: {
|
||||||
|
note: this.newBio,
|
||||||
|
locked: this.newLocked,
|
||||||
|
// Backend notation.
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
display_name: this.newName,
|
||||||
|
default_scope: this.newDefaultScope,
|
||||||
|
no_rich_text: this.newNoRichText,
|
||||||
|
hide_follows: this.hideFollows,
|
||||||
|
hide_followers: this.hideFollowers,
|
||||||
|
discoverable: this.discoverable,
|
||||||
|
allow_following_move: this.allowFollowingMove,
|
||||||
|
hide_follows_count: this.hideFollowsCount,
|
||||||
|
hide_followers_count: this.hideFollowersCount,
|
||||||
|
show_role: this.showRole
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
} }).then((user) => {
|
||||||
|
this.$store.commit('addNewUsers', [user])
|
||||||
|
this.$store.commit('setCurrentUser', user)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
changeVis (visibility) {
|
||||||
|
this.newDefaultScope = visibility
|
||||||
|
},
|
||||||
|
uploadFile (slot, e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (!file) { return }
|
||||||
|
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||||
|
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||||
|
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||||
|
this[slot + 'UploadError'] = [
|
||||||
|
this.$t('upload.error.base'),
|
||||||
|
this.$t(
|
||||||
|
'upload.error.file_too_big',
|
||||||
|
{
|
||||||
|
filesize: filesize.num,
|
||||||
|
filesizeunit: filesize.unit,
|
||||||
|
allowedsize: allowedsize.num,
|
||||||
|
allowedsizeunit: allowedsize.unit
|
||||||
|
}
|
||||||
|
)
|
||||||
|
].join(' ')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = ({ target }) => {
|
||||||
|
const img = target.result
|
||||||
|
this[slot + 'Preview'] = img
|
||||||
|
this[slot] = file
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
},
|
||||||
|
submitAvatar (cropper, file) {
|
||||||
|
const that = this
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
function updateAvatar (avatar) {
|
||||||
|
that.$store.state.api.backendInteractor.updateAvatar({ avatar })
|
||||||
|
.then((user) => {
|
||||||
|
that.$store.commit('addNewUsers', [user])
|
||||||
|
that.$store.commit('setCurrentUser', user)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cropper) {
|
||||||
|
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
||||||
|
} else {
|
||||||
|
updateAvatar(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitBanner () {
|
||||||
|
if (!this.bannerPreview) { return }
|
||||||
|
|
||||||
|
this.bannerUploading = true
|
||||||
|
this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
|
||||||
|
.then((user) => {
|
||||||
|
this.$store.commit('addNewUsers', [user])
|
||||||
|
this.$store.commit('setCurrentUser', user)
|
||||||
|
this.bannerPreview = null
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
|
||||||
|
})
|
||||||
|
.then(() => { this.bannerUploading = false })
|
||||||
|
},
|
||||||
|
submitBg () {
|
||||||
|
if (!this.backgroundPreview) { return }
|
||||||
|
let background = this.background
|
||||||
|
this.backgroundUploading = true
|
||||||
|
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
||||||
|
if (!data.error) {
|
||||||
|
this.$store.commit('addNewUsers', [data])
|
||||||
|
this.$store.commit('setCurrentUser', data)
|
||||||
|
this.backgroundPreview = null
|
||||||
|
} else {
|
||||||
|
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||||
|
}
|
||||||
|
this.backgroundUploading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfileTab
|
82
src/components/settings_modal/tabs/profile.scss
Normal file
82
src/components/settings_modal/tabs/profile.scss
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
@import '../../../_variables.scss';
|
||||||
|
.profile-tab {
|
||||||
|
.bio {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tray {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=file] {
|
||||||
|
padding: 5px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploading {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-changer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-avatar {
|
||||||
|
display: block;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
border-radius: $fallback--avatarRadius;
|
||||||
|
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-tokens {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-usersearch-wrapper {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-bulk-actions {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 1em;
|
||||||
|
min-height: 28px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-domain-mute-form {
|
||||||
|
padding: 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 1em;
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-subitem {
|
||||||
|
margin-left: 1.75em;
|
||||||
|
}
|
||||||
|
}
|
213
src/components/settings_modal/tabs/profile.vue
Normal file
213
src/components/settings_modal/tabs/profile.vue
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
<template>
|
||||||
|
<div class="profile-tab">
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.name_bio') }}</h2>
|
||||||
|
<p>{{ $t('settings.name') }}</p>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newName"
|
||||||
|
enable-emoji-picker
|
||||||
|
:suggest="emojiSuggestor"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="newName"
|
||||||
|
classname="name-changer"
|
||||||
|
>
|
||||||
|
</EmojiInput>
|
||||||
|
<p>{{ $t('settings.bio') }}</p>
|
||||||
|
<EmojiInput
|
||||||
|
v-model="newBio"
|
||||||
|
enable-emoji-picker
|
||||||
|
:suggest="emojiUserSuggestor"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-model="newBio"
|
||||||
|
classname="bio"
|
||||||
|
/>
|
||||||
|
</EmojiInput>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="newLocked">
|
||||||
|
{{ $t('settings.lock_account_description') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
|
||||||
|
<div
|
||||||
|
id="default-vis"
|
||||||
|
class="visibility-tray"
|
||||||
|
>
|
||||||
|
<scope-selector
|
||||||
|
:show-all="true"
|
||||||
|
:user-default="newDefaultScope"
|
||||||
|
:initial-scope="newDefaultScope"
|
||||||
|
:on-scope-change="changeVis"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="newNoRichText">
|
||||||
|
{{ $t('settings.no_rich_text_description') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="hideFollows">
|
||||||
|
{{ $t('settings.hide_follows_description') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p class="setting-subitem">
|
||||||
|
<Checkbox
|
||||||
|
v-model="hideFollowsCount"
|
||||||
|
:disabled="!hideFollows"
|
||||||
|
>
|
||||||
|
{{ $t('settings.hide_follows_count_description') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="hideFollowers">
|
||||||
|
{{ $t('settings.hide_followers_description') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p class="setting-subitem">
|
||||||
|
<Checkbox
|
||||||
|
v-model="hideFollowersCount"
|
||||||
|
:disabled="!hideFollowers"
|
||||||
|
>
|
||||||
|
{{ $t('settings.hide_followers_count_description') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="allowFollowingMove">
|
||||||
|
{{ $t('settings.allow_following_move') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p v-if="role === 'admin' || role === 'moderator'">
|
||||||
|
<Checkbox v-model="showRole">
|
||||||
|
<template v-if="role === 'admin'">
|
||||||
|
{{ $t('settings.show_admin_badge') }}
|
||||||
|
</template>
|
||||||
|
<template v-if="role === 'moderator'">
|
||||||
|
{{ $t('settings.show_moderator_badge') }}
|
||||||
|
</template>
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Checkbox v-model="discoverable">
|
||||||
|
{{ $t('settings.discoverable') }}
|
||||||
|
</Checkbox>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
:disabled="newName && newName.length === 0"
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="updateProfile"
|
||||||
|
>
|
||||||
|
{{ $t('general.submit') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.avatar') }}</h2>
|
||||||
|
<p class="visibility-notice">
|
||||||
|
{{ $t('settings.avatar_size_instruction') }}
|
||||||
|
</p>
|
||||||
|
<p>{{ $t('settings.current_avatar') }}</p>
|
||||||
|
<img
|
||||||
|
:src="user.profile_image_url_original"
|
||||||
|
class="current-avatar"
|
||||||
|
>
|
||||||
|
<p>{{ $t('settings.set_new_avatar') }}</p>
|
||||||
|
<button
|
||||||
|
v-show="pickAvatarBtnVisible"
|
||||||
|
id="pick-avatar"
|
||||||
|
class="btn"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{{ $t('settings.upload_a_photo') }}
|
||||||
|
</button>
|
||||||
|
<image-cropper
|
||||||
|
trigger="#pick-avatar"
|
||||||
|
:submit-handler="submitAvatar"
|
||||||
|
@open="pickAvatarBtnVisible=false"
|
||||||
|
@close="pickAvatarBtnVisible=true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.profile_banner') }}</h2>
|
||||||
|
<p>{{ $t('settings.current_profile_banner') }}</p>
|
||||||
|
<img
|
||||||
|
:src="user.cover_photo"
|
||||||
|
class="banner"
|
||||||
|
>
|
||||||
|
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
||||||
|
<img
|
||||||
|
v-if="bannerPreview"
|
||||||
|
class="banner"
|
||||||
|
:src="bannerPreview"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
@change="uploadFile('banner', $event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
v-if="bannerUploading"
|
||||||
|
class=" icon-spin4 animate-spin uploading"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-else-if="bannerPreview"
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="submitBanner"
|
||||||
|
>
|
||||||
|
{{ $t('general.submit') }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="bannerUploadError"
|
||||||
|
class="alert error"
|
||||||
|
>
|
||||||
|
Error: {{ bannerUploadError }}
|
||||||
|
<i
|
||||||
|
class="button-icon icon-cancel"
|
||||||
|
@click="clearUploadError('banner')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.profile_background') }}</h2>
|
||||||
|
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
||||||
|
<img
|
||||||
|
v-if="backgroundPreview"
|
||||||
|
class="bg"
|
||||||
|
:src="backgroundPreview"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
@change="uploadFile('background', $event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
v-if="backgroundUploading"
|
||||||
|
class=" icon-spin4 animate-spin uploading"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-else-if="backgroundPreview"
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="submitBg"
|
||||||
|
>
|
||||||
|
{{ $t('general.submit') }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="backgroundUploadError"
|
||||||
|
class="alert error"
|
||||||
|
>
|
||||||
|
Error: {{ backgroundUploadError }}
|
||||||
|
<i
|
||||||
|
class="button-icon icon-cancel"
|
||||||
|
@click="clearUploadError('background')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./profile.js"></script>
|
||||||
|
<style lang="scss" src="./profile.scss"></style>
|
106
src/components/settings_modal/tabs/security.js
Normal file
106
src/components/settings_modal/tabs/security.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import ProgressButton from '../../progress_button/progress_button.vue'
|
||||||
|
import Checkbox from '../../checkbox/checkbox.vue'
|
||||||
|
import Mfa from '../../user_settings/mfa.vue'
|
||||||
|
|
||||||
|
const Security = {
|
||||||
|
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 Security
|
143
src/components/settings_modal/tabs/security.vue
Normal file
143
src/components/settings_modal/tabs/security.vue
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div :label="$t('settings.security_tab')">
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.change_email') }}</h2>
|
||||||
|
<div>
|
||||||
|
<p>{{ $t('settings.new_email') }}</p>
|
||||||
|
<input
|
||||||
|
v-model="newEmail"
|
||||||
|
type="email"
|
||||||
|
autocomplete="email"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{{ $t('settings.current_password') }}</p>
|
||||||
|
<input
|
||||||
|
v-model="changeEmailPassword"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="changeEmail"
|
||||||
|
>
|
||||||
|
{{ $t('general.submit') }}
|
||||||
|
</button>
|
||||||
|
<p v-if="changedEmail">
|
||||||
|
{{ $t('settings.changed_email') }}
|
||||||
|
</p>
|
||||||
|
<template v-if="changeEmailError !== false">
|
||||||
|
<p>{{ $t('settings.change_email_error') }}</p>
|
||||||
|
<p>{{ changeEmailError }}</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.change_password') }}</h2>
|
||||||
|
<div>
|
||||||
|
<p>{{ $t('settings.current_password') }}</p>
|
||||||
|
<input
|
||||||
|
v-model="changePasswordInputs[0]"
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{{ $t('settings.new_password') }}</p>
|
||||||
|
<input
|
||||||
|
v-model="changePasswordInputs[1]"
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>{{ $t('settings.confirm_new_password') }}</p>
|
||||||
|
<input
|
||||||
|
v-model="changePasswordInputs[2]"
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="changePassword"
|
||||||
|
>
|
||||||
|
{{ $t('general.submit') }}
|
||||||
|
</button>
|
||||||
|
<p v-if="changedPassword">
|
||||||
|
{{ $t('settings.changed_password') }}
|
||||||
|
</p>
|
||||||
|
<p v-else-if="changePasswordError !== false">
|
||||||
|
{{ $t('settings.change_password_error') }}
|
||||||
|
</p>
|
||||||
|
<p v-if="changePasswordError">
|
||||||
|
{{ changePasswordError }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.oauth_tokens') }}</h2>
|
||||||
|
<table class="oauth-tokens">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('settings.app_name') }}</th>
|
||||||
|
<th>{{ $t('settings.valid_until') }}</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="oauthToken in oauthTokens"
|
||||||
|
:key="oauthToken.id"
|
||||||
|
>
|
||||||
|
<td>{{ oauthToken.appName }}</td>
|
||||||
|
<td>{{ oauthToken.validUntil }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="revokeToken(oauthToken.id)"
|
||||||
|
>
|
||||||
|
{{ $t('settings.revoke_token') }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<mfa />
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.delete_account') }}</h2>
|
||||||
|
<p v-if="!deletingAccount">
|
||||||
|
{{ $t('settings.delete_account_description') }}
|
||||||
|
</p>
|
||||||
|
<div v-if="deletingAccount">
|
||||||
|
<p>{{ $t('settings.delete_account_instructions') }}</p>
|
||||||
|
<p>{{ $t('login.password') }}</p>
|
||||||
|
<input
|
||||||
|
v-model="deleteAccountConfirmPasswordInput"
|
||||||
|
type="password"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="deleteAccount"
|
||||||
|
>
|
||||||
|
{{ $t('settings.delete_account') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="deleteAccountError !== false">
|
||||||
|
{{ $t('settings.delete_account_error') }}
|
||||||
|
</p>
|
||||||
|
<p v-if="deleteAccountError">
|
||||||
|
{{ deleteAccountError }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
v-if="!deletingAccount"
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="confirmDelete"
|
||||||
|
>
|
||||||
|
{{ $t('general.submit') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./security.js"></script>
|
||||||
|
<!-- <style lang="scss" src="./profile.scss"></style> -->
|
|
@ -24,6 +24,11 @@ export default Vue.component('tab-switcher', {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
sideTabBar: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -105,7 +110,7 @@ export default Vue.component('tab-switcher', {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="tab-switcher">
|
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
{tabs}
|
{tabs}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,116 @@
|
||||||
|
|
||||||
.tab-switcher {
|
.tab-switcher {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
&.top-tabs {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
> .tabs {
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-top: 5px;
|
||||||
|
flex-direction: row;
|
||||||
|
&::after, &::before {
|
||||||
|
content: '';
|
||||||
|
flex: 1 1 auto;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
border-bottom-color: $fallback--border;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
.tab-wrapper {
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
|
&:not(.active)::after {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
border-bottom-color: $fallback--border;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 1px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
padding-bottom: 99px;
|
||||||
|
margin-bottom: 6px - 99px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contents.scrollable-tabs {
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.side-tabs {
|
||||||
|
flex-direction: row;
|
||||||
|
> .contents {
|
||||||
|
flex: 0 1 80%;
|
||||||
|
}
|
||||||
|
> .tabs {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-top: 5px;
|
||||||
|
flex-direction: column;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1 1 auto;
|
||||||
|
border-right: 1px solid;
|
||||||
|
border-right-color: $fallback--border;
|
||||||
|
border-right-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
.tab-wrapper {
|
||||||
|
min-width: 10em;
|
||||||
|
&: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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
min-width: 10em;
|
||||||
|
min-width: 1px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
// padding-right: 200px;
|
||||||
|
// margin-right: 6px - 200px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-wrapper {
|
||||||
|
min-width: 10em;
|
||||||
|
&: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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
min-width: 10em;
|
||||||
|
min-width: 1px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
// padding-right: 200px;
|
||||||
|
// margin-right: 6px - 200px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.contents {
|
.contents {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
@ -13,45 +122,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.scrollable-tabs {
|
&.scrollable-tabs {
|
||||||
flex-basis: 0;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: hidden;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-top: 5px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&::after, &::before {
|
|
||||||
display: block;
|
|
||||||
content: '';
|
|
||||||
flex: 1 1 auto;
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
border-bottom-color: $fallback--border;
|
|
||||||
border-bottom-color: var(--border, $fallback--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-wrapper {
|
|
||||||
height: 28px;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
width: 100%;
|
|
||||||
min-width: 1px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
padding: 6px 1em;
|
|
||||||
padding-bottom: 99px;
|
|
||||||
margin-bottom: 6px - 99px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
padding: 6px 1em;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--tabText, $fallback--text);
|
color: var(--tabText, $fallback--text);
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
|
@ -79,20 +158,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::after, &::before {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 7;
|
z-index: 7;
|
||||||
border-bottom: 1px solid;
|
|
||||||
border-bottom-color: $fallback--border;
|
|
||||||
border-bottom-color: var(--border, $fallback--border);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
import unescape from 'lodash/unescape'
|
|
||||||
import get from 'lodash/get'
|
import get from 'lodash/get'
|
||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
import reject from 'lodash/reject'
|
import reject from 'lodash/reject'
|
||||||
|
import Autosuggest from '../autosuggest/autosuggest.vue'
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
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 BlockCard from '../block_card/block_card.vue'
|
||||||
import MuteCard from '../mute_card/mute_card.vue'
|
import MuteCard from '../mute_card/mute_card.vue'
|
||||||
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
|
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
|
||||||
import SelectableList from '../selectable_list/selectable_list.vue'
|
import SelectableList from '../selectable_list/selectable_list.vue'
|
||||||
import ProgressButton from '../progress_button/progress_button.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 Importer from '../importer/importer.vue'
|
||||||
import Exporter from '../exporter/exporter.vue'
|
import Exporter from '../exporter/exporter.vue'
|
||||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
import Mfa from './mfa.vue'
|
|
||||||
|
|
||||||
const BlockList = withSubscription({
|
const BlockList = withSubscription({
|
||||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||||
|
@ -42,40 +34,7 @@ const DomainMuteList = withSubscription({
|
||||||
const UserSettings = {
|
const UserSettings = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
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',
|
activeTab: 'profile',
|
||||||
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
|
||||||
newDomainToMute: ''
|
newDomainToMute: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -83,176 +42,29 @@ const UserSettings = {
|
||||||
this.$store.dispatch('fetchTokens')
|
this.$store.dispatch('fetchTokens')
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StyleSwitcher,
|
|
||||||
ScopeSelector,
|
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
ImageCropper,
|
|
||||||
BlockList,
|
BlockList,
|
||||||
MuteList,
|
MuteList,
|
||||||
DomainMuteList,
|
DomainMuteList,
|
||||||
EmojiInput,
|
|
||||||
Autosuggest,
|
|
||||||
BlockCard,
|
BlockCard,
|
||||||
MuteCard,
|
MuteCard,
|
||||||
DomainMuteCard,
|
DomainMuteCard,
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Importer,
|
Autosuggest,
|
||||||
Exporter,
|
|
||||||
Mfa,
|
|
||||||
Checkbox
|
Checkbox
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
emojiUserSuggestor () {
|
|
||||||
return suggestor({
|
|
||||||
emoji: [
|
|
||||||
...this.$store.state.instance.emoji,
|
|
||||||
...this.$store.state.instance.customEmoji
|
|
||||||
],
|
|
||||||
users: this.$store.state.users.users,
|
|
||||||
updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
emojiSuggestor () {
|
|
||||||
return suggestor({ emoji: [
|
|
||||||
...this.$store.state.instance.emoji,
|
|
||||||
...this.$store.state.instance.customEmoji
|
|
||||||
] })
|
|
||||||
},
|
|
||||||
pleromaBackend () {
|
pleromaBackend () {
|
||||||
return this.$store.state.instance.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 () {
|
currentSaveStateNotice () {
|
||||||
return this.$store.state.interface.settings.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: {
|
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) {
|
importFollows (file) {
|
||||||
return this.$store.state.api.backendInteractor.importFollows({ file })
|
return this.$store.state.api.backendInteractor.importFollows({ file })
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
|
@ -281,74 +93,9 @@ const UserSettings = {
|
||||||
return user.screen_name
|
return user.screen_name
|
||||||
}).join('\n')
|
}).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) {
|
activateTab (tabName) {
|
||||||
this.activeTab = 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) {
|
filterUnblockedUsers (userIds) {
|
||||||
return reject(userIds, (userId) => {
|
return reject(userIds, (userId) => {
|
||||||
const user = this.$store.getters.findUser(userId)
|
const user = this.$store.getters.findUser(userId)
|
||||||
|
|
|
@ -25,603 +25,6 @@
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body profile-edit">
|
<div class="panel-body profile-edit">
|
||||||
<tab-switcher>
|
|
||||||
<div :label="$t('settings.profile_tab')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.name_bio') }}</h2>
|
|
||||||
<p>{{ $t('settings.name') }}</p>
|
|
||||||
<EmojiInput
|
|
||||||
v-model="newName"
|
|
||||||
enable-emoji-picker
|
|
||||||
:suggest="emojiSuggestor"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
v-model="newName"
|
|
||||||
classname="name-changer"
|
|
||||||
>
|
|
||||||
</EmojiInput>
|
|
||||||
<p>{{ $t('settings.bio') }}</p>
|
|
||||||
<EmojiInput
|
|
||||||
v-model="newBio"
|
|
||||||
enable-emoji-picker
|
|
||||||
:suggest="emojiUserSuggestor"
|
|
||||||
>
|
|
||||||
<textarea
|
|
||||||
v-model="newBio"
|
|
||||||
classname="bio"
|
|
||||||
/>
|
|
||||||
</EmojiInput>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="newLocked">
|
|
||||||
{{ $t('settings.lock_account_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
|
|
||||||
<div
|
|
||||||
id="default-vis"
|
|
||||||
class="visibility-tray"
|
|
||||||
>
|
|
||||||
<scope-selector
|
|
||||||
:show-all="true"
|
|
||||||
:user-default="newDefaultScope"
|
|
||||||
:initial-scope="newDefaultScope"
|
|
||||||
:on-scope-change="changeVis"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="newNoRichText">
|
|
||||||
{{ $t('settings.no_rich_text_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="hideFollows">
|
|
||||||
{{ $t('settings.hide_follows_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p class="setting-subitem">
|
|
||||||
<Checkbox
|
|
||||||
v-model="hideFollowsCount"
|
|
||||||
:disabled="!hideFollows"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_follows_count_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="hideFollowers">
|
|
||||||
{{ $t('settings.hide_followers_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p class="setting-subitem">
|
|
||||||
<Checkbox
|
|
||||||
v-model="hideFollowersCount"
|
|
||||||
:disabled="!hideFollowers"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_followers_count_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="allowFollowingMove">
|
|
||||||
{{ $t('settings.allow_following_move') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p v-if="role === 'admin' || role === 'moderator'">
|
|
||||||
<Checkbox v-model="showRole">
|
|
||||||
<template v-if="role === 'admin'">
|
|
||||||
{{ $t('settings.show_admin_badge') }}
|
|
||||||
</template>
|
|
||||||
<template v-if="role === 'moderator'">
|
|
||||||
{{ $t('settings.show_moderator_badge') }}
|
|
||||||
</template>
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="discoverable">
|
|
||||||
{{ $t('settings.discoverable') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
:disabled="newName && newName.length === 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="updateProfile"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.avatar') }}</h2>
|
|
||||||
<p class="visibility-notice">
|
|
||||||
{{ $t('settings.avatar_size_instruction') }}
|
|
||||||
</p>
|
|
||||||
<p>{{ $t('settings.current_avatar') }}</p>
|
|
||||||
<img
|
|
||||||
:src="user.profile_image_url_original"
|
|
||||||
class="current-avatar"
|
|
||||||
>
|
|
||||||
<p>{{ $t('settings.set_new_avatar') }}</p>
|
|
||||||
<button
|
|
||||||
v-show="pickAvatarBtnVisible"
|
|
||||||
id="pick-avatar"
|
|
||||||
class="btn"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{{ $t('settings.upload_a_photo') }}
|
|
||||||
</button>
|
|
||||||
<image-cropper
|
|
||||||
trigger="#pick-avatar"
|
|
||||||
:submit-handler="submitAvatar"
|
|
||||||
@open="pickAvatarBtnVisible=false"
|
|
||||||
@close="pickAvatarBtnVisible=true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.profile_banner') }}</h2>
|
|
||||||
<p>{{ $t('settings.current_profile_banner') }}</p>
|
|
||||||
<img
|
|
||||||
:src="user.cover_photo"
|
|
||||||
class="banner"
|
|
||||||
>
|
|
||||||
<p>{{ $t('settings.set_new_profile_banner') }}</p>
|
|
||||||
<img
|
|
||||||
v-if="bannerPreview"
|
|
||||||
class="banner"
|
|
||||||
:src="bannerPreview"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
@change="uploadFile('banner', $event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
v-if="bannerUploading"
|
|
||||||
class=" icon-spin4 animate-spin uploading"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-else-if="bannerPreview"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="submitBanner"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="bannerUploadError"
|
|
||||||
class="alert error"
|
|
||||||
>
|
|
||||||
Error: {{ bannerUploadError }}
|
|
||||||
<i
|
|
||||||
class="button-icon icon-cancel"
|
|
||||||
@click="clearUploadError('banner')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.profile_background') }}</h2>
|
|
||||||
<p>{{ $t('settings.set_new_profile_background') }}</p>
|
|
||||||
<img
|
|
||||||
v-if="backgroundPreview"
|
|
||||||
class="bg"
|
|
||||||
:src="backgroundPreview"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
@change="uploadFile('background', $event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
v-if="backgroundUploading"
|
|
||||||
class=" icon-spin4 animate-spin uploading"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-else-if="backgroundPreview"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="submitBg"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="backgroundUploadError"
|
|
||||||
class="alert error"
|
|
||||||
>
|
|
||||||
Error: {{ backgroundUploadError }}
|
|
||||||
<i
|
|
||||||
class="button-icon icon-cancel"
|
|
||||||
@click="clearUploadError('background')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.security_tab')">
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.change_email') }}</h2>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.new_email') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="newEmail"
|
|
||||||
type="email"
|
|
||||||
autocomplete="email"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.current_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changeEmailPassword"
|
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="changeEmail"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<p v-if="changedEmail">
|
|
||||||
{{ $t('settings.changed_email') }}
|
|
||||||
</p>
|
|
||||||
<template v-if="changeEmailError !== false">
|
|
||||||
<p>{{ $t('settings.change_email_error') }}</p>
|
|
||||||
<p>{{ changeEmailError }}</p>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.change_password') }}</h2>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.current_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changePasswordInputs[0]"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.new_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changePasswordInputs[1]"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>{{ $t('settings.confirm_new_password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="changePasswordInputs[2]"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="changePassword"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
<p v-if="changedPassword">
|
|
||||||
{{ $t('settings.changed_password') }}
|
|
||||||
</p>
|
|
||||||
<p v-else-if="changePasswordError !== false">
|
|
||||||
{{ $t('settings.change_password_error') }}
|
|
||||||
</p>
|
|
||||||
<p v-if="changePasswordError">
|
|
||||||
{{ changePasswordError }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.oauth_tokens') }}</h2>
|
|
||||||
<table class="oauth-tokens">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ $t('settings.app_name') }}</th>
|
|
||||||
<th>{{ $t('settings.valid_until') }}</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="oauthToken in oauthTokens"
|
|
||||||
:key="oauthToken.id"
|
|
||||||
>
|
|
||||||
<td>{{ oauthToken.appName }}</td>
|
|
||||||
<td>{{ oauthToken.validUntil }}</td>
|
|
||||||
<td class="actions">
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="revokeToken(oauthToken.id)"
|
|
||||||
>
|
|
||||||
{{ $t('settings.revoke_token') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<mfa />
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.delete_account') }}</h2>
|
|
||||||
<p v-if="!deletingAccount">
|
|
||||||
{{ $t('settings.delete_account_description') }}
|
|
||||||
</p>
|
|
||||||
<div v-if="deletingAccount">
|
|
||||||
<p>{{ $t('settings.delete_account_instructions') }}</p>
|
|
||||||
<p>{{ $t('login.password') }}</p>
|
|
||||||
<input
|
|
||||||
v-model="deleteAccountConfirmPasswordInput"
|
|
||||||
type="password"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="deleteAccount"
|
|
||||||
>
|
|
||||||
{{ $t('settings.delete_account') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p v-if="deleteAccountError !== false">
|
|
||||||
{{ $t('settings.delete_account_error') }}
|
|
||||||
</p>
|
|
||||||
<p v-if="deleteAccountError">
|
|
||||||
{{ deleteAccountError }}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
v-if="!deletingAccount"
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="confirmDelete"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="pleromaBackend"
|
|
||||||
:label="$t('settings.notifications')"
|
|
||||||
>
|
|
||||||
<div class="setting-item">
|
|
||||||
<div class="select-multiple">
|
|
||||||
<span class="label">{{ $t('settings.notification_setting') }}</span>
|
|
||||||
<ul class="option-list">
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.follows">
|
|
||||||
{{ $t('settings.notification_setting_follows') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.followers">
|
|
||||||
{{ $t('settings.notification_setting_followers') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.non_follows">
|
|
||||||
{{ $t('settings.notification_setting_non_follows') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Checkbox v-model="notificationSettings.non_followers">
|
|
||||||
{{ $t('settings.notification_setting_non_followers') }}
|
|
||||||
</Checkbox>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
|
||||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
|
||||||
<button
|
|
||||||
class="btn btn-default"
|
|
||||||
@click="updateNotificationSettings"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="pleromaBackend"
|
|
||||||
:label="$t('settings.data_import_export_tab')"
|
|
||||||
>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.follow_import') }}</h2>
|
|
||||||
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
|
|
||||||
<Importer
|
|
||||||
:submit-handler="importFollows"
|
|
||||||
:success-message="$t('settings.follows_imported')"
|
|
||||||
:error-message="$t('settings.follow_import_error')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.follow_export') }}</h2>
|
|
||||||
<Exporter
|
|
||||||
:get-content="getFollowsContent"
|
|
||||||
filename="friends.csv"
|
|
||||||
:export-button-label="$t('settings.follow_export_button')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.block_import') }}</h2>
|
|
||||||
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
|
|
||||||
<Importer
|
|
||||||
:submit-handler="importBlocks"
|
|
||||||
:success-message="$t('settings.blocks_imported')"
|
|
||||||
:error-message="$t('settings.block_import_error')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.block_export') }}</h2>
|
|
||||||
<Exporter
|
|
||||||
:get-content="getBlocksContent"
|
|
||||||
filename="blocks.csv"
|
|
||||||
:export-button-label="$t('settings.block_export_button')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.blocks_tab')">
|
|
||||||
<div class="profile-edit-usersearch-wrapper">
|
|
||||||
<Autosuggest
|
|
||||||
:filter="filterUnblockedUsers"
|
|
||||||
:query="queryUserIds"
|
|
||||||
:placeholder="$t('settings.search_user_to_block')"
|
|
||||||
>
|
|
||||||
<BlockCard
|
|
||||||
slot-scope="row"
|
|
||||||
:user-id="row.item"
|
|
||||||
/>
|
|
||||||
</Autosuggest>
|
|
||||||
</div>
|
|
||||||
<BlockList
|
|
||||||
:refresh="true"
|
|
||||||
:get-key="identity"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
slot="header"
|
|
||||||
slot-scope="{selected}"
|
|
||||||
>
|
|
||||||
<div class="profile-edit-bulk-actions">
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => blockUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.block') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.block_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => unblockUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.unblock') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.unblock_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
slot="item"
|
|
||||||
slot-scope="{item}"
|
|
||||||
>
|
|
||||||
<BlockCard :user-id="item" />
|
|
||||||
</template>
|
|
||||||
<template slot="empty">
|
|
||||||
{{ $t('settings.no_blocks') }}
|
|
||||||
</template>
|
|
||||||
</BlockList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.mutes_tab')">
|
|
||||||
<tab-switcher>
|
|
||||||
<div label="Users">
|
|
||||||
<div class="profile-edit-usersearch-wrapper">
|
|
||||||
<Autosuggest
|
|
||||||
:filter="filterUnMutedUsers"
|
|
||||||
:query="queryUserIds"
|
|
||||||
:placeholder="$t('settings.search_user_to_mute')"
|
|
||||||
>
|
|
||||||
<MuteCard
|
|
||||||
slot-scope="row"
|
|
||||||
:user-id="row.item"
|
|
||||||
/>
|
|
||||||
</Autosuggest>
|
|
||||||
</div>
|
|
||||||
<MuteList
|
|
||||||
:refresh="true"
|
|
||||||
:get-key="identity"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
slot="header"
|
|
||||||
slot-scope="{selected}"
|
|
||||||
>
|
|
||||||
<div class="profile-edit-bulk-actions">
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => muteUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.mute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.mute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => unmuteUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.unmute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('user_card.unmute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
slot="item"
|
|
||||||
slot-scope="{item}"
|
|
||||||
>
|
|
||||||
<MuteCard :user-id="item" />
|
|
||||||
</template>
|
|
||||||
<template slot="empty">
|
|
||||||
{{ $t('settings.no_mutes') }}
|
|
||||||
</template>
|
|
||||||
</MuteList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.domain_mutes')">
|
|
||||||
<div class="profile-edit-domain-mute-form">
|
|
||||||
<input
|
|
||||||
v-model="newDomainToMute"
|
|
||||||
:placeholder="$t('settings.type_domains_to_mute')"
|
|
||||||
type="text"
|
|
||||||
@keyup.enter="muteDomain"
|
|
||||||
>
|
|
||||||
<ProgressButton
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="muteDomain"
|
|
||||||
>
|
|
||||||
{{ $t('domain_mute_card.mute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('domain_mute_card.mute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
<DomainMuteList
|
|
||||||
:refresh="true"
|
|
||||||
:get-key="identity"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
slot="header"
|
|
||||||
slot-scope="{selected}"
|
|
||||||
>
|
|
||||||
<div class="profile-edit-bulk-actions">
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn btn-default"
|
|
||||||
:click="() => unmuteDomains(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('domain_mute_card.unmute') }}
|
|
||||||
<template slot="progress">
|
|
||||||
{{ $t('domain_mute_card.unmute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
slot="item"
|
|
||||||
slot-scope="{item}"
|
|
||||||
>
|
|
||||||
<DomainMuteCard :domain="item" />
|
|
||||||
</template>
|
|
||||||
<template slot="empty">
|
|
||||||
{{ $t('settings.no_mutes') }}
|
|
||||||
</template>
|
|
||||||
</DomainMuteList>
|
|
||||||
</div>
|
|
||||||
</tab-switcher>
|
|
||||||
</div>
|
|
||||||
</tab-switcher>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -278,6 +278,7 @@
|
||||||
"current_avatar": "Your current avatar",
|
"current_avatar": "Your current avatar",
|
||||||
"current_password": "Current password",
|
"current_password": "Current password",
|
||||||
"current_profile_banner": "Your current profile banner",
|
"current_profile_banner": "Your current profile banner",
|
||||||
|
"mutes_and_blocks": "Mutes and Blocks",
|
||||||
"data_import_export_tab": "Data Import / Export",
|
"data_import_export_tab": "Data Import / Export",
|
||||||
"default_vis": "Default visibility scope",
|
"default_vis": "Default visibility scope",
|
||||||
"delete_account": "Delete Account",
|
"delete_account": "Delete Account",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { set, delete as del } from 'vue'
|
import { set, delete as del } from 'vue'
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
settingsModalState: 'hidden',
|
||||||
settings: {
|
settings: {
|
||||||
currentSaveStateNotice: null,
|
currentSaveStateNotice: null,
|
||||||
noticeClearTimeout: null,
|
noticeClearTimeout: null,
|
||||||
|
@ -35,6 +36,24 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
setMobileLayout (state, value) {
|
setMobileLayout (state, value) {
|
||||||
state.mobileLayout = value
|
state.mobileLayout = value
|
||||||
|
},
|
||||||
|
closeSettingsModal (state) {
|
||||||
|
state.settingsModalState = 'hidden'
|
||||||
|
},
|
||||||
|
togglePeekSettingsModal (state) {
|
||||||
|
switch (state.settingsModalState) {
|
||||||
|
case 'minimized':
|
||||||
|
state.settingsModalState = 'visible'
|
||||||
|
return
|
||||||
|
case 'visible':
|
||||||
|
state.settingsModalState = 'minimized'
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
throw new Error('Illegal minimization state of settings modal')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openSettingsModal (state) {
|
||||||
|
state.settingsModalState = 'visible'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -49,6 +68,15 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
setMobileLayout ({ commit }, value) {
|
setMobileLayout ({ commit }, value) {
|
||||||
commit('setMobileLayout', value)
|
commit('setMobileLayout', value)
|
||||||
|
},
|
||||||
|
closeSettingsModal ({ commit }) {
|
||||||
|
commit('closeSettingsModal')
|
||||||
|
},
|
||||||
|
openSettingsModal ({ commit }) {
|
||||||
|
commit('openSettingsModal')
|
||||||
|
},
|
||||||
|
togglePeekSettingsModal ({ commit }) {
|
||||||
|
commit('togglePeekSettingsModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue