aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js5
-rw-r--r--src/App.scss3
-rw-r--r--src/App.vue7
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.js2
-rw-r--r--src/components/post_status_modal/post_status_modal.js3
-rw-r--r--src/components/post_status_modal/post_status_modal.vue1
-rw-r--r--src/components/settings_modal/settings_modal.js39
-rw-r--r--src/components/settings_modal/settings_modal.scss59
-rw-r--r--src/components/settings_modal/settings_modal.vue31
-rw-r--r--src/components/settings_modal/tabs/data_import_export.js65
-rw-r--r--src/components/settings_modal/tabs/data_import_export.vue43
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks.js124
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks.vue173
-rw-r--r--src/components/settings_modal/tabs/notifications.js27
-rw-r--r--src/components/settings_modal/tabs/notifications.vue42
-rw-r--r--src/components/settings_modal/tabs/profile.js179
-rw-r--r--src/components/settings_modal/tabs/profile.scss82
-rw-r--r--src/components/settings_modal/tabs/profile.vue213
-rw-r--r--src/components/settings_modal/tabs/security.js106
-rw-r--r--src/components/settings_modal/tabs/security.vue143
-rw-r--r--src/components/tab_switcher/tab_switcher.js7
-rw-r--r--src/components/tab_switcher/tab_switcher.scss220
-rw-r--r--src/components/user_settings/user_settings.js257
-rw-r--r--src/components/user_settings/user_settings.vue597
-rw-r--r--src/i18n/en.json1
-rw-r--r--src/modules/interface.js28
26 files changed, 1530 insertions, 927 deletions
diff --git a/src/App.js b/src/App.js
index 61b5eec1..4d9d50d4 100644
--- a/src/App.js
+++ b/src/App.js
@@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ChatPanel from './components/chat_panel/chat_panel.vue'
+import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
@@ -29,6 +30,7 @@ export default {
SideDrawer,
MobilePostStatusButton,
MobileNav,
+ SettingsModal,
UserReportingModal,
PostStatusModal
},
@@ -112,6 +114,9 @@ export default {
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden
},
+ openSettingsModal () {
+ this.$store.dispatch('openSettingsModal')
+ },
updateMobileState () {
const mobileLayout = windowWidth() <= 800
const changed = mobileLayout !== this.isMobileLayout
diff --git a/src/App.scss b/src/App.scss
index 89aa3215..7db9461c 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -860,6 +860,7 @@ nav {
}
}
+// DELETE
.setting-item {
border-bottom: 2px solid var(--fg, $fallback--fg);
margin: 1em 1em 1.4em;
@@ -905,6 +906,8 @@ nav {
max-width: 6em;
}
}
+// DELETE
+
.select-multiple {
display: flex;
.option-list {
diff --git a/src/App.vue b/src/App.vue
index ff62fc51..db3f981f 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -46,15 +46,15 @@
@toggled="onSearchBarToggled"
@click.stop.native
/>
- <router-link
+ <a
class="mobile-hidden"
- :to="{ name: 'settings'}"
+ @click.stop="openSettingsModal"
>
<i
class="button-icon icon-cog nav-icon"
:title="$t('nav.preferences')"
/>
- </router-link>
+ </a>
<a
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"
@@ -122,6 +122,7 @@
<MobilePostStatusButton />
<UserReportingModal />
<PostStatusModal />
+ <SettingsModal />
<portal-target name="modal" />
</div>
</template>
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
index 0ad12bb1..ff2d4eaa 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.js
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.js
@@ -52,7 +52,7 @@ const MobilePostStatusButton = {
window.removeEventListener('scroll', this.handleScrollEnd)
},
openPostForm () {
- this.$store.dispatch('openPostStatusModal')
+ this.$store.dispatch('openSettingsModal')
},
handleOSK () {
// This is a big hack: we're guessing from changed window sizes if the
diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js
index b44354db..be945400 100644
--- a/src/components/post_status_modal/post_status_modal.js
+++ b/src/components/post_status_modal/post_status_modal.js
@@ -13,9 +13,6 @@ const PostStatusModal = {
}
},
computed: {
- isLoggedIn () {
- return !!this.$store.state.users.currentUser
- },
modalActivated () {
return this.$store.state.postStatus.modalActivated
},
diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue
index dbcd321e..07c58f74 100644
--- a/src/components/post_status_modal/post_status_modal.vue
+++ b/src/components/post_status_modal/post_status_modal.vue
@@ -1,6 +1,5 @@
<template>
<Modal
- v-if="isLoggedIn && !resettingForm"
:is-open="modalActivated"
class="post-form-modal-view"
@backdropClicked="closeModal"
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
new file mode 100644
index 00000000..1f4c038f
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.js
@@ -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
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
new file mode 100644
index 00000000..8cea52d2
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
new file mode 100644
index 00000000..9e35d3f6
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.vue
@@ -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>
diff --git a/src/components/settings_modal/tabs/data_import_export.js b/src/components/settings_modal/tabs/data_import_export.js
new file mode 100644
index 00000000..f68d12e9
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export.js
@@ -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
diff --git a/src/components/settings_modal/tabs/data_import_export.vue b/src/components/settings_modal/tabs/data_import_export.vue
new file mode 100644
index 00000000..464df6d3
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export.vue
@@ -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> -->
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.js b/src/components/settings_modal/tabs/mutes_and_blocks.js
new file mode 100644
index 00000000..51895ddc
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks.js
@@ -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
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.vue b/src/components/settings_modal/tabs/mutes_and_blocks.vue
new file mode 100644
index 00000000..3aff47a0
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks.vue
@@ -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> -->
diff --git a/src/components/settings_modal/tabs/notifications.js b/src/components/settings_modal/tabs/notifications.js
new file mode 100644
index 00000000..0a870b3f
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications.js
@@ -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
diff --git a/src/components/settings_modal/tabs/notifications.vue b/src/components/settings_modal/tabs/notifications.vue
new file mode 100644
index 00000000..f9a7c17b
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications.vue
@@ -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> -->
diff --git a/src/components/settings_modal/tabs/profile.js b/src/components/settings_modal/tabs/profile.js
new file mode 100644
index 00000000..18c44024
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.js
@@ -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
diff --git a/src/components/settings_modal/tabs/profile.scss b/src/components/settings_modal/tabs/profile.scss
new file mode 100644
index 00000000..4aab81eb
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.scss
@@ -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;
+ }
+}
diff --git a/src/components/settings_modal/tabs/profile.vue b/src/components/settings_modal/tabs/profile.vue
new file mode 100644
index 00000000..335fc12e
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.vue
@@ -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>
diff --git a/src/components/settings_modal/tabs/security.js b/src/components/settings_modal/tabs/security.js
new file mode 100644
index 00000000..cc791b7a
--- /dev/null
+++ b/src/components/settings_modal/tabs/security.js
@@ -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
diff --git a/src/components/settings_modal/tabs/security.vue b/src/components/settings_modal/tabs/security.vue
new file mode 100644
index 00000000..603c9a04
--- /dev/null
+++ b/src/components/settings_modal/tabs/security.vue
@@ -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> -->
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 008e1e95..97791de3 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -24,6 +24,11 @@ export default Vue.component('tab-switcher', {
required: false,
type: Boolean,
default: false
+ },
+ sideTabBar: {
+ required: false,
+ type: Boolean,
+ default: false
}
},
data () {
@@ -105,7 +110,7 @@ export default Vue.component('tab-switcher', {
})
return (
- <div class="tab-switcher">
+ <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
<div class="tabs">
{tabs}
</div>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index df585faa..a443531e 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -2,7 +2,116 @@
.tab-switcher {
display: flex;
- flex-direction: column;
+
+ &.top-tabs {
+ flex-direction: column;
+ > .tabs {
+ width: 100%;
+ overflow-y: hidden;
+ overflow-x: auto;
+ padding-top: 5px;
+ flex-direction: row;
+ &::after, &::before {
+ content: '';
+ flex: 1 1 auto;
+ border-bottom: 1px solid;
+ border-bottom-color: $fallback--border;
+ border-bottom-color: var(--border, $fallback--border);
+ }
+ .tab-wrapper {
+ height: 28px;
+
+ &:not(.active)::after {
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-bottom: 1px solid;
+ border-bottom-color: $fallback--border;
+ border-bottom-color: var(--border, $fallback--border);
+ }
+ }
+ .tab {
+ width: 100%;
+ min-width: 1px;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ padding-bottom: 99px;
+ margin-bottom: 6px - 99px;
+ }
+ }
+ .contents.scrollable-tabs {
+ flex-basis: 0;
+ }
+ }
+
+ &.side-tabs {
+ flex-direction: row;
+ > .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 {
flex: 1 0 auto;
@@ -13,86 +122,65 @@
}
&.scrollable-tabs {
- flex-basis: 0;
overflow-y: auto;
}
}
+
+ .tab {
+ position: relative;
+ white-space: nowrap;
+
+ padding: 6px 1em;
+ color: $fallback--text;
+ color: var(--tabText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--tab, $fallback--fg);
+
+ &:not(.active) {
+ z-index: 4;
+
+ &:hover {
+ z-index: 6;
+ }
+ }
+
+ &.active {
+ background: transparent;
+ z-index: 5;
+ color: $fallback--text;
+ color: var(--tabActiveText, $fallback--text);
+ }
+
+ img {
+ max-height: 26px;
+ vertical-align: top;
+ margin-top: -5px;
+ }
+ }
+
+
.tabs {
display: flex;
position: relative;
- width: 100%;
- overflow-y: hidden;
- overflow-x: auto;
- padding-top: 5px;
box-sizing: border-box;
&::after, &::before {
display: block;
- content: '';
flex: 1 1 auto;
- border-bottom: 1px solid;
- border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
}
+ }
- .tab-wrapper {
- height: 28px;
- position: relative;
- display: flex;
- flex: 0 0 auto;
-
- .tab {
- width: 100%;
- min-width: 1px;
- position: relative;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- padding: 6px 1em;
- padding-bottom: 99px;
- margin-bottom: 6px - 99px;
- white-space: nowrap;
-
- color: $fallback--text;
- color: var(--tabText, $fallback--text);
- background-color: $fallback--fg;
- background-color: var(--tab, $fallback--fg);
-
- &:not(.active) {
- z-index: 4;
-
- &:hover {
- z-index: 6;
- }
- }
-
- &.active {
- background: transparent;
- z-index: 5;
- color: $fallback--text;
- color: var(--tabActiveText, $fallback--text);
- }
-
- img {
- max-height: 26px;
- vertical-align: top;
- margin-top: -5px;
- }
- }
+ .tab-wrapper {
+ position: relative;
+ display: flex;
+ flex: 0 0 auto;
- &:not(.active) {
- &::after {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 7;
- border-bottom: 1px solid;
- border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
- }
+ &:not(.active) {
+ &::after {
+ content: '';
+ position: absolute;
+ z-index: 7;
}
}
-
}
}
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index eca6f9b1..e07d4e56 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,25 +1,17 @@
-import unescape from 'lodash/unescape'
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 ImageCropper from '../image_cropper/image_cropper.vue'
-import StyleSwitcher from '../style_switcher/style_switcher.vue'
-import ScopeSelector from '../scope_selector/scope_selector.vue'
-import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
-import EmojiInput from '../emoji_input/emoji_input.vue'
-import suggestor from '../emoji_input/suggestor.js'
-import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
import Checkbox from '../checkbox/checkbox.vue'
-import Mfa from './mfa.vue'
const BlockList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
@@ -42,40 +34,7 @@ const DomainMuteList = withSubscription({
const UserSettings = {
data () {
return {
- newEmail: '',
- newName: this.$store.state.users.currentUser.name,
- newBio: unescape(this.$store.state.users.currentUser.description),
- newLocked: this.$store.state.users.currentUser.locked,
- newNoRichText: this.$store.state.users.currentUser.no_rich_text,
- newDefaultScope: this.$store.state.users.currentUser.default_scope,
- hideFollows: this.$store.state.users.currentUser.hide_follows,
- hideFollowers: this.$store.state.users.currentUser.hide_followers,
- hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
- hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
- showRole: this.$store.state.users.currentUser.show_role,
- role: this.$store.state.users.currentUser.role,
- discoverable: this.$store.state.users.currentUser.discoverable,
- allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
- pickAvatarBtnVisible: true,
- bannerUploading: false,
- backgroundUploading: false,
- banner: null,
- bannerPreview: null,
- background: null,
- backgroundPreview: null,
- bannerUploadError: null,
- backgroundUploadError: null,
- changeEmailError: false,
- changeEmailPassword: '',
- changedEmail: false,
- deletingAccount: false,
- deleteAccountConfirmPasswordInput: '',
- deleteAccountError: false,
- changePasswordInputs: [ '', '', '' ],
- changedPassword: false,
- changePasswordError: false,
activeTab: 'profile',
- notificationSettings: this.$store.state.users.currentUser.notification_settings,
newDomainToMute: ''
}
},
@@ -83,176 +42,29 @@ const UserSettings = {
this.$store.dispatch('fetchTokens')
},
components: {
- StyleSwitcher,
- ScopeSelector,
TabSwitcher,
- ImageCropper,
BlockList,
MuteList,
DomainMuteList,
- EmojiInput,
- Autosuggest,
BlockCard,
MuteCard,
DomainMuteCard,
ProgressButton,
- Importer,
- Exporter,
- Mfa,
+ Autosuggest,
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
},
- emojiUserSuggestor () {
- return suggestor({
- emoji: [
- ...this.$store.state.instance.emoji,
- ...this.$store.state.instance.customEmoji
- ],
- users: this.$store.state.users.users,
- updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
- })
- },
- emojiSuggestor () {
- return suggestor({ emoji: [
- ...this.$store.state.instance.emoji,
- ...this.$store.state.instance.customEmoji
- ] })
- },
pleromaBackend () {
return this.$store.state.instance.pleromaBackend
},
- minimalScopesMode () {
- return this.$store.state.instance.minimalScopesMode
- },
- vis () {
- return {
- public: { selected: this.newDefaultScope === 'public' },
- unlisted: { selected: this.newDefaultScope === 'unlisted' },
- private: { selected: this.newDefaultScope === 'private' },
- direct: { selected: this.newDefaultScope === 'direct' }
- }
- },
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
- },
- oauthTokens () {
- return this.$store.state.oauthTokens.tokens.map(oauthToken => {
- return {
- id: oauthToken.id,
- appName: oauthToken.app_name,
- validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
- }
- })
}
},
methods: {
- updateProfile () {
- this.$store.state.api.backendInteractor
- .updateProfile({
- params: {
- note: this.newBio,
- locked: this.newLocked,
- // Backend notation.
- /* eslint-disable camelcase */
- display_name: this.newName,
- default_scope: this.newDefaultScope,
- no_rich_text: this.newNoRichText,
- hide_follows: this.hideFollows,
- hide_followers: this.hideFollowers,
- discoverable: this.discoverable,
- allow_following_move: this.allowFollowingMove,
- hide_follows_count: this.hideFollowsCount,
- hide_followers_count: this.hideFollowersCount,
- show_role: this.showRole
- /* eslint-enable camelcase */
- } }).then((user) => {
- this.$store.commit('addNewUsers', [user])
- this.$store.commit('setCurrentUser', user)
- })
- },
- updateNotificationSettings () {
- this.$store.state.api.backendInteractor
- .updateNotificationSettings({ settings: this.notificationSettings })
- },
- changeVis (visibility) {
- this.newDefaultScope = visibility
- },
- uploadFile (slot, e) {
- const file = e.target.files[0]
- if (!file) { return }
- if (file.size > this.$store.state.instance[slot + 'limit']) {
- const filesize = fileSizeFormatService.fileSizeFormat(file.size)
- const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
- this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
- return
- }
- // eslint-disable-next-line no-undef
- const reader = new FileReader()
- reader.onload = ({ target }) => {
- const img = target.result
- this[slot + 'Preview'] = img
- this[slot] = file
- }
- reader.readAsDataURL(file)
- },
- submitAvatar (cropper, file) {
- const that = this
- return new Promise((resolve, reject) => {
- function updateAvatar (avatar) {
- that.$store.state.api.backendInteractor.updateAvatar({ avatar })
- .then((user) => {
- that.$store.commit('addNewUsers', [user])
- that.$store.commit('setCurrentUser', user)
- resolve()
- })
- .catch((err) => {
- reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
- })
- }
-
- if (cropper) {
- cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
- } else {
- updateAvatar(file)
- }
- })
- },
- clearUploadError (slot) {
- this[slot + 'UploadError'] = null
- },
- submitBanner () {
- if (!this.bannerPreview) { return }
-
- this.bannerUploading = true
- this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
- .then((user) => {
- this.$store.commit('addNewUsers', [user])
- this.$store.commit('setCurrentUser', user)
- this.bannerPreview = null
- })
- .catch((err) => {
- this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
- })
- .then(() => { this.bannerUploading = false })
- },
- submitBg () {
- if (!this.backgroundPreview) { return }
- let background = this.background
- this.backgroundUploading = true
- this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
- if (!data.error) {
- this.$store.commit('addNewUsers', [data])
- this.$store.commit('setCurrentUser', data)
- this.backgroundPreview = null
- } else {
- this.backgroundUploadError = this.$t('upload.error.base') + data.error
- }
- this.backgroundUploading = false
- })
- },
importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
@@ -281,74 +93,9 @@ const UserSettings = {
return user.screen_name
}).join('\n')
},
- getFollowsContent () {
- return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
- .then(this.generateExportableUsersContent)
- },
- getBlocksContent () {
- return this.$store.state.api.backendInteractor.fetchBlocks()
- .then(this.generateExportableUsersContent)
- },
- confirmDelete () {
- this.deletingAccount = true
- },
- deleteAccount () {
- this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
- .then((res) => {
- if (res.status === 'success') {
- this.$store.dispatch('logout')
- this.$router.push({ name: 'root' })
- } else {
- this.deleteAccountError = res.error
- }
- })
- },
- changePassword () {
- const params = {
- password: this.changePasswordInputs[0],
- newPassword: this.changePasswordInputs[1],
- newPasswordConfirmation: this.changePasswordInputs[2]
- }
- this.$store.state.api.backendInteractor.changePassword(params)
- .then((res) => {
- if (res.status === 'success') {
- this.changedPassword = true
- this.changePasswordError = false
- this.logout()
- } else {
- this.changedPassword = false
- this.changePasswordError = res.error
- }
- })
- },
- changeEmail () {
- const params = {
- email: this.newEmail,
- password: this.changeEmailPassword
- }
- this.$store.state.api.backendInteractor.changeEmail(params)
- .then((res) => {
- if (res.status === 'success') {
- this.changedEmail = true
- this.changeEmailError = false
- } else {
- this.changedEmail = false
- this.changeEmailError = res.error
- }
- })
- },
activateTab (tabName) {
this.activeTab = tabName
},
- logout () {
- this.$store.dispatch('logout')
- this.$router.replace('/')
- },
- revokeToken (id) {
- if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
- this.$store.dispatch('revokeToken', id)
- }
- },
filterUnblockedUsers (userIds) {
return reject(userIds, (userId) => {
const user = this.$store.getters.findUser(userId)
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 8b2336b4..2a88714f 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -25,603 +25,6 @@
</transition>
</div>
<div class="panel-body profile-edit">
- <tab-switcher>
- <div :label="$t('settings.profile_tab')">
- <div class="setting-item">
- <h2>{{ $t('settings.name_bio') }}</h2>
- <p>{{ $t('settings.name') }}</p>
- <EmojiInput
- v-model="newName"
- enable-emoji-picker
- :suggest="emojiSuggestor"
- >
- <input
- id="username"
- v-model="newName"
- classname="name-changer"
- >
- </EmojiInput>
- <p>{{ $t('settings.bio') }}</p>
- <EmojiInput
- v-model="newBio"
- enable-emoji-picker
- :suggest="emojiUserSuggestor"
- >
- <textarea
- v-model="newBio"
- classname="bio"
- />
- </EmojiInput>
- <p>
- <Checkbox v-model="newLocked">
- {{ $t('settings.lock_account_description') }}
- </Checkbox>
- </p>
- <div>
- <label for="default-vis">{{ $t('settings.default_vis') }}</label>
- <div
- id="default-vis"
- class="visibility-tray"
- >
- <scope-selector
- :show-all="true"
- :user-default="newDefaultScope"
- :initial-scope="newDefaultScope"
- :on-scope-change="changeVis"
- />
- </div>
- </div>
- <p>
- <Checkbox v-model="newNoRichText">
- {{ $t('settings.no_rich_text_description') }}
- </Checkbox>
- </p>
- <p>
- <Checkbox v-model="hideFollows">
- {{ $t('settings.hide_follows_description') }}
- </Checkbox>
- </p>
- <p class="setting-subitem">
- <Checkbox
- v-model="hideFollowsCount"
- :disabled="!hideFollows"
- >
- {{ $t('settings.hide_follows_count_description') }}
- </Checkbox>
- </p>
- <p>
- <Checkbox v-model="hideFollowers">
- {{ $t('settings.hide_followers_description') }}
- </Checkbox>
- </p>
- <p class="setting-subitem">
- <Checkbox
- v-model="hideFollowersCount"
- :disabled="!hideFollowers"
- >
- {{ $t('settings.hide_followers_count_description') }}
- </Checkbox>
- </p>
- <p>
- <Checkbox v-model="allowFollowingMove">
- {{ $t('settings.allow_following_move') }}
- </Checkbox>
- </p>
- <p v-if="role === 'admin' || role === 'moderator'">
- <Checkbox v-model="showRole">
- <template v-if="role === 'admin'">
- {{ $t('settings.show_admin_badge') }}
- </template>
- <template v-if="role === 'moderator'">
- {{ $t('settings.show_moderator_badge') }}
- </template>
- </Checkbox>
- </p>
- <p>
- <Checkbox v-model="discoverable">
- {{ $t('settings.discoverable') }}
- </Checkbox>
- </p>
- <button
- :disabled="newName && newName.length === 0"
- class="btn btn-default"
- @click="updateProfile"
- >
- {{ $t('general.submit') }}
- </button>
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.avatar') }}</h2>
- <p class="visibility-notice">
- {{ $t('settings.avatar_size_instruction') }}
- </p>
- <p>{{ $t('settings.current_avatar') }}</p>
- <img
- :src="user.profile_image_url_original"
- class="current-avatar"
- >
- <p>{{ $t('settings.set_new_avatar') }}</p>
- <button
- v-show="pickAvatarBtnVisible"
- id="pick-avatar"
- class="btn"
- type="button"
- >
- {{ $t('settings.upload_a_photo') }}
- </button>
- <image-cropper
- trigger="#pick-avatar"
- :submit-handler="submitAvatar"
- @open="pickAvatarBtnVisible=false"
- @close="pickAvatarBtnVisible=true"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.profile_banner') }}</h2>
- <p>{{ $t('settings.current_profile_banner') }}</p>
- <img
- :src="user.cover_photo"
- class="banner"
- >
- <p>{{ $t('settings.set_new_profile_banner') }}</p>
- <img
- v-if="bannerPreview"
- class="banner"
- :src="bannerPreview"
- >
- <div>
- <input
- type="file"
- @change="uploadFile('banner', $event)"
- >
- </div>
- <i
- v-if="bannerUploading"
- class=" icon-spin4 animate-spin uploading"
- />
- <button
- v-else-if="bannerPreview"
- class="btn btn-default"
- @click="submitBanner"
- >
- {{ $t('general.submit') }}
- </button>
- <div
- v-if="bannerUploadError"
- class="alert error"
- >
- Error: {{ bannerUploadError }}
- <i
- class="button-icon icon-cancel"
- @click="clearUploadError('banner')"
- />
- </div>
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.profile_background') }}</h2>
- <p>{{ $t('settings.set_new_profile_background') }}</p>
- <img
- v-if="backgroundPreview"
- class="bg"
- :src="backgroundPreview"
- >
- <div>
- <input
- type="file"
- @change="uploadFile('background', $event)"
- >
- </div>
- <i
- v-if="backgroundUploading"
- class=" icon-spin4 animate-spin uploading"
- />
- <button
- v-else-if="backgroundPreview"
- class="btn btn-default"
- @click="submitBg"
- >
- {{ $t('general.submit') }}
- </button>
- <div
- v-if="backgroundUploadError"
- class="alert error"
- >
- Error: {{ backgroundUploadError }}
- <i
- class="button-icon icon-cancel"
- @click="clearUploadError('background')"
- />
- </div>
- </div>
- </div>
-
- <div :label="$t('settings.security_tab')">
- <div class="setting-item">
- <h2>{{ $t('settings.change_email') }}</h2>
- <div>
- <p>{{ $t('settings.new_email') }}</p>
- <input
- v-model="newEmail"
- type="email"
- autocomplete="email"
- >
- </div>
- <div>
- <p>{{ $t('settings.current_password') }}</p>
- <input
- v-model="changeEmailPassword"
- type="password"
- autocomplete="current-password"
- >
- </div>
- <button
- class="btn btn-default"
- @click="changeEmail"
- >
- {{ $t('general.submit') }}
- </button>
- <p v-if="changedEmail">
- {{ $t('settings.changed_email') }}
- </p>
- <template v-if="changeEmailError !== false">
- <p>{{ $t('settings.change_email_error') }}</p>
- <p>{{ changeEmailError }}</p>
- </template>
- </div>
-
- <div class="setting-item">
- <h2>{{ $t('settings.change_password') }}</h2>
- <div>
- <p>{{ $t('settings.current_password') }}</p>
- <input
- v-model="changePasswordInputs[0]"
- type="password"
- >
- </div>
- <div>
- <p>{{ $t('settings.new_password') }}</p>
- <input
- v-model="changePasswordInputs[1]"
- type="password"
- >
- </div>
- <div>
- <p>{{ $t('settings.confirm_new_password') }}</p>
- <input
- v-model="changePasswordInputs[2]"
- type="password"
- >
- </div>
- <button
- class="btn btn-default"
- @click="changePassword"
- >
- {{ $t('general.submit') }}
- </button>
- <p v-if="changedPassword">
- {{ $t('settings.changed_password') }}
- </p>
- <p v-else-if="changePasswordError !== false">
- {{ $t('settings.change_password_error') }}
- </p>
- <p v-if="changePasswordError">
- {{ changePasswordError }}
- </p>
- </div>
-
- <div class="setting-item">
- <h2>{{ $t('settings.oauth_tokens') }}</h2>
- <table class="oauth-tokens">
- <thead>
- <tr>
- <th>{{ $t('settings.app_name') }}</th>
- <th>{{ $t('settings.valid_until') }}</th>
- <th />
- </tr>
- </thead>
- <tbody>
- <tr
- v-for="oauthToken in oauthTokens"
- :key="oauthToken.id"
- >
- <td>{{ oauthToken.appName }}</td>
- <td>{{ oauthToken.validUntil }}</td>
- <td class="actions">
- <button
- class="btn btn-default"
- @click="revokeToken(oauthToken.id)"
- >
- {{ $t('settings.revoke_token') }}
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <mfa />
- <div class="setting-item">
- <h2>{{ $t('settings.delete_account') }}</h2>
- <p v-if="!deletingAccount">
- {{ $t('settings.delete_account_description') }}
- </p>
- <div v-if="deletingAccount">
- <p>{{ $t('settings.delete_account_instructions') }}</p>
- <p>{{ $t('login.password') }}</p>
- <input
- v-model="deleteAccountConfirmPasswordInput"
- type="password"
- >
- <button
- class="btn btn-default"
- @click="deleteAccount"
- >
- {{ $t('settings.delete_account') }}
- </button>
- </div>
- <p v-if="deleteAccountError !== false">
- {{ $t('settings.delete_account_error') }}
- </p>
- <p v-if="deleteAccountError">
- {{ deleteAccountError }}
- </p>
- <button
- v-if="!deletingAccount"
- class="btn btn-default"
- @click="confirmDelete"
- >
- {{ $t('general.submit') }}
- </button>
- </div>
- </div>
-
- <div
- v-if="pleromaBackend"
- :label="$t('settings.notifications')"
- >
- <div class="setting-item">
- <div class="select-multiple">
- <span class="label">{{ $t('settings.notification_setting') }}</span>
- <ul class="option-list">
- <li>
- <Checkbox v-model="notificationSettings.follows">
- {{ $t('settings.notification_setting_follows') }}
- </Checkbox>
- </li>
- <li>
- <Checkbox v-model="notificationSettings.followers">
- {{ $t('settings.notification_setting_followers') }}
- </Checkbox>
- </li>
- <li>
- <Checkbox v-model="notificationSettings.non_follows">
- {{ $t('settings.notification_setting_non_follows') }}
- </Checkbox>
- </li>
- <li>
- <Checkbox v-model="notificationSettings.non_followers">
- {{ $t('settings.notification_setting_non_followers') }}
- </Checkbox>
- </li>
- </ul>
- </div>
- <p>{{ $t('settings.notification_mutes') }}</p>
- <p>{{ $t('settings.notification_blocks') }}</p>
- <button
- class="btn btn-default"
- @click="updateNotificationSettings"
- >
- {{ $t('general.submit') }}
- </button>
- </div>
- </div>
-
- <div
- v-if="pleromaBackend"
- :label="$t('settings.data_import_export_tab')"
- >
- <div class="setting-item">
- <h2>{{ $t('settings.follow_import') }}</h2>
- <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
- <Importer
- :submit-handler="importFollows"
- :success-message="$t('settings.follows_imported')"
- :error-message="$t('settings.follow_import_error')"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.follow_export') }}</h2>
- <Exporter
- :get-content="getFollowsContent"
- filename="friends.csv"
- :export-button-label="$t('settings.follow_export_button')"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.block_import') }}</h2>
- <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
- <Importer
- :submit-handler="importBlocks"
- :success-message="$t('settings.blocks_imported')"
- :error-message="$t('settings.block_import_error')"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.block_export') }}</h2>
- <Exporter
- :get-content="getBlocksContent"
- filename="blocks.csv"
- :export-button-label="$t('settings.block_export_button')"
- />
- </div>
- </div>
-
- <div :label="$t('settings.blocks_tab')">
- <div class="profile-edit-usersearch-wrapper">
- <Autosuggest
- :filter="filterUnblockedUsers"
- :query="queryUserIds"
- :placeholder="$t('settings.search_user_to_block')"
- >
- <BlockCard
- slot-scope="row"
- :user-id="row.item"
- />
- </Autosuggest>
- </div>
- <BlockList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => blockUsers(selected)"
- >
- {{ $t('user_card.block') }}
- <template slot="progress">
- {{ $t('user_card.block_progress') }}
- </template>
- </ProgressButton>
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => unblockUsers(selected)"
- >
- {{ $t('user_card.unblock') }}
- <template slot="progress">
- {{ $t('user_card.unblock_progress') }}
- </template>
- </ProgressButton>
- </div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <BlockCard :user-id="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_blocks') }}
- </template>
- </BlockList>
- </div>
-
- <div :label="$t('settings.mutes_tab')">
- <tab-switcher>
- <div label="Users">
- <div class="profile-edit-usersearch-wrapper">
- <Autosuggest
- :filter="filterUnMutedUsers"
- :query="queryUserIds"
- :placeholder="$t('settings.search_user_to_mute')"
- >
- <MuteCard
- slot-scope="row"
- :user-id="row.item"
- />
- </Autosuggest>
- </div>
- <MuteList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => muteUsers(selected)"
- >
- {{ $t('user_card.mute') }}
- <template slot="progress">
- {{ $t('user_card.mute_progress') }}
- </template>
- </ProgressButton>
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => unmuteUsers(selected)"
- >
- {{ $t('user_card.unmute') }}
- <template slot="progress">
- {{ $t('user_card.unmute_progress') }}
- </template>
- </ProgressButton>
- </div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <MuteCard :user-id="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_mutes') }}
- </template>
- </MuteList>
- </div>
-
- <div :label="$t('settings.domain_mutes')">
- <div class="profile-edit-domain-mute-form">
- <input
- v-model="newDomainToMute"
- :placeholder="$t('settings.type_domains_to_mute')"
- type="text"
- @keyup.enter="muteDomain"
- >
- <ProgressButton
- class="btn btn-default"
- :click="muteDomain"
- >
- {{ $t('domain_mute_card.mute') }}
- <template slot="progress">
- {{ $t('domain_mute_card.mute_progress') }}
- </template>
- </ProgressButton>
- </div>
- <DomainMuteList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => unmuteDomains(selected)"
- >
- {{ $t('domain_mute_card.unmute') }}
- <template slot="progress">
- {{ $t('domain_mute_card.unmute_progress') }}
- </template>
- </ProgressButton>
- </div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <DomainMuteCard :domain="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_mutes') }}
- </template>
- </DomainMuteList>
- </div>
- </tab-switcher>
- </div>
- </tab-switcher>
</div>
</div>
</template>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 37d9591c..312f7283 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -278,6 +278,7 @@
"current_avatar": "Your current avatar",
"current_password": "Current password",
"current_profile_banner": "Your current profile banner",
+ "mutes_and_blocks": "Mutes and Blocks",
"data_import_export_tab": "Data Import / Export",
"default_vis": "Default visibility scope",
"delete_account": "Delete Account",
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 5b2762e5..e55b7290 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,6 +1,7 @@
import { set, delete as del } from 'vue'
const defaultState = {
+ settingsModalState: 'hidden',
settings: {
currentSaveStateNotice: null,
noticeClearTimeout: null,
@@ -35,6 +36,24 @@ const interfaceMod = {
},
setMobileLayout (state, value) {
state.mobileLayout = value
+ },
+ closeSettingsModal (state) {
+ state.settingsModalState = 'hidden'
+ },
+ togglePeekSettingsModal (state) {
+ switch (state.settingsModalState) {
+ case 'minimized':
+ state.settingsModalState = 'visible'
+ return
+ case 'visible':
+ state.settingsModalState = 'minimized'
+ return
+ default:
+ throw new Error('Illegal minimization state of settings modal')
+ }
+ },
+ openSettingsModal (state) {
+ state.settingsModalState = 'visible'
}
},
actions: {
@@ -49,6 +68,15 @@ const interfaceMod = {
},
setMobileLayout ({ commit }, value) {
commit('setMobileLayout', value)
+ },
+ closeSettingsModal ({ commit }) {
+ commit('closeSettingsModal')
+ },
+ openSettingsModal ({ commit }) {
+ commit('openSettingsModal')
+ },
+ togglePeekSettingsModal ({ commit }) {
+ commit('togglePeekSettingsModal')
}
}
}