From 44cd5ef8145b6799f7956401bb7cdd847e34c878 Mon Sep 17 00:00:00 2001
From: taehoon
Date: Fri, 15 Nov 2019 12:52:29 -0500
Subject: show badge visibility user setting checkbox only if needed
---
src/components/user_settings/user_settings.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'src/components/user_settings/user_settings.vue')
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 8c18cf49..3f1982a6 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -104,7 +104,7 @@
{{ $t('settings.hide_followers_count_description') }}
-
+
{{ $t('settings.show_admin_badge') }}
--
cgit v1.2.3-70-g09d2
From 7a013ac39392ef251c0789f27dd4660dcd30bd6d Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson
Date: Wed, 15 Jan 2020 20:22:54 +0000
Subject: Implement domain mutes v2
---
.../domain_mute_card/domain_mute_card.js | 15 ++
.../domain_mute_card/domain_mute_card.vue | 38 +++++
src/components/user_settings/user_settings.js | 19 ++-
src/components/user_settings/user_settings.vue | 157 +++++++++++++++------
src/i18n/en.json | 9 ++
src/modules/users.js | 44 ++++++
src/services/api/api.service.js | 28 +++-
7 files changed, 263 insertions(+), 47 deletions(-)
create mode 100644 src/components/domain_mute_card/domain_mute_card.js
create mode 100644 src/components/domain_mute_card/domain_mute_card.vue
(limited to 'src/components/user_settings/user_settings.vue')
diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js
new file mode 100644
index 00000000..c8e838ba
--- /dev/null
+++ b/src/components/domain_mute_card/domain_mute_card.js
@@ -0,0 +1,15 @@
+import ProgressButton from '../progress_button/progress_button.vue'
+
+const DomainMuteCard = {
+ props: ['domain'],
+ components: {
+ ProgressButton
+ },
+ methods: {
+ unmuteDomain () {
+ return this.$store.dispatch('unmuteDomain', this.domain)
+ }
+ }
+}
+
+export default DomainMuteCard
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
new file mode 100644
index 00000000..567d81c5
--- /dev/null
+++ b/src/components/domain_mute_card/domain_mute_card.vue
@@ -0,0 +1,38 @@
+
+
+
+ {{ domain }}
+
+
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
+
+
+
+
+
+
+
+
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index d5d671e4..1709b48f 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -9,6 +9,7 @@ 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'
@@ -32,6 +33,12 @@ const MuteList = withSubscription({
childPropName: 'items'
})(SelectableList)
+const DomainMuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+ childPropName: 'items'
+})(SelectableList)
+
const UserSettings = {
data () {
return {
@@ -67,7 +74,8 @@ const UserSettings = {
changedPassword: false,
changePasswordError: false,
activeTab: 'profile',
- notificationSettings: this.$store.state.users.currentUser.notification_settings
+ notificationSettings: this.$store.state.users.currentUser.notification_settings,
+ newDomainToMute: ''
}
},
created () {
@@ -80,10 +88,12 @@ const UserSettings = {
ImageCropper,
BlockList,
MuteList,
+ DomainMuteList,
EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
+ DomainMuteCard,
ProgressButton,
Importer,
Exporter,
@@ -365,6 +375,13 @@ const UserSettings = {
unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
+ unmuteDomains (domains) {
+ return this.$store.dispatch('unmuteDomains', domains)
+ },
+ muteDomain () {
+ return this.$store.dispatch('muteDomain', this.newDomainToMute)
+ .then(() => { this.newDomainToMute = '' })
+ },
identity (value) {
return value
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 3f1982a6..2222c293 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -509,59 +509,114 @@
-
-
-
-
-
+
+
+
+
+
+
+ {{ $t('user_card.mute') }}
+
+ {{ $t('user_card.mute_progress') }}
+
+
+
+ {{ $t('user_card.unmute') }}
+
+ {{ $t('user_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
+
+
+
- {{ $t('user_card.mute') }}
-
- {{ $t('user_card.mute_progress') }}
-
-
- {{ $t('user_card.unmute') }}
+ {{ $t('domain_mute_card.mute') }}
- {{ $t('user_card.unmute_progress') }}
+ {{ $t('domain_mute_card.mute_progress') }}
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
+
+
+
+
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
@@ -639,6 +694,18 @@
}
}
+ &-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/i18n/en.json b/src/i18n/en.json
index 75d66b9f..31f4ac24 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -21,6 +21,12 @@
"chat": {
"title": "Chat"
},
+ "domain_mute_card": {
+ "mute": "Mute",
+ "mute_progress": "Muting...",
+ "unmute": "Unmute",
+ "unmute_progress": "Unmuting..."
+ },
"exporter": {
"export": "Export",
"processing": "Processing, you'll soon be asked to download your file"
@@ -264,6 +270,7 @@
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"discoverable": "Allow discovery of this account in search results and other services",
+ "domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
"export_theme": "Save preset",
@@ -361,6 +368,7 @@
"post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
+ "user_mutes": "Users",
"useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text",
@@ -369,6 +377,7 @@
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
+ "type_domains_to_mute": "Type in domains to mute",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
diff --git a/src/modules/users.js b/src/modules/users.js
index b9ed0efa..ce3e595d 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
+const muteDomain = (store, domain) => {
+ return store.rootState.api.backendInteractor.muteDomain({ domain })
+ .then(() => store.commit('addDomainMute', domain))
+}
+
+const unmuteDomain = (store, domain) => {
+ return store.rootState.api.backendInteractor.unmuteDomain({ domain })
+ .then(() => store.commit('removeDomainMute', domain))
+}
+
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
@@ -177,6 +187,20 @@ export const mutations = {
state.currentUser.muteIds.push(muteId)
}
},
+ saveDomainMutes (state, domainMutes) {
+ state.currentUser.domainMutes = domainMutes
+ },
+ addDomainMute (state, domain) {
+ if (state.currentUser.domainMutes.indexOf(domain) === -1) {
+ state.currentUser.domainMutes.push(domain)
+ }
+ },
+ removeDomainMute (state, domain) {
+ const index = state.currentUser.domainMutes.indexOf(domain)
+ if (index !== -1) {
+ state.currentUser.domainMutes.splice(index, 1)
+ }
+ },
setPinnedToUser (state, status) {
const user = state.usersObject[status.user.id]
const index = user.pinnedStatusIds.indexOf(status.id)
@@ -297,6 +321,25 @@ const users = {
unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id)))
},
+ fetchDomainMutes (store) {
+ return store.rootState.api.backendInteractor.fetchDomainMutes()
+ .then((domainMutes) => {
+ store.commit('saveDomainMutes', domainMutes)
+ return domainMutes
+ })
+ },
+ muteDomain (store, domain) {
+ return muteDomain(store, domain)
+ },
+ unmuteDomain (store, domain) {
+ return unmuteDomain(store, domain)
+ },
+ muteDomains (store, domains = []) {
+ return Promise.all(domains.map(domain => muteDomain(store, domain)))
+ },
+ unmuteDomains (store, domain = []) {
+ return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
+ },
fetchFriends ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
@@ -460,6 +503,7 @@ const users = {
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
+ user.domainMutes = []
commit('setCurrentUser', user)
commit('addNewUsers', [user])
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ef0267aa..dcbedd8b 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -72,6 +72,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_STREAMING = '/api/v1/streaming'
const oldfetch = window.fetch
@@ -948,6 +949,28 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
})
}
+const fetchDomainMutes = ({ credentials }) => {
+ return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
+}
+
+const muteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'POST',
+ payload: { domain },
+ credentials
+ })
+}
+
+const unmuteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'DELETE',
+ payload: { domain },
+ credentials
+ })
+}
+
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({
...(credentials
@@ -1110,7 +1133,10 @@ const apiService = {
reportUser,
updateNotificationSettings,
search2,
- searchUsers
+ searchUsers,
+ fetchDomainMutes,
+ muteDomain,
+ unmuteDomain
}
export default apiService
--
cgit v1.2.3-70-g09d2
From 9bbf10b55d97f6dbe3197ebbd1bb29d294ff6b55 Mon Sep 17 00:00:00 2001
From: kPherox
Date: Tue, 4 Feb 2020 04:26:32 +0900
Subject: Add setting for allow_following_move
---
src/components/user_settings/user_settings.js | 2 ++
src/components/user_settings/user_settings.vue | 9 ++++++---
src/services/entity_normalizer/entity_normalizer.service.js | 2 ++
3 files changed, 10 insertions(+), 3 deletions(-)
(limited to 'src/components/user_settings/user_settings.vue')
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 38373056..eca6f9b1 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -55,6 +55,7 @@ const UserSettings = {
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,
@@ -162,6 +163,7 @@ const UserSettings = {
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
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 2222c293..8b2336b4 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -90,9 +90,7 @@
-
+
{{ $t('settings.hide_followers_description') }}
@@ -104,6 +102,11 @@
{{ $t('settings.hide_followers_count_description') }}
+
+
+ {{ $t('settings.allow_following_move') }}
+
+
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index a3d0b782..3bc46886 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -81,6 +81,8 @@ export const parseUser = (data) => {
output.subscribed = relationship.subscribing
}
+ output.allow_following_move = data.pleroma.allow_following_move
+
output.hide_follows = data.pleroma.hide_follows
output.hide_followers = data.pleroma.hide_followers
output.hide_follows_count = data.pleroma.hide_follows_count
--
cgit v1.2.3-70-g09d2
From 2e35289c3376881ca17b9330113c816a3327f245 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Sun, 3 May 2020 17:36:12 +0300
Subject: initial work on settings modal
---
src/App.js | 5 +
src/App.scss | 3 +
src/App.vue | 7 +-
.../mobile_post_status_button.js | 2 +-
.../post_status_modal/post_status_modal.js | 3 -
.../post_status_modal/post_status_modal.vue | 1 -
src/components/settings_modal/settings_modal.js | 39 ++
src/components/settings_modal/settings_modal.scss | 59 ++
src/components/settings_modal/settings_modal.vue | 31 ++
.../settings_modal/tabs/data_import_export.js | 65 +++
.../settings_modal/tabs/data_import_export.vue | 43 ++
.../settings_modal/tabs/mutes_and_blocks.js | 124 +++++
.../settings_modal/tabs/mutes_and_blocks.vue | 173 ++++++
.../settings_modal/tabs/notifications.js | 27 +
.../settings_modal/tabs/notifications.vue | 42 ++
src/components/settings_modal/tabs/profile.js | 179 ++++++
src/components/settings_modal/tabs/profile.scss | 82 +++
src/components/settings_modal/tabs/profile.vue | 213 ++++++++
src/components/settings_modal/tabs/security.js | 106 ++++
src/components/settings_modal/tabs/security.vue | 143 +++++
src/components/tab_switcher/tab_switcher.js | 7 +-
src/components/tab_switcher/tab_switcher.scss | 220 +++++---
src/components/user_settings/user_settings.js | 257 +--------
src/components/user_settings/user_settings.vue | 597 ---------------------
src/i18n/en.json | 1 +
src/modules/interface.js | 28 +
26 files changed, 1530 insertions(+), 927 deletions(-)
create mode 100644 src/components/settings_modal/settings_modal.js
create mode 100644 src/components/settings_modal/settings_modal.scss
create mode 100644 src/components/settings_modal/settings_modal.vue
create mode 100644 src/components/settings_modal/tabs/data_import_export.js
create mode 100644 src/components/settings_modal/tabs/data_import_export.vue
create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.js
create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.vue
create mode 100644 src/components/settings_modal/tabs/notifications.js
create mode 100644 src/components/settings_modal/tabs/notifications.vue
create mode 100644 src/components/settings_modal/tabs/profile.js
create mode 100644 src/components/settings_modal/tabs/profile.scss
create mode 100644 src/components/settings_modal/tabs/profile.vue
create mode 100644 src/components/settings_modal/tabs/security.js
create mode 100644 src/components/settings_modal/tabs/security.vue
(limited to 'src/components/user_settings/user_settings.vue')
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
/>
-
-
+
+
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 @@
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 @@
+
+
+
+
+ {{ $t('settings.settings') }}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
{{ $t('settings.follow_import') }}
+
{{ $t('settings.import_followers_from_a_csv_file') }}
+
+
+
+
{{ $t('settings.follow_export') }}
+
+
+
+
{{ $t('settings.block_import') }}
+
{{ $t('settings.import_blocks_from_a_csv_file') }}
+
+
+
+
{{ $t('settings.block_export') }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ $t('user_card.block') }}
+
+ {{ $t('user_card.block_progress') }}
+
+
+
+ {{ $t('user_card.unblock') }}
+
+ {{ $t('user_card.unblock_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_blocks') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('user_card.mute') }}
+
+ {{ $t('user_card.mute_progress') }}
+
+
+
+ {{ $t('user_card.unmute') }}
+
+ {{ $t('user_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
+
+
+
+
+ {{ $t('domain_mute_card.mute') }}
+
+ {{ $t('domain_mute_card.mute_progress') }}
+
+
+
+
+
+
+
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
{{ $t('settings.notification_setting') }}
+
+ -
+
+ {{ $t('settings.notification_setting_follows') }}
+
+
+ -
+
+ {{ $t('settings.notification_setting_followers') }}
+
+
+ -
+
+ {{ $t('settings.notification_setting_non_follows') }}
+
+
+ -
+
+ {{ $t('settings.notification_setting_non_followers') }}
+
+
+
+
+
{{ $t('settings.notification_mutes') }}
+
{{ $t('settings.notification_blocks') }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
{{ $t('settings.name_bio') }}
+
{{ $t('settings.name') }}
+
+
+
+
{{ $t('settings.bio') }}
+
+
+
+
+
+ {{ $t('settings.lock_account_description') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_rich_text_description') }}
+
+
+
+
+ {{ $t('settings.hide_follows_description') }}
+
+
+
+
+ {{ $t('settings.hide_follows_count_description') }}
+
+
+
+
+ {{ $t('settings.hide_followers_description') }}
+
+
+
+
+ {{ $t('settings.hide_followers_count_description') }}
+
+
+
+
+ {{ $t('settings.allow_following_move') }}
+
+
+
+
+
+ {{ $t('settings.show_admin_badge') }}
+
+
+ {{ $t('settings.show_moderator_badge') }}
+
+
+
+
+
+ {{ $t('settings.discoverable') }}
+
+
+
+
+
+
{{ $t('settings.avatar') }}
+
+ {{ $t('settings.avatar_size_instruction') }}
+
+
{{ $t('settings.current_avatar') }}
+
![]()
+
{{ $t('settings.set_new_avatar') }}
+
+
+
+
+
{{ $t('settings.profile_banner') }}
+
{{ $t('settings.current_profile_banner') }}
+
![]()
+
{{ $t('settings.set_new_profile_banner') }}
+
![]()
+
+
+
+
+
+
+ Error: {{ bannerUploadError }}
+
+
+
+
+
{{ $t('settings.profile_background') }}
+
{{ $t('settings.set_new_profile_background') }}
+
![]()
+
+
+
+
+
+
+ Error: {{ backgroundUploadError }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
{{ $t('settings.change_email') }}
+
+
{{ $t('settings.new_email') }}
+
+
+
+
{{ $t('settings.current_password') }}
+
+
+
+
+ {{ $t('settings.changed_email') }}
+
+
+ {{ $t('settings.change_email_error') }}
+ {{ changeEmailError }}
+
+
+
+
+
{{ $t('settings.change_password') }}
+
+
{{ $t('settings.current_password') }}
+
+
+
+
{{ $t('settings.new_password') }}
+
+
+
+
{{ $t('settings.confirm_new_password') }}
+
+
+
+
+ {{ $t('settings.changed_password') }}
+
+
+ {{ $t('settings.change_password_error') }}
+
+
+ {{ changePasswordError }}
+
+
+
+
+
{{ $t('settings.oauth_tokens') }}
+
+
+
+ | {{ $t('settings.app_name') }} |
+ {{ $t('settings.valid_until') }} |
+ |
+
+
+
+
+ | {{ oauthToken.appName }} |
+ {{ oauthToken.validUntil }} |
+
+
+ |
+
+
+
+
+
+
+
{{ $t('settings.delete_account') }}
+
+ {{ $t('settings.delete_account_description') }}
+
+
+
{{ $t('settings.delete_account_instructions') }}
+
{{ $t('login.password') }}
+
+
+
+
+ {{ $t('settings.delete_account_error') }}
+
+
+ {{ deleteAccountError }}
+
+
+
+
+
+
+
+
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 (
-
+
{tabs}
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 @@
-
-
-
-
{{ $t('settings.name_bio') }}
-
{{ $t('settings.name') }}
-
-
-
-
{{ $t('settings.bio') }}
-
-
-
-
-
- {{ $t('settings.lock_account_description') }}
-
-
-
-
-
-
-
-
-
-
- {{ $t('settings.no_rich_text_description') }}
-
-
-
-
- {{ $t('settings.hide_follows_description') }}
-
-
-
-
- {{ $t('settings.hide_follows_count_description') }}
-
-
-
-
- {{ $t('settings.hide_followers_description') }}
-
-
-
-
- {{ $t('settings.hide_followers_count_description') }}
-
-
-
-
- {{ $t('settings.allow_following_move') }}
-
-
-
-
-
- {{ $t('settings.show_admin_badge') }}
-
-
- {{ $t('settings.show_moderator_badge') }}
-
-
-
-
-
- {{ $t('settings.discoverable') }}
-
-
-
-
-
-
{{ $t('settings.avatar') }}
-
- {{ $t('settings.avatar_size_instruction') }}
-
-
{{ $t('settings.current_avatar') }}
-
![]()
-
{{ $t('settings.set_new_avatar') }}
-
-
-
-
-
{{ $t('settings.profile_banner') }}
-
{{ $t('settings.current_profile_banner') }}
-
![]()
-
{{ $t('settings.set_new_profile_banner') }}
-
![]()
-
-
-
-
-
-
- Error: {{ bannerUploadError }}
-
-
-
-
-
{{ $t('settings.profile_background') }}
-
{{ $t('settings.set_new_profile_background') }}
-
![]()
-
-
-
-
-
-
- Error: {{ backgroundUploadError }}
-
-
-
-
-
-
-
-
{{ $t('settings.change_email') }}
-
-
{{ $t('settings.new_email') }}
-
-
-
-
{{ $t('settings.current_password') }}
-
-
-
-
- {{ $t('settings.changed_email') }}
-
-
- {{ $t('settings.change_email_error') }}
- {{ changeEmailError }}
-
-
-
-
-
{{ $t('settings.change_password') }}
-
-
{{ $t('settings.current_password') }}
-
-
-
-
{{ $t('settings.new_password') }}
-
-
-
-
{{ $t('settings.confirm_new_password') }}
-
-
-
-
- {{ $t('settings.changed_password') }}
-
-
- {{ $t('settings.change_password_error') }}
-
-
- {{ changePasswordError }}
-
-
-
-
-
{{ $t('settings.oauth_tokens') }}
-
-
-
- | {{ $t('settings.app_name') }} |
- {{ $t('settings.valid_until') }} |
- |
-
-
-
-
- | {{ oauthToken.appName }} |
- {{ oauthToken.validUntil }} |
-
-
- |
-
-
-
-
-
-
-
{{ $t('settings.delete_account') }}
-
- {{ $t('settings.delete_account_description') }}
-
-
-
{{ $t('settings.delete_account_instructions') }}
-
{{ $t('login.password') }}
-
-
-
-
- {{ $t('settings.delete_account_error') }}
-
-
- {{ deleteAccountError }}
-
-
-
-
-
-
-
-
-
{{ $t('settings.notification_setting') }}
-
- -
-
- {{ $t('settings.notification_setting_follows') }}
-
-
- -
-
- {{ $t('settings.notification_setting_followers') }}
-
-
- -
-
- {{ $t('settings.notification_setting_non_follows') }}
-
-
- -
-
- {{ $t('settings.notification_setting_non_followers') }}
-
-
-
-
-
{{ $t('settings.notification_mutes') }}
-
{{ $t('settings.notification_blocks') }}
-
-
-
-
-
-
-
{{ $t('settings.follow_import') }}
-
{{ $t('settings.import_followers_from_a_csv_file') }}
-
-
-
-
{{ $t('settings.follow_export') }}
-
-
-
-
{{ $t('settings.block_import') }}
-
{{ $t('settings.import_blocks_from_a_csv_file') }}
-
-
-
-
{{ $t('settings.block_export') }}
-
-
-
-
-
-
-
-
-
-
- {{ $t('user_card.block') }}
-
- {{ $t('user_card.block_progress') }}
-
-
-
- {{ $t('user_card.unblock') }}
-
- {{ $t('user_card.unblock_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_blocks') }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('user_card.mute') }}
-
- {{ $t('user_card.mute_progress') }}
-
-
-
- {{ $t('user_card.unmute') }}
-
- {{ $t('user_card.unmute_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
-
-
-
-
-
-
- {{ $t('domain_mute_card.mute') }}
-
- {{ $t('domain_mute_card.mute_progress') }}
-
-
-
-
-
-
-
- {{ $t('domain_mute_card.unmute') }}
-
- {{ $t('domain_mute_card.unmute_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
-
-
-
-
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')
}
}
}
--
cgit v1.2.3-70-g09d2
From bcebec478e43b3851e85c94335940e8fc7546cc8 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Sun, 10 May 2020 06:46:06 +0300
Subject: moved stuff from settings, cleaned up naming for tabs, added close
and peek
---
src/App.scss | 48 --
src/components/modal/modal.vue | 16 +-
src/components/settings/settings.js | 128 ---
src/components/settings/settings.vue | 424 ---------
src/components/settings_modal/settings_modal.js | 42 +-
src/components/settings_modal/settings_modal.scss | 30 +-
src/components/settings_modal/settings_modal.vue | 26 +-
.../settings_modal/tabs/data_import_export.js | 65 --
.../settings_modal/tabs/data_import_export.vue | 43 -
.../settings_modal/tabs/data_import_export_tab.js | 65 ++
.../settings_modal/tabs/data_import_export_tab.vue | 43 +
.../settings_modal/tabs/filtering_tab.js | 26 +
.../settings_modal/tabs/filtering_tab.vue | 86 ++
src/components/settings_modal/tabs/general_tab.js | 32 +
src/components/settings_modal/tabs/general_tab.vue | 272 ++++++
.../tabs/helpers/shared_computed_object.js | 69 ++
.../settings_modal/tabs/mutes_and_blocks.js | 124 ---
.../settings_modal/tabs/mutes_and_blocks.vue | 173 ----
.../settings_modal/tabs/mutes_and_blocks_tab.js | 124 +++
.../settings_modal/tabs/mutes_and_blocks_tab.vue | 173 ++++
.../settings_modal/tabs/notifications.js | 27 -
.../settings_modal/tabs/notifications.vue | 42 -
.../settings_modal/tabs/notifications_tab.js | 27 +
.../settings_modal/tabs/notifications_tab.vue | 42 +
src/components/settings_modal/tabs/profile.js | 179 ----
src/components/settings_modal/tabs/profile.scss | 82 --
src/components/settings_modal/tabs/profile.vue | 213 -----
src/components/settings_modal/tabs/profile_tab.js | 179 ++++
.../settings_modal/tabs/profile_tab.scss | 82 ++
src/components/settings_modal/tabs/profile_tab.vue | 213 +++++
src/components/settings_modal/tabs/security.js | 106 ---
src/components/settings_modal/tabs/security.vue | 143 ---
.../settings_modal/tabs/security_tab/confirm.js | 9 +
.../settings_modal/tabs/security_tab/confirm.vue | 22 +
.../settings_modal/tabs/security_tab/mfa.js | 155 ++++
.../settings_modal/tabs/security_tab/mfa.vue | 174 ++++
.../tabs/security_tab/mfa_backup_codes.js | 17 +
.../tabs/security_tab/mfa_backup_codes.vue | 35 +
.../settings_modal/tabs/security_tab/mfa_totp.js | 49 ++
.../settings_modal/tabs/security_tab/mfa_totp.vue | 43 +
.../tabs/security_tab/security_tab.js | 106 +++
.../tabs/security_tab/security_tab.vue | 143 +++
.../settings_modal/tabs/theme_tab/preview.vue | 117 +++
.../settings_modal/tabs/theme_tab/theme_tab.js | 759 ++++++++++++++++
.../settings_modal/tabs/theme_tab/theme_tab.scss | 339 ++++++++
.../settings_modal/tabs/theme_tab/theme_tab.vue | 956 +++++++++++++++++++++
src/components/settings_modal/tabs/version_tab.js | 24 +
src/components/settings_modal/tabs/version_tab.vue | 31 +
src/components/style_switcher/preview.vue | 117 ---
src/components/style_switcher/style_switcher.js | 758 ----------------
src/components/style_switcher/style_switcher.scss | 335 --------
src/components/style_switcher/style_switcher.vue | 956 ---------------------
src/components/tab_switcher/tab_switcher.scss | 6 +
src/components/user_card/user_card.vue | 11 +-
src/components/user_settings/confirm.js | 9 -
src/components/user_settings/confirm.vue | 22 -
src/components/user_settings/mfa.js | 155 ----
src/components/user_settings/mfa.vue | 173 ----
src/components/user_settings/mfa_backup_codes.js | 17 -
src/components/user_settings/mfa_backup_codes.vue | 33 -
src/components/user_settings/mfa_totp.js | 49 --
src/components/user_settings/mfa_totp.vue | 43 -
src/components/user_settings/user_settings.js | 140 ---
src/components/user_settings/user_settings.vue | 119 ---
src/i18n/en.json | 4 +-
65 files changed, 4509 insertions(+), 4761 deletions(-)
delete mode 100644 src/components/settings/settings.js
delete mode 100644 src/components/settings/settings.vue
delete mode 100644 src/components/settings_modal/tabs/data_import_export.js
delete mode 100644 src/components/settings_modal/tabs/data_import_export.vue
create mode 100644 src/components/settings_modal/tabs/data_import_export_tab.js
create mode 100644 src/components/settings_modal/tabs/data_import_export_tab.vue
create mode 100644 src/components/settings_modal/tabs/filtering_tab.js
create mode 100644 src/components/settings_modal/tabs/filtering_tab.vue
create mode 100644 src/components/settings_modal/tabs/general_tab.js
create mode 100644 src/components/settings_modal/tabs/general_tab.vue
create mode 100644 src/components/settings_modal/tabs/helpers/shared_computed_object.js
delete mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.js
delete mode 100644 src/components/settings_modal/tabs/mutes_and_blocks.vue
create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks_tab.js
create mode 100644 src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
delete mode 100644 src/components/settings_modal/tabs/notifications.js
delete mode 100644 src/components/settings_modal/tabs/notifications.vue
create mode 100644 src/components/settings_modal/tabs/notifications_tab.js
create mode 100644 src/components/settings_modal/tabs/notifications_tab.vue
delete mode 100644 src/components/settings_modal/tabs/profile.js
delete mode 100644 src/components/settings_modal/tabs/profile.scss
delete mode 100644 src/components/settings_modal/tabs/profile.vue
create mode 100644 src/components/settings_modal/tabs/profile_tab.js
create mode 100644 src/components/settings_modal/tabs/profile_tab.scss
create mode 100644 src/components/settings_modal/tabs/profile_tab.vue
delete mode 100644 src/components/settings_modal/tabs/security.js
delete mode 100644 src/components/settings_modal/tabs/security.vue
create mode 100644 src/components/settings_modal/tabs/security_tab/confirm.js
create mode 100644 src/components/settings_modal/tabs/security_tab/confirm.vue
create mode 100644 src/components/settings_modal/tabs/security_tab/mfa.js
create mode 100644 src/components/settings_modal/tabs/security_tab/mfa.vue
create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_totp.js
create mode 100644 src/components/settings_modal/tabs/security_tab/mfa_totp.vue
create mode 100644 src/components/settings_modal/tabs/security_tab/security_tab.js
create mode 100644 src/components/settings_modal/tabs/security_tab/security_tab.vue
create mode 100644 src/components/settings_modal/tabs/theme_tab/preview.vue
create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.js
create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.scss
create mode 100644 src/components/settings_modal/tabs/theme_tab/theme_tab.vue
create mode 100644 src/components/settings_modal/tabs/version_tab.js
create mode 100644 src/components/settings_modal/tabs/version_tab.vue
delete mode 100644 src/components/style_switcher/preview.vue
delete mode 100644 src/components/style_switcher/style_switcher.js
delete mode 100644 src/components/style_switcher/style_switcher.scss
delete mode 100644 src/components/style_switcher/style_switcher.vue
delete mode 100644 src/components/user_settings/confirm.js
delete mode 100644 src/components/user_settings/confirm.vue
delete mode 100644 src/components/user_settings/mfa.js
delete mode 100644 src/components/user_settings/mfa.vue
delete mode 100644 src/components/user_settings/mfa_backup_codes.js
delete mode 100644 src/components/user_settings/mfa_backup_codes.vue
delete mode 100644 src/components/user_settings/mfa_totp.js
delete mode 100644 src/components/user_settings/mfa_totp.vue
delete mode 100644 src/components/user_settings/user_settings.js
delete mode 100644 src/components/user_settings/user_settings.vue
(limited to 'src/components/user_settings/user_settings.vue')
diff --git a/src/App.scss b/src/App.scss
index 7db9461c..120eea53 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -860,54 +860,6 @@ nav {
}
}
-// DELETE
-.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;
- }
-}
-// DELETE
-
.select-multiple {
display: flex;
.option-list {
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index cee24241..e5ecc0c0 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -3,6 +3,7 @@
v-show="isOpen"
v-body-scroll-lock="isOpen"
class="modal-view"
+ :class="{ 'modal-background': !noBackground }"
@click.self="$emit('backdropClicked')"
>
@@ -15,6 +16,10 @@ export default {
isOpen: {
type: Boolean,
default: true
+ },
+ noBackground: {
+ type: Boolean,
+ default: false
}
}
}
@@ -32,10 +37,19 @@ export default {
justify-content: center;
align-items: center;
overflow: auto;
+ pointer-events: none;
animation-duration: 0.2s;
- background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein;
+ > * {
+ pointer-events: initial;
+ }
+
+ &.modal-background {
+ pointer-events: initial;
+ background-color: rgba(0, 0, 0, 0.5);
+ }
+
body:not(.scroll-locked) & {
opacity: 0;
}
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
deleted file mode 100644
index 31a9e9be..00000000
--- a/src/components/settings/settings.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/* eslint-env browser */
-import { filter, trim } from 'lodash'
-
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import StyleSwitcher from '../style_switcher/style_switcher.vue'
-import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
-import { extractCommit } from '../../services/version/version.service'
-import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
-import Checkbox from '../checkbox/checkbox.vue'
-
-const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
-const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
-
-const multiChoiceProperties = [
- 'postContentType',
- 'subjectLineBehavior'
-]
-
-const settings = {
- data () {
- const instance = this.$store.state.instance
-
- return {
- loopSilentAvailable:
- // Firefox
- Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
- // Chrome-likes
- Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
- // Future spec, still not supported in Nightly 63 as of 08/2018
- Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
-
- backendVersion: instance.backendVersion,
- frontendVersion: instance.frontendVersion
- }
- },
- components: {
- TabSwitcher,
- StyleSwitcher,
- InterfaceLanguageSwitcher,
- Checkbox
- },
- computed: {
- user () {
- return this.$store.state.users.currentUser
- },
- currentSaveStateNotice () {
- return this.$store.state.interface.settings.currentSaveStateNotice
- },
- postFormats () {
- return this.$store.state.instance.postFormats || []
- },
- instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
- frontendVersionLink () {
- return pleromaFeCommitUrl + this.frontendVersion
- },
- backendVersionLink () {
- return pleromaBeCommitUrl + extractCommit(this.backendVersion)
- },
- // Getting localized values for instance-default properties
- ...instanceDefaultProperties
- .filter(key => multiChoiceProperties.includes(key))
- .map(key => [
- key + 'DefaultValue',
- function () {
- return this.$store.getters.instanceDefaultConfig[key]
- }
- ])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- ...instanceDefaultProperties
- .filter(key => !multiChoiceProperties.includes(key))
- .map(key => [
- key + 'LocalizedValue',
- function () {
- return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
- }
- ])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Generating computed values for vuex properties
- ...Object.keys(configDefaultState)
- .map(key => [key, {
- get () { return this.$store.getters.mergedConfig[key] },
- set (value) {
- this.$store.dispatch('setOption', { name: key, value })
- }
- }])
- .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
- // Special cases (need to transform values or perform actions first)
- muteWordsString: {
- get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
- set (value) {
- this.$store.dispatch('setOption', {
- name: 'muteWords',
- value: filter(value.split('\n'), (word) => trim(word).length > 0)
- })
- }
- },
- useStreamingApi: {
- get () { return this.$store.getters.mergedConfig.useStreamingApi },
- set (value) {
- const promise = value
- ? this.$store.dispatch('enableMastoSockets')
- : this.$store.dispatch('disableMastoSockets')
-
- promise.then(() => {
- this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
- }).catch((e) => {
- console.error('Failed starting MastoAPI Streaming socket', e)
- this.$store.dispatch('disableMastoSockets')
- this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
- })
- }
- }
- },
- // Updating nested properties
- watch: {
- notificationVisibility: {
- handler (value) {
- this.$store.dispatch('setOption', {
- name: 'notificationVisibility',
- value: this.$store.getters.mergedConfig.notificationVisibility
- })
- },
- deep: true
- }
- }
-}
-
-export default settings
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
deleted file mode 100644
index 9e14b449..00000000
--- a/src/components/settings/settings.vue
+++ /dev/null
@@ -1,424 +0,0 @@
-
-
-
-
- {{ $t('settings.settings') }}
-
-
-
-
-
- {{ $t('settings.saving_err') }}
-
-
-
- {{ $t('settings.saving_ok') }}
-
-
-
-
-
-
-
-
-
-
{{ $t('settings.interface') }}
-
- -
-
-
- -
-
- {{ $t('settings.hide_isp') }}
-
-
-
-
-
-
{{ $t('nav.timeline') }}
-
- -
-
- {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
-
-
- -
-
- {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
-
-
- -
-
- {{ $t('settings.streaming') }}
-
-
- -
-
- {{ $t('settings.pause_on_unfocused') }}
-
-
-
-
- -
-
- {{ $t('settings.useStreamingApi') }}
-
-
- {{ $t('settings.useStreamingApiWarning') }}
-
-
-
- -
-
- {{ $t('settings.autoload') }}
-
-
- -
-
- {{ $t('settings.reply_link_preview') }}
-
-
- -
-
- {{ $t('settings.emoji_reactions_on_timeline') }}
-
-
-
-
-
-
-
{{ $t('settings.composing') }}
-
- -
-
- {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
-
-
- -
-
- {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
-
-
- -
-
- {{ $t('settings.subject_line_behavior') }}
-
-
-
- -
-
- {{ $t('settings.post_status_content_type') }}
-
-
-
- -
-
- {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
-
-
- -
-
- {{ $t('settings.autohide_floating_post_button') }}
-
-
- -
-
- {{ $t('settings.pad_emoji') }}
-
-
-
-
-
-
-
{{ $t('settings.attachments') }}
-
-
-
-
-
{{ $t('settings.notifications') }}
-
- -
-
- {{ $t('settings.enable_web_push_notifications') }}
-
-
-
-
-
-
-
{{ $t('settings.fun') }}
-
- -
-
- {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
-
-
-
-
-
-
-
-
-
-
-
-
{{ $t('settings.notification_visibility') }}
-
- -
-
- {{ $t('settings.notification_visibility_likes') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_repeats') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_follows') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_mentions') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_moves') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_emoji_reactions') }}
-
-
-
-
-
- {{ $t('settings.replies_in_timeline') }}
-
-
-
-
- {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
-
-
-
-
- {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
-
-
-
-
-
-
{{ $t('settings.filtering_explanation') }}
-
-
-
-
- {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 1f4c038f..d60babf6 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -1,21 +1,30 @@
-import Modal from '../modal/modal.vue'
-import TabSwitcher from '../tab_switcher/tab_switcher.js'
+import Modal from 'src/components/modal/modal.vue'
+import TabSwitcher from 'src/components/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'
+import DataImportExportTab from './tabs/data_import_export_tab.vue'
+import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
+import NotificationsTab from './tabs/notifications_tab.vue'
+import FilteringTab from './tabs/filtering_tab.vue'
+import SecurityTab from './tabs/security_tab/security_tab.vue'
+import ProfileTab from './tabs/profile_tab.vue'
+import GeneralTab from './tabs/general_tab.vue'
+import VersionTab from './tabs/version_tab.vue'
+import ThemeTab from './tabs/theme_tab/theme_tab.vue'
const SettingsModal = {
components: {
Modal,
TabSwitcher,
- Profile,
- Security,
- Notifications,
- DataImportExport,
- MutesAndBlocks
+
+ DataImportExportTab,
+ MutesAndBlocksTab,
+ NotificationsTab,
+ FilteringTab,
+ SecurityTab,
+ ProfileTab,
+ GeneralTab,
+ VersionTab,
+ ThemeTab
},
data () {
return {
@@ -28,11 +37,20 @@ const SettingsModal = {
},
modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden'
+ },
+ modalPeeked () {
+ return this.$store.state.interface.settingsModalState === 'minimized'
}
},
watch: {
},
methods: {
+ closeModal () {
+ this.$store.dispatch('closeSettingsModal')
+ },
+ peekModal () {
+ this.$store.dispatch('togglePeekSettingsModal')
+ }
}
}
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 8cea52d2..3efbe205 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -1,14 +1,29 @@
-@import '../../_variables.scss';
+@import 'src/_variables.scss';
.settings-modal {
+
.settings_tab-switcher {
height: 100%;
}
+ &.peek {
+ .settings-modal-panel {
+ transform: translateY(calc(100% - 50px));
+ }
+ }
+
.settings-modal-panel {
+ transition: transform;
+ transition-timing-function: ease-in-out;
+ transition-duration: 300ms;
width: 1000px;
max-width: 90vw;
height: 90vh;
+ @media all and (max-width: 800px) {
+ max-width: 100vw;
+ height: 100vh;
+ }
}
.panel-body {
+ height: 100%;
overflow-y: hidden;
}
.setting-item {
@@ -16,6 +31,12 @@
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
+ .btn {
+ min-height: 28px;
+ min-width: 10em;
+ padding: 0 2em;
+ }
+
> div {
margin-bottom: .5em;
&:last-child {
@@ -33,7 +54,6 @@
min-width: 10em;
}
-
textarea {
width: 100%;
max-width: 100%;
@@ -46,12 +66,6 @@
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
index 9e35d3f6..53481bdd 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -3,10 +3,20 @@
v-if="isLoggedIn && !resettingForm"
:is-open="modalActivated"
class="settings-modal"
+ :class="{ peek: modalPeeked }"
+ :no-background="modalPeeked"
>
- {{ $t('settings.settings') }}
+
+ {{ $t('settings.settings') }}
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/data_import_export.js b/src/components/settings_modal/tabs/data_import_export.js
deleted file mode 100644
index f68d12e9..00000000
--- a/src/components/settings_modal/tabs/data_import_export.js
+++ /dev/null
@@ -1,65 +0,0 @@
-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
deleted file mode 100644
index 464df6d3..00000000
--- a/src/components/settings_modal/tabs/data_import_export.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
{{ $t('settings.follow_import') }}
-
{{ $t('settings.import_followers_from_a_csv_file') }}
-
-
-
-
{{ $t('settings.follow_export') }}
-
-
-
-
{{ $t('settings.block_import') }}
-
{{ $t('settings.import_blocks_from_a_csv_file') }}
-
-
-
-
{{ $t('settings.block_export') }}
-
-
-
-
-
-
-
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js
new file mode 100644
index 00000000..168f89e1
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export_tab.js
@@ -0,0 +1,65 @@
+import Importer from 'src/components/importer/importer.vue'
+import Exporter from 'src/components/exporter/exporter.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const DataImportExportTab = {
+ data () {
+ return {
+ activeTab: 'profile',
+ newDomainToMute: ''
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchTokens')
+ },
+ components: {
+ Importer,
+ Exporter,
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ }
+ },
+ methods: {
+ getFollowsContent () {
+ return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
+ .then(this.generateExportableUsersContent)
+ },
+ getBlocksContent () {
+ return this.$store.state.api.backendInteractor.fetchBlocks()
+ .then(this.generateExportableUsersContent)
+ },
+ importFollows (file) {
+ return this.$store.state.api.backendInteractor.importFollows({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ importBlocks (file) {
+ return this.$store.state.api.backendInteractor.importBlocks({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ generateExportableUsersContent (users) {
+ // Get addresses
+ return users.map((user) => {
+ // check is it's a local user
+ if (user && user.is_local) {
+ // append the instance address
+ // eslint-disable-next-line no-undef
+ return user.screen_name + '@' + location.hostname
+ }
+ return user.screen_name
+ }).join('\n')
+ }
+ }
+}
+
+export default DataImportExportTab
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue
new file mode 100644
index 00000000..3ddc8b03
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export_tab.vue
@@ -0,0 +1,43 @@
+
+
+
+
{{ $t('settings.follow_import') }}
+
{{ $t('settings.import_followers_from_a_csv_file') }}
+
+
+
+
{{ $t('settings.follow_export') }}
+
+
+
+
{{ $t('settings.block_import') }}
+
{{ $t('settings.import_blocks_from_a_csv_file') }}
+
+
+
+
{{ $t('settings.block_export') }}
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
new file mode 100644
index 00000000..ec330667
--- /dev/null
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -0,0 +1,26 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+import SharedComputedObject from './helpers/shared_computed_object.js'
+
+const FilteringTab = {
+ components: {
+ Checkbox
+ },
+ computed: {
+ ...SharedComputedObject()
+ },
+ // Updating nested properties
+ watch: {
+ notificationVisibility: {
+ handler (value) {
+ this.$store.dispatch('setOption', {
+ name: 'notificationVisibility',
+ value: this.$store.getters.mergedConfig.notificationVisibility
+ })
+ },
+ deep: true
+ }
+ }
+}
+
+export default FilteringTab
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
new file mode 100644
index 00000000..647ec7b4
--- /dev/null
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
{{ $t('settings.notification_visibility') }}
+
+ -
+
+ {{ $t('settings.notification_visibility_likes') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_repeats') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_follows') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_mentions') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_moves') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_emoji_reactions') }}
+
+
+
+
+
+ {{ $t('settings.replies_in_timeline') }}
+
+
+
+
+ {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
+
+
+
+
+ {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
+
+
+
+
+
+
{{ $t('settings.filtering_explanation') }}
+
+
+
+
+ {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
new file mode 100644
index 00000000..82bf6862
--- /dev/null
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -0,0 +1,32 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
+
+import SharedComputedObject from './helpers/shared_computed_object.js'
+
+const GeneralTab = {
+ data () {
+ const instance = this.$store.state.instance
+ return {
+ loopSilentAvailable:
+ // Firefox
+ Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
+ // Chrome-likes
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
+ // Future spec, still not supported in Nightly 63 as of 08/2018
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
+ }
+ },
+ components: {
+ Checkbox,
+ InterfaceLanguageSwitcher
+ },
+ computed: {
+ postFormats () {
+ return this.$store.state.instance.postFormats || []
+ },
+ instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
+ ...SharedComputedObject()
+ }
+}
+
+export default GeneralTab
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
new file mode 100644
index 00000000..0d2da07a
--- /dev/null
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -0,0 +1,272 @@
+
+
+
+
{{ $t('settings.interface') }}
+
+ -
+
+
+ -
+
+ {{ $t('settings.hide_isp') }}
+
+
+
+
+
+
{{ $t('nav.timeline') }}
+
+ -
+
+ {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
+
+
+ -
+
+ {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
+
+
+ -
+
+ {{ $t('settings.streaming') }}
+
+
+ -
+
+ {{ $t('settings.pause_on_unfocused') }}
+
+
+
+
+ -
+
+ {{ $t('settings.useStreamingApi') }}
+
+
+ {{ $t('settings.useStreamingApiWarning') }}
+
+
+
+ -
+
+ {{ $t('settings.autoload') }}
+
+
+ -
+
+ {{ $t('settings.reply_link_preview') }}
+
+
+ -
+
+ {{ $t('settings.emoji_reactions_on_timeline') }}
+
+
+
+
+
+
+
{{ $t('settings.composing') }}
+
+ -
+
+ {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
+
+
+ -
+
+ {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
+
+
+ -
+
+ {{ $t('settings.subject_line_behavior') }}
+
+
+
+ -
+
+ {{ $t('settings.post_status_content_type') }}
+
+
+
+ -
+
+ {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
+
+
+ -
+
+ {{ $t('settings.autohide_floating_post_button') }}
+
+
+ -
+
+ {{ $t('settings.pad_emoji') }}
+
+
+
+
+
+
+
{{ $t('settings.attachments') }}
+
+
+
+
+
{{ $t('settings.notifications') }}
+
+ -
+
+ {{ $t('settings.enable_web_push_notifications') }}
+
+
+
+
+
+
+
{{ $t('settings.fun') }}
+
+ -
+
+ {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/helpers/shared_computed_object.js b/src/components/settings_modal/tabs/helpers/shared_computed_object.js
new file mode 100644
index 00000000..61643e3b
--- /dev/null
+++ b/src/components/settings_modal/tabs/helpers/shared_computed_object.js
@@ -0,0 +1,69 @@
+import { filter, trim } from 'lodash'
+import { instanceDefaultProperties, defaultState as configDefaultState } from 'src/modules/config.js'
+
+const multiChoiceProperties = [
+ 'postContentType',
+ 'subjectLineBehavior'
+]
+
+const SharedComputedObject = () => ({
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ // Getting localized values for instance-default properties
+ ...instanceDefaultProperties
+ .filter(key => multiChoiceProperties.includes(key))
+ .map(key => [
+ key + 'DefaultValue',
+ function () {
+ return this.$store.getters.instanceDefaultConfig[key]
+ }
+ ])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+ ...instanceDefaultProperties
+ .filter(key => !multiChoiceProperties.includes(key))
+ .map(key => [
+ key + 'LocalizedValue',
+ function () {
+ return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
+ }
+ ])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+ // Generating computed values for vuex properties
+ ...Object.keys(configDefaultState)
+ .map(key => [key, {
+ get () { return this.$store.getters.mergedConfig[key] },
+ set (value) {
+ this.$store.dispatch('setOption', { name: key, value })
+ }
+ }])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+ // Special cases (need to transform values or perform actions first)
+ muteWordsString: {
+ get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
+ set (value) {
+ this.$store.dispatch('setOption', {
+ name: 'muteWords',
+ value: filter(value.split('\n'), (word) => trim(word).length > 0)
+ })
+ }
+ },
+ useStreamingApi: {
+ get () { return this.$store.getters.mergedConfig.useStreamingApi },
+ set (value) {
+ const promise = value
+ ? this.$store.dispatch('enableMastoSockets')
+ : this.$store.dispatch('disableMastoSockets')
+
+ promise.then(() => {
+ this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
+ }).catch((e) => {
+ console.error('Failed starting MastoAPI Streaming socket', e)
+ this.$store.dispatch('disableMastoSockets')
+ this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
+ })
+ }
+ }
+})
+
+export default SharedComputedObject
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.js b/src/components/settings_modal/tabs/mutes_and_blocks.js
deleted file mode 100644
index 51895ddc..00000000
--- a/src/components/settings_modal/tabs/mutes_and_blocks.js
+++ /dev/null
@@ -1,124 +0,0 @@
-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
deleted file mode 100644
index 3aff47a0..00000000
--- a/src/components/settings_modal/tabs/mutes_and_blocks.vue
+++ /dev/null
@@ -1,173 +0,0 @@
-
-
-
-
-
-
-
-
- {{ $t('user_card.block') }}
-
- {{ $t('user_card.block_progress') }}
-
-
-
- {{ $t('user_card.unblock') }}
-
- {{ $t('user_card.unblock_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_blocks') }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $t('user_card.mute') }}
-
- {{ $t('user_card.mute_progress') }}
-
-
-
- {{ $t('user_card.unmute') }}
-
- {{ $t('user_card.unmute_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
-
-
-
-
-
-
- {{ $t('domain_mute_card.mute') }}
-
- {{ $t('domain_mute_card.mute_progress') }}
-
-
-
-
-
-
-
- {{ $t('domain_mute_card.unmute') }}
-
- {{ $t('domain_mute_card.unmute_progress') }}
-
-
-
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
new file mode 100644
index 00000000..3f6b7205
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -0,0 +1,124 @@
+import get from 'lodash/get'
+import map from 'lodash/map'
+import reject from 'lodash/reject'
+import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import BlockCard from 'src/components/block_card/block_card.vue'
+import MuteCard from 'src/components/mute_card/mute_card.vue'
+import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
+import SelectableList from 'src/components/selectable_list/selectable_list.vue'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const BlockList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchBlocks'),
+ select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+ childPropName: 'items'
+})(SelectableList)
+
+const MuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+ childPropName: 'items'
+})(SelectableList)
+
+const DomainMuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+ childPropName: 'items'
+})(SelectableList)
+
+const MutesAndBlocks = {
+ data () {
+ return {
+ activeTab: 'profile',
+ newDomainToMute: ''
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchTokens')
+ },
+ components: {
+ TabSwitcher,
+ BlockList,
+ MuteList,
+ DomainMuteList,
+ BlockCard,
+ MuteCard,
+ DomainMuteCard,
+ ProgressButton,
+ Autosuggest,
+ Checkbox
+ },
+ methods: {
+ importFollows (file) {
+ return this.$store.state.api.backendInteractor.importFollows({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ importBlocks (file) {
+ return this.$store.state.api.backendInteractor.importBlocks({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ generateExportableUsersContent (users) {
+ // Get addresses
+ return users.map((user) => {
+ // check is it's a local user
+ if (user && user.is_local) {
+ // append the instance address
+ // eslint-disable-next-line no-undef
+ return user.screen_name + '@' + location.hostname
+ }
+ return user.screen_name
+ }).join('\n')
+ },
+ activateTab (tabName) {
+ this.activeTab = tabName
+ },
+ filterUnblockedUsers (userIds) {
+ return reject(userIds, (userId) => {
+ const 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_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
new file mode 100644
index 00000000..7fce7b78
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+ {{ $t('user_card.block') }}
+
+ {{ $t('user_card.block_progress') }}
+
+
+
+ {{ $t('user_card.unblock') }}
+
+ {{ $t('user_card.unblock_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_blocks') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('user_card.mute') }}
+
+ {{ $t('user_card.mute_progress') }}
+
+
+
+ {{ $t('user_card.unmute') }}
+
+ {{ $t('user_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
+
+
+
+
+ {{ $t('domain_mute_card.mute') }}
+
+ {{ $t('domain_mute_card.mute_progress') }}
+
+
+
+
+
+
+
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/notifications.js b/src/components/settings_modal/tabs/notifications.js
deleted file mode 100644
index 0a870b3f..00000000
--- a/src/components/settings_modal/tabs/notifications.js
+++ /dev/null
@@ -1,27 +0,0 @@
-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
deleted file mode 100644
index f9a7c17b..00000000
--- a/src/components/settings_modal/tabs/notifications.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
{{ $t('settings.notification_setting') }}
-
- -
-
- {{ $t('settings.notification_setting_follows') }}
-
-
- -
-
- {{ $t('settings.notification_setting_followers') }}
-
-
- -
-
- {{ $t('settings.notification_setting_non_follows') }}
-
-
- -
-
- {{ $t('settings.notification_setting_non_followers') }}
-
-
-
-
-
{{ $t('settings.notification_mutes') }}
-
{{ $t('settings.notification_blocks') }}
-
-
-
-
-
-
-
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
new file mode 100644
index 00000000..3e44c95d
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -0,0 +1,27 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const NotificationsTab = {
+ data () {
+ return {
+ activeTab: 'profile',
+ notificationSettings: this.$store.state.users.currentUser.notification_settings,
+ newDomainToMute: ''
+ }
+ },
+ components: {
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ }
+ },
+ methods: {
+ updateNotificationSettings () {
+ this.$store.state.api.backendInteractor
+ .updateNotificationSettings({ settings: this.notificationSettings })
+ }
+ }
+}
+
+export default NotificationsTab
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
new file mode 100644
index 00000000..ab33a6a5
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
{{ $t('settings.notification_setting') }}
+
+ -
+
+ {{ $t('settings.notification_setting_follows') }}
+
+
+ -
+
+ {{ $t('settings.notification_setting_followers') }}
+
+
+ -
+
+ {{ $t('settings.notification_setting_non_follows') }}
+
+
+ -
+
+ {{ $t('settings.notification_setting_non_followers') }}
+
+
+
+
+
{{ $t('settings.notification_mutes') }}
+
{{ $t('settings.notification_blocks') }}
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/profile.js b/src/components/settings_modal/tabs/profile.js
deleted file mode 100644
index 18c44024..00000000
--- a/src/components/settings_modal/tabs/profile.js
+++ /dev/null
@@ -1,179 +0,0 @@
-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
deleted file mode 100644
index 4aab81eb..00000000
--- a/src/components/settings_modal/tabs/profile.scss
+++ /dev/null
@@ -1,82 +0,0 @@
-@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
deleted file mode 100644
index 335fc12e..00000000
--- a/src/components/settings_modal/tabs/profile.vue
+++ /dev/null
@@ -1,213 +0,0 @@
-
-
-
-
{{ $t('settings.name_bio') }}
-
{{ $t('settings.name') }}
-
-
-
-
{{ $t('settings.bio') }}
-
-
-
-
-
- {{ $t('settings.lock_account_description') }}
-
-
-
-
-
-
-
-
-
-
- {{ $t('settings.no_rich_text_description') }}
-
-
-
-
- {{ $t('settings.hide_follows_description') }}
-
-
-
-
- {{ $t('settings.hide_follows_count_description') }}
-
-
-
-
- {{ $t('settings.hide_followers_description') }}
-
-
-
-
- {{ $t('settings.hide_followers_count_description') }}
-
-
-
-
- {{ $t('settings.allow_following_move') }}
-
-
-
-
-
- {{ $t('settings.show_admin_badge') }}
-
-
- {{ $t('settings.show_moderator_badge') }}
-
-
-
-
-
- {{ $t('settings.discoverable') }}
-
-
-
-
-
-
{{ $t('settings.avatar') }}
-
- {{ $t('settings.avatar_size_instruction') }}
-
-
{{ $t('settings.current_avatar') }}
-
![]()
-
{{ $t('settings.set_new_avatar') }}
-
-
-
-
-
{{ $t('settings.profile_banner') }}
-
{{ $t('settings.current_profile_banner') }}
-
![]()
-
{{ $t('settings.set_new_profile_banner') }}
-
![]()
-
-
-
-
-
-
- Error: {{ bannerUploadError }}
-
-
-
-
-
{{ $t('settings.profile_background') }}
-
{{ $t('settings.set_new_profile_background') }}
-
![]()
-
-
-
-
-
-
- Error: {{ backgroundUploadError }}
-
-
-
-
-
-
-
-
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
new file mode 100644
index 00000000..949b480b
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -0,0 +1,179 @@
+import unescape from 'lodash/unescape'
+import ImageCropper from 'src/components/image_cropper/image_cropper.vue'
+import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
+import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
+import suggestor from 'src/components/emoji_input/suggestor.js'
+import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+const ProfileTab = {
+ data () {
+ return {
+ newName: this.$store.state.users.currentUser.name,
+ newBio: unescape(this.$store.state.users.currentUser.description),
+ newLocked: this.$store.state.users.currentUser.locked,
+ newNoRichText: this.$store.state.users.currentUser.no_rich_text,
+ newDefaultScope: this.$store.state.users.currentUser.default_scope,
+ hideFollows: this.$store.state.users.currentUser.hide_follows,
+ hideFollowers: this.$store.state.users.currentUser.hide_followers,
+ hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
+ hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
+ showRole: this.$store.state.users.currentUser.show_role,
+ role: this.$store.state.users.currentUser.role,
+ discoverable: this.$store.state.users.currentUser.discoverable,
+ allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
+ pickAvatarBtnVisible: true,
+ bannerUploading: false,
+ backgroundUploading: false,
+ banner: null,
+ bannerPreview: null,
+ background: null,
+ backgroundPreview: null,
+ bannerUploadError: null,
+ backgroundUploadError: null,
+ }
+ },
+ components: {
+ ScopeSelector,
+ ImageCropper,
+ EmojiInput,
+ Autosuggest,
+ ProgressButton,
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ emojiUserSuggestor () {
+ return suggestor({
+ emoji: [
+ ...this.$store.state.instance.emoji,
+ ...this.$store.state.instance.customEmoji
+ ],
+ users: this.$store.state.users.users,
+ updateUsersList: (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_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
new file mode 100644
index 00000000..4aab81eb
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.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_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
new file mode 100644
index 00000000..9dd89b99
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -0,0 +1,213 @@
+
+
+
+
{{ $t('settings.name_bio') }}
+
{{ $t('settings.name') }}
+
+
+
+
{{ $t('settings.bio') }}
+
+
+
+
+
+ {{ $t('settings.lock_account_description') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_rich_text_description') }}
+
+
+
+
+ {{ $t('settings.hide_follows_description') }}
+
+
+
+
+ {{ $t('settings.hide_follows_count_description') }}
+
+
+
+
+ {{ $t('settings.hide_followers_description') }}
+
+
+
+
+ {{ $t('settings.hide_followers_count_description') }}
+
+
+
+
+ {{ $t('settings.allow_following_move') }}
+
+
+
+
+
+ {{ $t('settings.show_admin_badge') }}
+
+
+ {{ $t('settings.show_moderator_badge') }}
+
+
+
+
+
+ {{ $t('settings.discoverable') }}
+
+
+
+
+
+
{{ $t('settings.avatar') }}
+
+ {{ $t('settings.avatar_size_instruction') }}
+
+
{{ $t('settings.current_avatar') }}
+
![]()
+
{{ $t('settings.set_new_avatar') }}
+
+
+
+
+
{{ $t('settings.profile_banner') }}
+
{{ $t('settings.current_profile_banner') }}
+
![]()
+
{{ $t('settings.set_new_profile_banner') }}
+
![]()
+
+
+
+
+
+
+ Error: {{ bannerUploadError }}
+
+
+
+
+
{{ $t('settings.profile_background') }}
+
{{ $t('settings.set_new_profile_background') }}
+
![]()
+
+
+
+
+
+
+ Error: {{ backgroundUploadError }}
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/security.js b/src/components/settings_modal/tabs/security.js
deleted file mode 100644
index cc791b7a..00000000
--- a/src/components/settings_modal/tabs/security.js
+++ /dev/null
@@ -1,106 +0,0 @@
-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
deleted file mode 100644
index 603c9a04..00000000
--- a/src/components/settings_modal/tabs/security.vue
+++ /dev/null
@@ -1,143 +0,0 @@
-
-
-
-
{{ $t('settings.change_email') }}
-
-
{{ $t('settings.new_email') }}
-
-
-
-
{{ $t('settings.current_password') }}
-
-
-
-
- {{ $t('settings.changed_email') }}
-
-
- {{ $t('settings.change_email_error') }}
- {{ changeEmailError }}
-
-
-
-
-
{{ $t('settings.change_password') }}
-
-
{{ $t('settings.current_password') }}
-
-
-
-
{{ $t('settings.new_password') }}
-
-
-
-
{{ $t('settings.confirm_new_password') }}
-
-
-
-
- {{ $t('settings.changed_password') }}
-
-
- {{ $t('settings.change_password_error') }}
-
-
- {{ changePasswordError }}
-
-
-
-
-
{{ $t('settings.oauth_tokens') }}
-
-
-
- | {{ $t('settings.app_name') }} |
- {{ $t('settings.valid_until') }} |
- |
-
-
-
-
- | {{ oauthToken.appName }} |
- {{ oauthToken.validUntil }} |
-
-
- |
-
-
-
-
-
-
-
{{ $t('settings.delete_account') }}
-
- {{ $t('settings.delete_account_description') }}
-
-
-
{{ $t('settings.delete_account_instructions') }}
-
{{ $t('login.password') }}
-
-
-
-
- {{ $t('settings.delete_account_error') }}
-
-
- {{ deleteAccountError }}
-
-
-
-
-
-
-
-
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.js b/src/components/settings_modal/tabs/security_tab/confirm.js
new file mode 100644
index 00000000..0f4ddfc9
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/confirm.js
@@ -0,0 +1,9 @@
+const Confirm = {
+ props: ['disabled'],
+ data: () => ({}),
+ methods: {
+ confirm () { this.$emit('confirm') },
+ cancel () { this.$emit('cancel') }
+ }
+}
+export default Confirm
diff --git a/src/components/settings_modal/tabs/security_tab/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue
new file mode 100644
index 00000000..69b3811b
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/confirm.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.js b/src/components/settings_modal/tabs/security_tab/mfa.js
new file mode 100644
index 00000000..abf37062
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa.js
@@ -0,0 +1,155 @@
+import RecoveryCodes from './mfa_backup_codes.vue'
+import TOTP from './mfa_totp.vue'
+import Confirm from './confirm.vue'
+import VueQrcode from '@chenfengyuan/vue-qrcode'
+import { mapState } from 'vuex'
+
+const Mfa = {
+ data: () => ({
+ settings: { // current settings of MFA
+ available: false,
+ enabled: false,
+ totp: false
+ },
+ setupState: { // setup mfa
+ state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
+ setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
+ },
+ backupCodes: {
+ getNewCodes: false,
+ inProgress: false, // progress of fetch codes
+ codes: []
+ },
+ otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
+ provisioning_uri: '',
+ key: ''
+ },
+ currentPassword: null,
+ otpConfirmToken: null,
+ error: null,
+ readyInit: false
+ }),
+ components: {
+ 'recovery-codes': RecoveryCodes,
+ 'totp-item': TOTP,
+ 'qrcode': VueQrcode,
+ 'confirm': Confirm
+ },
+ computed: {
+ canSetupOTP () {
+ return (
+ (this.setupInProgress && this.backupCodesPrepared) ||
+ this.settings.enabled
+ ) && !this.settings.totp && !this.setupOTPInProgress
+ },
+ setupInProgress () {
+ return this.setupState.state !== '' && this.setupState.state !== 'complete'
+ },
+ setupOTPInProgress () {
+ return this.setupState.state === 'setupOTP' && !this.completedOTP
+ },
+ prepareOTP () {
+ return this.setupState.setupOTPState === 'prepare'
+ },
+ confirmOTP () {
+ return this.setupState.setupOTPState === 'confirm'
+ },
+ completedOTP () {
+ return this.setupState.setupOTPState === 'completed'
+ },
+ backupCodesPrepared () {
+ return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
+ },
+ confirmNewBackupCodes () {
+ return this.backupCodes.getNewCodes
+ },
+ ...mapState({
+ backendInteractor: (state) => state.api.backendInteractor
+ })
+ },
+
+ methods: {
+ activateOTP () {
+ if (!this.settings.enabled) {
+ this.setupState.state = 'getBackupcodes'
+ this.fetchBackupCodes()
+ }
+ },
+ fetchBackupCodes () {
+ this.backupCodes.inProgress = true
+ this.backupCodes.codes = []
+
+ return this.backendInteractor.generateMfaBackupCodes()
+ .then((res) => {
+ this.backupCodes.codes = res.codes
+ this.backupCodes.inProgress = false
+ })
+ },
+ getBackupCodes () { // get a new backup codes
+ this.backupCodes.getNewCodes = true
+ },
+ confirmBackupCodes () { // confirm getting new backup codes
+ this.fetchBackupCodes().then((res) => {
+ this.backupCodes.getNewCodes = false
+ })
+ },
+ cancelBackupCodes () { // cancel confirm form of new backup codes
+ this.backupCodes.getNewCodes = false
+ },
+
+ // Setup OTP
+ setupOTP () { // prepare setup OTP
+ this.setupState.state = 'setupOTP'
+ this.setupState.setupOTPState = 'prepare'
+ this.backendInteractor.mfaSetupOTP()
+ .then((res) => {
+ this.otpSettings = res
+ this.setupState.setupOTPState = 'confirm'
+ })
+ },
+ doConfirmOTP () { // handler confirm enable OTP
+ this.error = null
+ this.backendInteractor.mfaConfirmOTP({
+ token: this.otpConfirmToken,
+ password: this.currentPassword
+ })
+ .then((res) => {
+ if (res.error) {
+ this.error = res.error
+ return
+ }
+ this.completeSetup()
+ })
+ },
+
+ completeSetup () {
+ this.setupState.setupOTPState = 'complete'
+ this.setupState.state = 'complete'
+ this.currentPassword = null
+ this.error = null
+ this.fetchSettings()
+ },
+ cancelSetup () { // cancel setup
+ this.setupState.setupOTPState = ''
+ this.setupState.state = ''
+ this.currentPassword = null
+ this.error = null
+ },
+ // end Setup OTP
+
+ // fetch settings from server
+ async fetchSettings () {
+ let result = await this.backendInteractor.settingsMFA()
+ if (result.error) return
+ this.settings = result.settings
+ this.settings.available = true
+ return result
+ }
+ },
+ mounted () {
+ this.fetchSettings().then(() => {
+ this.readyInit = true
+ })
+ }
+}
+export default Mfa
diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue
new file mode 100644
index 00000000..25c4d1dc
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa.vue
@@ -0,0 +1,174 @@
+
+
+
+
{{ $t('settings.mfa.title') }}
+
+
+
+
+
+
{{ $t('settings.mfa.authentication_methods') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.mfa.warning_of_generate_new_codes') }}
+
+
+
+
+
+
+
+
+
+
{{ $t('settings.mfa.setup_otp') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.mfa.wait_pre_setup_otp') }}
+
+
+
+
+
{{ $t('settings.mfa.scan.title') }}
+
{{ $t('settings.mfa.scan.desc') }}
+
+
+ {{ $t('settings.mfa.scan.secret_code') }}:
+ {{ otpSettings.key }}
+
+
+
+
+
{{ $t('general.verify') }}
+
{{ $t('settings.mfa.verify.desc') }}
+
+
+
{{ $t('settings.enter_current_password_to_confirm') }}:
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
new file mode 100644
index 00000000..f0a984ec
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.js
@@ -0,0 +1,17 @@
+export default {
+ props: {
+ backupCodes: {
+ type: Object,
+ default: () => ({
+ inProgress: false,
+ codes: []
+ })
+ }
+ },
+ data: () => ({}),
+ computed: {
+ inProgress () { return this.backupCodes.inProgress },
+ ready () { return this.backupCodes.codes.length > 0 },
+ displayTitle () { return this.inProgress || this.ready }
+ }
+}
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
new file mode 100644
index 00000000..d7e98b3c
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue
@@ -0,0 +1,35 @@
+
+
+
+ {{ $t('settings.mfa.recovery_codes') }}
+
+
{{ $t('settings.mfa.waiting_a_recovery_codes') }}
+
+
+ {{ $t('settings.mfa.recovery_codes_warning') }}
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.js b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
new file mode 100644
index 00000000..8408d8e9
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.js
@@ -0,0 +1,49 @@
+import Confirm from './confirm.vue'
+import { mapState } from 'vuex'
+
+export default {
+ props: ['settings'],
+ data: () => ({
+ error: false,
+ currentPassword: '',
+ deactivate: false,
+ inProgress: false // progress peform request to disable otp method
+ }),
+ components: {
+ 'confirm': Confirm
+ },
+ computed: {
+ isActivated () {
+ return this.settings.totp
+ },
+ ...mapState({
+ backendInteractor: (state) => state.api.backendInteractor
+ })
+ },
+ methods: {
+ doActivate () {
+ this.$emit('activate')
+ },
+ cancelDeactivate () { this.deactivate = false },
+ doDeactivate () {
+ this.error = null
+ this.deactivate = true
+ },
+ confirmDeactivate () { // confirm deactivate TOTP method
+ this.error = null
+ this.inProgress = true
+ this.backendInteractor.mfaDisableOTP({
+ password: this.currentPassword
+ })
+ .then((res) => {
+ this.inProgress = false
+ if (res.error) {
+ this.error = res.error
+ return
+ }
+ this.deactivate = false
+ this.$emit('deactivate')
+ })
+ }
+ }
+}
diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
new file mode 100644
index 00000000..c6f2cc7b
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue
@@ -0,0 +1,43 @@
+
+
+
+ {{ $t('settings.mfa.otp') }}
+
+
+
+
+
+
+ {{ $t('settings.enter_current_password_to_confirm') }}:
+
+
+
+ {{ error }}
+
+
+
+
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js
new file mode 100644
index 00000000..811161a5
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.js
@@ -0,0 +1,106 @@
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import Mfa from './mfa.vue'
+
+const SecurityTab = {
+ data () {
+ return {
+ newEmail: '',
+ changeEmailError: false,
+ changeEmailPassword: '',
+ changedEmail: false,
+ deletingAccount: false,
+ deleteAccountConfirmPasswordInput: '',
+ deleteAccountError: false,
+ changePasswordInputs: [ '', '', '' ],
+ changedPassword: false,
+ changePasswordError: false
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchTokens')
+ },
+ components: {
+ ProgressButton,
+ Mfa,
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ pleromaBackend () {
+ return this.$store.state.instance.pleromaBackend
+ },
+ oauthTokens () {
+ return this.$store.state.oauthTokens.tokens.map(oauthToken => {
+ return {
+ id: oauthToken.id,
+ appName: oauthToken.app_name,
+ validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
+ }
+ })
+ }
+ },
+ methods: {
+ confirmDelete () {
+ this.deletingAccount = true
+ },
+ deleteAccount () {
+ this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
+ .then((res) => {
+ if (res.status === 'success') {
+ this.$store.dispatch('logout')
+ this.$router.push({ name: 'root' })
+ } else {
+ this.deleteAccountError = res.error
+ }
+ })
+ },
+ changePassword () {
+ const params = {
+ password: this.changePasswordInputs[0],
+ newPassword: this.changePasswordInputs[1],
+ newPasswordConfirmation: this.changePasswordInputs[2]
+ }
+ this.$store.state.api.backendInteractor.changePassword(params)
+ .then((res) => {
+ if (res.status === 'success') {
+ this.changedPassword = true
+ this.changePasswordError = false
+ this.logout()
+ } else {
+ this.changedPassword = false
+ this.changePasswordError = res.error
+ }
+ })
+ },
+ changeEmail () {
+ const params = {
+ email: this.newEmail,
+ password: this.changeEmailPassword
+ }
+ this.$store.state.api.backendInteractor.changeEmail(params)
+ .then((res) => {
+ if (res.status === 'success') {
+ this.changedEmail = true
+ this.changeEmailError = false
+ } else {
+ this.changedEmail = false
+ this.changeEmailError = res.error
+ }
+ })
+ },
+ logout () {
+ this.$store.dispatch('logout')
+ this.$router.replace('/')
+ },
+ revokeToken (id) {
+ if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
+ this.$store.dispatch('revokeToken', id)
+ }
+ }
+ }
+}
+
+export default SecurityTab
diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue
new file mode 100644
index 00000000..45bacec1
--- /dev/null
+++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue
@@ -0,0 +1,143 @@
+
+
+
+
{{ $t('settings.change_email') }}
+
+
{{ $t('settings.new_email') }}
+
+
+
+
{{ $t('settings.current_password') }}
+
+
+
+
+ {{ $t('settings.changed_email') }}
+
+
+ {{ $t('settings.change_email_error') }}
+ {{ changeEmailError }}
+
+
+
+
+
{{ $t('settings.change_password') }}
+
+
{{ $t('settings.current_password') }}
+
+
+
+
{{ $t('settings.new_password') }}
+
+
+
+
{{ $t('settings.confirm_new_password') }}
+
+
+
+
+ {{ $t('settings.changed_password') }}
+
+
+ {{ $t('settings.change_password_error') }}
+
+
+ {{ changePasswordError }}
+
+
+
+
+
{{ $t('settings.oauth_tokens') }}
+
+
+
+ | {{ $t('settings.app_name') }} |
+ {{ $t('settings.valid_until') }} |
+ |
+
+
+
+
+ | {{ oauthToken.appName }} |
+ {{ oauthToken.validUntil }} |
+
+
+ |
+
+
+
+
+
+
+
{{ $t('settings.delete_account') }}
+
+ {{ $t('settings.delete_account_description') }}
+
+
+
{{ $t('settings.delete_account_instructions') }}
+
{{ $t('login.password') }}
+
+
+
+
+ {{ $t('settings.delete_account_error') }}
+
+
+ {{ deleteAccountError }}
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue
new file mode 100644
index 00000000..9d984659
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/preview.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ {{ $t('settings.style.preview.header') }}
+
+ 99
+
+
+
+ {{ $t('settings.style.preview.header_faint') }}
+
+
+ {{ $t('settings.style.preview.error') }}
+
+
+
+
+
+
+ ( ͡° ͜ʖ ͡°)
+
+
+
+ {{ $t('settings.style.preview.content') }}
+
+
+
+
+ {{ $t('settings.style.preview.mono') }}
+
+
+ {{ $t('settings.style.preview.link') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.style.preview.error') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
new file mode 100644
index 00000000..9d61b0c4
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -0,0 +1,759 @@
+import { set, delete as del } from 'vue'
+import {
+ rgb2hex,
+ hex2rgb,
+ getContrastRatioLayers
+} from 'src/services/color_convert/color_convert.js'
+import {
+ DEFAULT_SHADOWS,
+ generateColors,
+ generateShadows,
+ generateRadii,
+ generateFonts,
+ composePreset,
+ getThemes,
+ shadows2to3,
+ colors2to3
+} from 'src/services/style_setter/style_setter.js'
+import {
+ SLOT_INHERITANCE
+} from 'src/services/theme_data/pleromafe.js'
+import {
+ CURRENT_VERSION,
+ OPACITIES,
+ getLayers,
+ getOpacitySlot
+} from 'src/services/theme_data/theme_data.service.js'
+import ColorInput from 'src/components/color_input/color_input.vue'
+import RangeInput from 'src/components/range_input/range_input.vue'
+import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
+import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
+import FontControl from 'src/components/font_control/font_control.vue'
+import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
+import ExportImport from 'src/components/export_import/export_import.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+
+import Preview from './preview.vue'
+
+// List of color values used in v1
+const v1OnlyNames = [
+ 'bg',
+ 'fg',
+ 'text',
+ 'link',
+ 'cRed',
+ 'cGreen',
+ 'cBlue',
+ 'cOrange'
+].map(_ => _ + 'ColorLocal')
+
+const colorConvert = (color) => {
+ if (color.startsWith('--') || color === 'transparent') {
+ return color
+ } else {
+ return hex2rgb(color)
+ }
+}
+
+export default {
+ data () {
+ return {
+ availableStyles: [],
+ selected: this.$store.getters.mergedConfig.theme,
+ themeWarning: undefined,
+ tempImportFile: undefined,
+ engineVersion: 0,
+
+ previewShadows: {},
+ previewColors: {},
+ previewRadii: {},
+ previewFonts: {},
+
+ shadowsInvalid: true,
+ colorsInvalid: true,
+ radiiInvalid: true,
+
+ keepColor: false,
+ keepShadows: false,
+ keepOpacity: false,
+ keepRoundness: false,
+ keepFonts: false,
+
+ ...Object.keys(SLOT_INHERITANCE)
+ .map(key => [key, ''])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
+
+ ...Object.keys(OPACITIES)
+ .map(key => [key, ''])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
+
+ shadowSelected: undefined,
+ shadowsLocal: {},
+ fontsLocal: {},
+
+ btnRadiusLocal: '',
+ inputRadiusLocal: '',
+ checkboxRadiusLocal: '',
+ panelRadiusLocal: '',
+ avatarRadiusLocal: '',
+ avatarAltRadiusLocal: '',
+ attachmentRadiusLocal: '',
+ tooltipRadiusLocal: ''
+ }
+ },
+ created () {
+ const self = this
+
+ getThemes()
+ .then((promises) => {
+ return Promise.all(
+ Object.entries(promises)
+ .map(([k, v]) => v.then(res => [k, res]))
+ )
+ })
+ .then(themes => themes.reduce((acc, [k, v]) => {
+ if (v) {
+ return {
+ ...acc,
+ [k]: v
+ }
+ } else {
+ return acc
+ }
+ }, {}))
+ .then((themesComplete) => {
+ self.availableStyles = themesComplete
+ })
+ },
+ mounted () {
+ this.loadThemeFromLocalStorage()
+ if (typeof this.shadowSelected === 'undefined') {
+ this.shadowSelected = this.shadowsAvailable[0]
+ }
+ },
+ computed: {
+ themeWarningHelp () {
+ if (!this.themeWarning) return
+ const t = this.$t
+ const pre = 'settings.style.switcher.help.'
+ const {
+ origin,
+ themeEngineVersion,
+ type,
+ noActionsPossible
+ } = this.themeWarning
+ if (origin === 'file') {
+ // Loaded v2 theme from file
+ if (themeEngineVersion === 2 && type === 'wrong_version') {
+ return t(pre + 'v2_imported')
+ }
+ if (themeEngineVersion > CURRENT_VERSION) {
+ return t(pre + 'future_version_imported') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present')
+ )
+ }
+ if (themeEngineVersion < CURRENT_VERSION) {
+ return t(pre + 'future_version_imported') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present')
+ )
+ }
+ } else if (origin === 'localStorage') {
+ if (type === 'snapshot_source_mismatch') {
+ return t(pre + 'snapshot_source_mismatch')
+ }
+ // FE upgraded from v2
+ if (themeEngineVersion === 2) {
+ return t(pre + 'upgraded_from_v2')
+ }
+ // Admin downgraded FE
+ if (themeEngineVersion > CURRENT_VERSION) {
+ return t(pre + 'fe_downgraded') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone')
+ )
+ }
+ // Admin upgraded FE
+ if (themeEngineVersion < CURRENT_VERSION) {
+ return t(pre + 'fe_upgraded') + ' ' +
+ (
+ noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone')
+ )
+ }
+ }
+ },
+ selectedVersion () {
+ return Array.isArray(this.selected) ? 1 : 2
+ },
+ currentColors () {
+ return Object.keys(SLOT_INHERITANCE)
+ .map(key => [key, this[key + 'ColorLocal']])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
+ },
+ currentOpacity () {
+ return Object.keys(OPACITIES)
+ .map(key => [key, this[key + 'OpacityLocal']])
+ .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
+ },
+ currentRadii () {
+ return {
+ btn: this.btnRadiusLocal,
+ input: this.inputRadiusLocal,
+ checkbox: this.checkboxRadiusLocal,
+ panel: this.panelRadiusLocal,
+ avatar: this.avatarRadiusLocal,
+ avatarAlt: this.avatarAltRadiusLocal,
+ tooltip: this.tooltipRadiusLocal,
+ attachment: this.attachmentRadiusLocal
+ }
+ },
+ preview () {
+ return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
+ },
+ previewTheme () {
+ if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
+ return this.preview.theme
+ },
+ // This needs optimization maybe
+ previewContrast () {
+ try {
+ if (!this.previewTheme.colors.bg) return {}
+ const colors = this.previewTheme.colors
+ const opacity = this.previewTheme.opacity
+ if (!colors.bg) return {}
+ const hints = (ratio) => ({
+ text: ratio.toPrecision(3) + ':1',
+ // AA level, AAA level
+ aa: ratio >= 4.5,
+ aaa: ratio >= 7,
+ // same but for 18pt+ texts
+ laa: ratio >= 3,
+ laaa: ratio >= 4.5
+ })
+ const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
+
+ const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
+ const slotIsBaseText = key === 'text' || key === 'link'
+ const slotIsText = slotIsBaseText || (
+ typeof value === 'object' && value !== null && value.textColor
+ )
+ if (!slotIsText) return acc
+ const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
+ const background = variant || layer
+ const opacitySlot = getOpacitySlot(background)
+ const textColors = [
+ key,
+ ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
+ ]
+
+ const layers = getLayers(
+ layer,
+ variant || layer,
+ opacitySlot,
+ colorsConverted,
+ opacity
+ )
+
+ return {
+ ...acc,
+ ...textColors.reduce((acc, textColorKey) => {
+ const newKey = slotIsBaseText
+ ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
+ : textColorKey
+ return {
+ ...acc,
+ [newKey]: getContrastRatioLayers(
+ colorsConverted[textColorKey],
+ layers,
+ colorsConverted[textColorKey]
+ )
+ }
+ }, {})
+ }
+ }, {})
+
+ return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
+ } catch (e) {
+ console.warn('Failure computing contrasts', e)
+ }
+ },
+ previewRules () {
+ if (!this.preview.rules) return ''
+ return [
+ ...Object.values(this.preview.rules),
+ 'color: var(--text)',
+ 'font-family: var(--interfaceFont, sans-serif)'
+ ].join(';')
+ },
+ shadowsAvailable () {
+ return Object.keys(DEFAULT_SHADOWS).sort()
+ },
+ currentShadowOverriden: {
+ get () {
+ return !!this.currentShadow
+ },
+ set (val) {
+ if (val) {
+ set(this.shadowsLocal, this.shadowSelected, this.currentShadowFallback.map(_ => Object.assign({}, _)))
+ } else {
+ del(this.shadowsLocal, this.shadowSelected)
+ }
+ }
+ },
+ currentShadowFallback () {
+ return (this.previewTheme.shadows || {})[this.shadowSelected]
+ },
+ currentShadow: {
+ get () {
+ return this.shadowsLocal[this.shadowSelected]
+ },
+ set (v) {
+ set(this.shadowsLocal, this.shadowSelected, v)
+ }
+ },
+ themeValid () {
+ return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
+ },
+ exportedTheme () {
+ const saveEverything = (
+ !this.keepFonts &&
+ !this.keepShadows &&
+ !this.keepOpacity &&
+ !this.keepRoundness &&
+ !this.keepColor
+ )
+
+ const source = {
+ themeEngineVersion: CURRENT_VERSION
+ }
+
+ if (this.keepFonts || saveEverything) {
+ source.fonts = this.fontsLocal
+ }
+ if (this.keepShadows || saveEverything) {
+ source.shadows = this.shadowsLocal
+ }
+ if (this.keepOpacity || saveEverything) {
+ source.opacity = this.currentOpacity
+ }
+ if (this.keepColor || saveEverything) {
+ source.colors = this.currentColors
+ }
+ if (this.keepRoundness || saveEverything) {
+ source.radii = this.currentRadii
+ }
+
+ const theme = {
+ themeEngineVersion: CURRENT_VERSION,
+ ...this.previewTheme
+ }
+
+ return {
+ // To separate from other random JSON files and possible future source formats
+ _pleroma_theme_version: 2, theme, source
+ }
+ }
+ },
+ components: {
+ ColorInput,
+ OpacityInput,
+ RangeInput,
+ ContrastRatio,
+ ShadowControl,
+ FontControl,
+ TabSwitcher,
+ Preview,
+ ExportImport,
+ Checkbox
+ },
+ methods: {
+ loadTheme (
+ {
+ theme,
+ source,
+ _pleroma_theme_version: fileVersion
+ },
+ origin,
+ forceUseSource = false
+ ) {
+ this.dismissWarning()
+ if (!source && !theme) {
+ throw new Error('Can\'t load theme: empty')
+ }
+ const version = (origin === 'localStorage' && !theme.colors)
+ ? 'l1'
+ : fileVersion
+ const snapshotEngineVersion = (theme || {}).themeEngineVersion
+ const themeEngineVersion = (source || {}).themeEngineVersion || 2
+ const versionsMatch = themeEngineVersion === CURRENT_VERSION
+ const sourceSnapshotMismatch = (
+ theme !== undefined &&
+ source !== undefined &&
+ themeEngineVersion !== snapshotEngineVersion
+ )
+ // Force loading of source if user requested it or if snapshot
+ // is unavailable
+ const forcedSourceLoad = (source && forceUseSource) || !theme
+ if (!(versionsMatch && !sourceSnapshotMismatch) &&
+ !forcedSourceLoad &&
+ version !== 'l1' &&
+ origin !== 'defaults'
+ ) {
+ if (sourceSnapshotMismatch && origin === 'localStorage') {
+ this.themeWarning = {
+ origin,
+ themeEngineVersion,
+ type: 'snapshot_source_mismatch'
+ }
+ } else if (!theme) {
+ this.themeWarning = {
+ origin,
+ noActionsPossible: true,
+ themeEngineVersion,
+ type: 'no_snapshot_old_version'
+ }
+ } else if (!versionsMatch) {
+ this.themeWarning = {
+ origin,
+ noActionsPossible: !source,
+ themeEngineVersion,
+ type: 'wrong_version'
+ }
+ }
+ }
+ this.normalizeLocalState(theme, version, source, forcedSourceLoad)
+ },
+ forceLoadLocalStorage () {
+ this.loadThemeFromLocalStorage(true)
+ },
+ dismissWarning () {
+ this.themeWarning = undefined
+ this.tempImportFile = undefined
+ },
+ forceLoad () {
+ const { origin } = this.themeWarning
+ switch (origin) {
+ case 'localStorage':
+ this.loadThemeFromLocalStorage(true)
+ break
+ case 'file':
+ this.onImport(this.tempImportFile, true)
+ break
+ }
+ this.dismissWarning()
+ },
+ forceSnapshot () {
+ const { origin } = this.themeWarning
+ switch (origin) {
+ case 'localStorage':
+ this.loadThemeFromLocalStorage(false, true)
+ break
+ case 'file':
+ console.err('Forcing snapshout from file is not supported yet')
+ break
+ }
+ this.dismissWarning()
+ },
+ loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
+ const {
+ customTheme: theme,
+ customThemeSource: source
+ } = this.$store.getters.mergedConfig
+ if (!theme && !source) {
+ // Anon user or never touched themes
+ this.loadTheme(
+ this.$store.state.instance.themeData,
+ 'defaults',
+ confirmLoadSource
+ )
+ } else {
+ this.loadTheme(
+ {
+ theme,
+ source: forceSnapshot ? theme : source
+ },
+ 'localStorage',
+ confirmLoadSource
+ )
+ }
+ },
+ setCustomTheme () {
+ this.$store.dispatch('setOption', {
+ name: 'customTheme',
+ value: {
+ themeEngineVersion: CURRENT_VERSION,
+ ...this.previewTheme
+ }
+ })
+ this.$store.dispatch('setOption', {
+ name: 'customThemeSource',
+ value: {
+ themeEngineVersion: CURRENT_VERSION,
+ shadows: this.shadowsLocal,
+ fonts: this.fontsLocal,
+ opacity: this.currentOpacity,
+ colors: this.currentColors,
+ radii: this.currentRadii
+ }
+ })
+ },
+ updatePreviewColorsAndShadows () {
+ this.previewColors = generateColors({
+ opacity: this.currentOpacity,
+ colors: this.currentColors
+ })
+ this.previewShadows = generateShadows(
+ { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
+ this.previewColors.theme.colors,
+ this.previewColors.mod
+ )
+ },
+ onImport (parsed, forceSource = false) {
+ this.tempImportFile = parsed
+ this.loadTheme(parsed, 'file', forceSource)
+ },
+ importValidator (parsed) {
+ const version = parsed._pleroma_theme_version
+ return version >= 1 || version <= 2
+ },
+ clearAll () {
+ this.loadThemeFromLocalStorage()
+ },
+
+ // Clears all the extra stuff when loading V1 theme
+ clearV1 () {
+ Object.keys(this.$data)
+ .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
+ .filter(_ => !v1OnlyNames.includes(_))
+ .forEach(key => {
+ set(this.$data, key, undefined)
+ })
+ },
+
+ clearRoundness () {
+ Object.keys(this.$data)
+ .filter(_ => _.endsWith('RadiusLocal'))
+ .forEach(key => {
+ set(this.$data, key, undefined)
+ })
+ },
+
+ clearOpacity () {
+ Object.keys(this.$data)
+ .filter(_ => _.endsWith('OpacityLocal'))
+ .forEach(key => {
+ set(this.$data, key, undefined)
+ })
+ },
+
+ clearShadows () {
+ this.shadowsLocal = {}
+ },
+
+ clearFonts () {
+ this.fontsLocal = {}
+ },
+
+ /**
+ * This applies stored theme data onto form. Supports three versions of data:
+ * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
+ * v2 (version = 2) - newer version of themes.
+ * v1 (version = 1) - older version of themes (import from file)
+ * v1l (version = l1) - older version of theme (load from local storage)
+ * v1 and v1l differ because of way themes were stored/exported.
+ * @param {Object} theme - theme data (snapshot)
+ * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
+ * @param {Object} source - theme source - this will be used if compatible
+ * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
+ * this allows importing source anyway
+ */
+ normalizeLocalState (theme, version = 0, source, forceSource = false) {
+ let input
+ if (typeof source !== 'undefined') {
+ if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
+ input = source
+ version = source.themeEngineVersion
+ } else {
+ input = theme
+ }
+ } else {
+ input = theme
+ }
+
+ const radii = input.radii || input
+ const opacity = input.opacity
+ const shadows = input.shadows || {}
+ const fonts = input.fonts || {}
+ const colors = !input.themeEngineVersion
+ ? colors2to3(input.colors || input)
+ : input.colors || input
+
+ if (version === 0) {
+ if (input.version) version = input.version
+ // Old v1 naming: fg is text, btn is foreground
+ if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
+ version = 1
+ }
+ // New v2 naming: text is text, fg is foreground
+ if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
+ version = 2
+ }
+ }
+
+ this.engineVersion = version
+
+ // Stuff that differs between V1 and V2
+ if (version === 1) {
+ this.fgColorLocal = rgb2hex(colors.btn)
+ this.textColorLocal = rgb2hex(colors.fg)
+ }
+
+ if (!this.keepColor) {
+ this.clearV1()
+ const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
+ if (version === 1 || version === 'l1') {
+ keys
+ .add('bg')
+ .add('link')
+ .add('cRed')
+ .add('cBlue')
+ .add('cGreen')
+ .add('cOrange')
+ }
+
+ keys.forEach(key => {
+ const color = colors[key]
+ const hex = rgb2hex(colors[key])
+ this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
+ })
+ }
+
+ if (opacity && !this.keepOpacity) {
+ this.clearOpacity()
+ Object.entries(opacity).forEach(([k, v]) => {
+ if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
+ this[k + 'OpacityLocal'] = v
+ })
+ }
+
+ if (!this.keepRoundness) {
+ this.clearRoundness()
+ Object.entries(radii).forEach(([k, v]) => {
+ // 'Radius' is kept mostly for v1->v2 localstorage transition
+ const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
+ this[key + 'RadiusLocal'] = v
+ })
+ }
+
+ if (!this.keepShadows) {
+ this.clearShadows()
+ if (version === 2) {
+ this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
+ } else {
+ this.shadowsLocal = shadows
+ }
+ this.shadowSelected = this.shadowsAvailable[0]
+ }
+
+ if (!this.keepFonts) {
+ this.clearFonts()
+ this.fontsLocal = fonts
+ }
+ }
+ },
+ watch: {
+ currentRadii () {
+ try {
+ this.previewRadii = generateRadii({ radii: this.currentRadii })
+ this.radiiInvalid = false
+ } catch (e) {
+ this.radiiInvalid = true
+ console.warn(e)
+ }
+ },
+ shadowsLocal: {
+ handler () {
+ if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
+ try {
+ this.updatePreviewColorsAndShadows()
+ this.shadowsInvalid = false
+ } catch (e) {
+ this.shadowsInvalid = true
+ console.warn(e)
+ }
+ },
+ deep: true
+ },
+ fontsLocal: {
+ handler () {
+ try {
+ this.previewFonts = generateFonts({ fonts: this.fontsLocal })
+ this.fontsInvalid = false
+ } catch (e) {
+ this.fontsInvalid = true
+ console.warn(e)
+ }
+ },
+ deep: true
+ },
+ currentColors () {
+ try {
+ this.updatePreviewColorsAndShadows()
+ this.colorsInvalid = false
+ this.shadowsInvalid = false
+ } catch (e) {
+ this.colorsInvalid = true
+ this.shadowsInvalid = true
+ console.warn(e)
+ }
+ },
+ currentOpacity () {
+ try {
+ this.updatePreviewColorsAndShadows()
+ } catch (e) {
+ console.warn(e)
+ }
+ },
+ selected () {
+ this.dismissWarning()
+ if (this.selectedVersion === 1) {
+ if (!this.keepRoundness) {
+ this.clearRoundness()
+ }
+
+ if (!this.keepShadows) {
+ this.clearShadows()
+ }
+
+ if (!this.keepOpacity) {
+ this.clearOpacity()
+ }
+
+ if (!this.keepColor) {
+ this.clearV1()
+
+ this.bgColorLocal = this.selected[1]
+ this.fgColorLocal = this.selected[2]
+ this.textColorLocal = this.selected[3]
+ this.linkColorLocal = this.selected[4]
+ this.cRedColorLocal = this.selected[5]
+ this.cGreenColorLocal = this.selected[6]
+ this.cBlueColorLocal = this.selected[7]
+ this.cOrangeColorLocal = this.selected[8]
+ }
+ } else if (this.selectedVersion >= 2) {
+ this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
+ }
+ }
+ }
+}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
new file mode 100644
index 00000000..75b3017d
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -0,0 +1,339 @@
+@import 'src/_variables.scss';
+.theme-tab {
+ .theme-warning {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: .5em;
+ .buttons {
+ .btn {
+ margin-bottom: .5em;
+ }
+ }
+ }
+ .preset-switcher {
+ margin-right: 1em;
+ }
+
+ .style-control {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: 5px;
+
+ .label {
+ flex: 1;
+ }
+
+ &.disabled {
+ input, select {
+ opacity: .5
+ }
+ }
+
+ .opt {
+ margin: .5em;
+ }
+
+ .color-input {
+ flex: 0 0 0;
+ }
+
+ input, select {
+ min-width: 3em;
+ margin: 0;
+ flex: 0;
+
+ &[type=number] {
+ min-width: 5em;
+ }
+
+ &[type=range] {
+ flex: 1;
+ min-width: 3em;
+ align-self: flex-start;
+ }
+ }
+ }
+
+ .reset-container {
+ flex-wrap: wrap;
+ }
+
+ .fonts-container,
+ .reset-container,
+ .apply-container,
+ .radius-container,
+ .color-container,
+ {
+ display: flex;
+ }
+
+ .fonts-container,
+ .radius-container {
+ flex-direction: column;
+ }
+
+ .color-container{
+ > h4 {
+ width: 99%;
+ }
+ flex-wrap: wrap;
+ justify-content: space-between;
+ }
+
+ .fonts-container,
+ .color-container,
+ .shadow-container,
+ .radius-container,
+ .presets-container {
+ margin: 1em 1em 0;
+ }
+
+ .tab-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ width: 100%;
+ min-height: 30px;
+
+ .btn {
+ min-width: 1px;
+ flex: 0 auto;
+ padding: 0 1em;
+ }
+
+ p {
+ flex: 1;
+ margin: 0;
+ margin-right: .5em;
+ }
+
+ margin-bottom: 1em;
+ }
+
+ .shadow-selector {
+ .override {
+ flex: 1;
+ margin-left: .5em;
+ }
+ .select-container {
+ margin-top: -4px;
+ margin-bottom: -3px;
+ }
+ }
+
+ .save-load,
+ .save-load-options {
+ display: flex;
+ justify-content: center;
+ align-items: baseline;
+ flex-wrap: wrap;
+
+ .presets,
+ .import-export {
+ margin-bottom: .5em;
+ }
+
+ .import-export {
+ display: flex;
+ }
+
+ .override {
+ margin-left: .5em;
+ }
+ }
+
+ .save-load-options {
+ flex-wrap: wrap;
+ margin-top: .5em;
+ justify-content: center;
+ .keep-option {
+ margin: 0 .5em .5em;
+ min-width: 25%;
+ }
+ }
+
+ .preview-container {
+ border-top: 1px dashed;
+ border-bottom: 1px dashed;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ margin: 1em 0;
+ padding: 1em;
+ background: var(--body-background-image);
+ background-size: cover;
+ background-position: 50% 50%;
+
+ .dummy {
+ .post {
+ font-family: var(--postFont);
+ display: flex;
+
+ .content {
+ flex: 1;
+
+ h4 {
+ margin-bottom: .25em;
+ }
+
+ .icons {
+ margin-top: .5em;
+ display: flex;
+
+ i {
+ margin-right: 1em;
+ }
+ }
+ }
+ }
+
+ .after-post {
+ margin-top: 1em;
+ display: flex;
+ align-items: center;
+ }
+
+ .avatar, .avatar-alt{
+ background: linear-gradient(135deg, #b8e1fc 0%,#a9d2f3 10%,#90bae4 25%,#90bcea 37%,#90bff0 50%,#6ba8e5 51%,#a2daf5 83%,#bdf3fd 100%);
+ color: black;
+ font-family: sans-serif;
+ text-align: center;
+ margin-right: 1em;
+ }
+
+ .avatar-alt {
+ flex: 0 auto;
+ margin-left: 28px;
+ font-size: 12px;
+ min-width: 20px;
+ min-height: 20px;
+ line-height: 20px;
+ border-radius: $fallback--avatarAltRadius;
+ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ }
+
+ .avatar {
+ flex: 0 auto;
+ width: 48px;
+ height: 48px;
+ font-size: 14px;
+ line-height: 48px;
+ }
+
+ .actions {
+ display: flex;
+ align-items: baseline;
+
+ .checkbox {
+ display: inline-flex;
+ align-items: baseline;
+ margin-right: 1em;
+ flex: 1;
+ }
+ }
+
+ .separator {
+ margin: 1em;
+ border-bottom: 1px solid;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ }
+
+ .panel-heading {
+ .badge, .alert, .btn, .faint {
+ margin-left: 1em;
+ white-space: nowrap;
+ }
+ .faint {
+ text-overflow: ellipsis;
+ min-width: 2em;
+ overflow-x: hidden;
+ }
+ .flex-spacer {
+ flex: 1;
+ }
+ }
+ .btn {
+ margin-left: 0;
+ padding: 0 1em;
+ min-width: 3em;
+ min-height: 30px;
+ }
+ }
+ }
+
+ .apply-container {
+ justify-content: center;
+ }
+
+ .radius-item,
+ .color-item {
+ min-width: 20em;
+ margin: 5px 6px 0 0;
+ display:flex;
+ flex-direction: column;
+ flex: 1 1 0;
+
+ &.wide {
+ min-width: 60%
+ }
+
+ &:not(.wide):nth-child(2n+1) {
+ margin-right: 7px;
+
+ }
+
+ .color, .opacity {
+ display:flex;
+ align-items: baseline;
+ }
+ }
+
+ .radius-item {
+ flex-basis: auto;
+ }
+
+ .theme-radius-rn,
+ .theme-color-cl {
+ border: 0;
+ box-shadow: none;
+ background: transparent;
+ color: var(--faint, $fallback--faint);
+ align-self: stretch;
+ }
+
+ .theme-color-cl,
+ .theme-radius-in,
+ .theme-color-in {
+ margin-left: 4px;
+ }
+
+ .theme-radius-in {
+ min-width: 1em;
+ }
+
+ .theme-radius-in {
+ max-width: 7em;
+ flex: 1;
+ }
+
+ .theme-radius-lb{
+ max-width: 50em;
+ }
+
+ .theme-preview-content {
+ padding: 20px;
+ }
+
+ .apply-container {
+ .btn {
+ min-height: 28px;
+ min-width: 10em;
+ padding: 0 2em;
+ }
+ }
+
+ .btn {
+ margin-left: .25em;
+ margin-right: .25em;
+ }
+}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
new file mode 100644
index 00000000..6f6cf1d6
--- /dev/null
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -0,0 +1,956 @@
+
+
+
+
+
+
+ {{ themeWarningHelp }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.presets') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.style.switcher.keep_color') }}
+
+
+
+
+ {{ $t('settings.style.switcher.keep_shadows') }}
+
+
+
+
+ {{ $t('settings.style.switcher.keep_opacity') }}
+
+
+
+
+ {{ $t('settings.style.switcher.keep_roundness') }}
+
+
+
+
+ {{ $t('settings.style.switcher.keep_fonts') }}
+
+
+
{{ $t('settings.style.switcher.save_load_hint') }}
+
+
+
+
+
+
+
+
+
+
{{ $t('settings.theme_help_v2_1') }}
+
{{ $t('settings.style.common_colors.main') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('settings.style.common_colors.foreground_hint') }}
+
+
{{ $t('settings.style.common_colors.rgbo') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('settings.theme_help_v2_2') }}
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.post') }}
+
+
+ {{ $t('settings.style.advanced_colors.alert') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.badge') }}
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.panel_header') }}
+
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.top_bar') }}
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.inputs') }}
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.buttons') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.style.advanced_colors.pressed') }}
+
+
+
+
+
+
+
+ {{ $t('settings.style.advanced_colors.disabled') }}
+
+
+
+
+ {{ $t('settings.style.advanced_colors.toggled') }}
+
+
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.tabs') }}
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.borders') }}
+
+
+
+
+
{{ $t('settings.style.advanced_colors.faint_text') }}
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.underlay') }}
+
+
+
+
+
{{ $t('settings.style.advanced_colors.poll') }}
+
+
+
+
+
{{ $t('settings.style.advanced_colors.icons') }}
+
+
+
+
{{ $t('settings.style.advanced_colors.highlight') }}
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.popover') }}
+
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.selectedPost') }}
+
+
+
+
+
+
+
+
{{ $t('settings.style.advanced_colors.selectedMenu') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.style.shadows.component') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ filter: drop-shadow()
+
+
{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}
+
+ drop-shadow
+ spread-radius
+ inset
+
+
+ box-shadow
+
+
{{ $t('settings.style.shadows.filter_hint.spread_zero') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/version_tab.js b/src/components/settings_modal/tabs/version_tab.js
new file mode 100644
index 00000000..616bdadf
--- /dev/null
+++ b/src/components/settings_modal/tabs/version_tab.js
@@ -0,0 +1,24 @@
+import { extractCommit } from 'src/services/version/version.service'
+
+const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
+const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
+
+const VersionTab = {
+ data () {
+ const instance = this.$store.state.instance
+ return {
+ backendVersion: instance.backendVersion,
+ frontendVersion: instance.frontendVersion
+ }
+ },
+ computed: {
+ frontendVersionLink () {
+ return pleromaFeCommitUrl + this.frontendVersion
+ },
+ backendVersionLink () {
+ return pleromaBeCommitUrl + extractCommit(this.backendVersion)
+ }
+ }
+}
+
+export default VersionTab
diff --git a/src/components/settings_modal/tabs/version_tab.vue b/src/components/settings_modal/tabs/version_tab.vue
new file mode 100644
index 00000000..acc43569
--- /dev/null
+++ b/src/components/settings_modal/tabs/version_tab.vue
@@ -0,0 +1,31 @@
+
+
+
+
-
-
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index a443531e..a7b790a3 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -46,8 +46,14 @@
&.side-tabs {
flex-direction: row;
+ @media all and (max-width: 800px) {
+ overflow-x: auto;
+ }
> .contents {
flex: 0 1 80%;
+ @media all and (max-width: 800px) {
+ min-width: 96vw;
+ }
}
> .tabs {
flex: 1 0 auto;
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 4ee040e8..b4f275d9 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -50,15 +50,6 @@
>
{{ user.name }}
-
-
-