diff options
Diffstat (limited to 'src/components/user_settings')
| -rw-r--r-- | src/components/user_settings/confirm.js | 9 | ||||
| -rw-r--r-- | src/components/user_settings/confirm.vue | 14 | ||||
| -rw-r--r-- | src/components/user_settings/mfa.js | 152 | ||||
| -rw-r--r-- | src/components/user_settings/mfa.vue | 121 | ||||
| -rw-r--r-- | src/components/user_settings/mfa_backup_codes.js | 17 | ||||
| -rw-r--r-- | src/components/user_settings/mfa_backup_codes.vue | 22 | ||||
| -rw-r--r-- | src/components/user_settings/mfa_totp.js | 49 | ||||
| -rw-r--r-- | src/components/user_settings/mfa_totp.vue | 23 | ||||
| -rw-r--r-- | src/components/user_settings/user_settings.js | 256 | ||||
| -rw-r--r-- | src/components/user_settings/user_settings.vue | 142 |
10 files changed, 650 insertions, 155 deletions
diff --git a/src/components/user_settings/confirm.js b/src/components/user_settings/confirm.js new file mode 100644 index 00000000..0f4ddfc9 --- /dev/null +++ b/src/components/user_settings/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/user_settings/confirm.vue b/src/components/user_settings/confirm.vue new file mode 100644 index 00000000..46a42e38 --- /dev/null +++ b/src/components/user_settings/confirm.vue @@ -0,0 +1,14 @@ +<template> +<div> + <slot></slot> + <button class="btn btn-default" @click="confirm" :disabled="disabled"> + {{$t('general.confirm')}} + </button> + <button class="btn btn-default" @click="cancel" :disabled="disabled"> + {{$t('general.cancel')}} + </button> +</div> +</template> + +<script src="./confirm.js"> +</script> diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js new file mode 100644 index 00000000..2acee862 --- /dev/null +++ b/src/components/user_settings/mfa.js @@ -0,0 +1,152 @@ +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 + 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.fetchSettingsMFA() + this.settings = result.settings + return result + } + }, + mounted () { + this.fetchSettings().then(() => { + this.readyInit = true + }) + } +} +export default Mfa diff --git a/src/components/user_settings/mfa.vue b/src/components/user_settings/mfa.vue new file mode 100644 index 00000000..ded426dd --- /dev/null +++ b/src/components/user_settings/mfa.vue @@ -0,0 +1,121 @@ +<template> +<div class="setting-item mfa-settings" v-if="readyInit"> + + <div class="mfa-heading"> + <h2>{{$t('settings.mfa.title')}}</h2> + </div> + + <div> + <div class="setting-item" v-if="!setupInProgress"> + <!-- Enabled methods --> + <h3>{{$t('settings.mfa.authentication_methods')}}</h3> + <totp-item :settings="settings" @deactivate="fetchSettings" @activate="activateOTP"/> + <br /> + + <div v-if="settings.enabled"> <!-- backup codes block--> + <recovery-codes :backup-codes="backupCodes" v-if="!confirmNewBackupCodes" /> + <button class="btn btn-default" @click="getBackupCodes" v-if="!confirmNewBackupCodes"> + {{$t('settings.mfa.generate_new_recovery_codes')}} + </button> + + <div v-if="confirmNewBackupCodes"> + <confirm @confirm="confirmBackupCodes" @cancel="cancelBackupCodes" + :disabled="backupCodes.inProgress"> + <p class="warning">{{$t('settings.mfa.warning_of_generate_new_codes')}}</p> + </confirm> + </div> + </div> + </div> + + <div v-if="setupInProgress"> <!-- setup block--> + + <h3>{{$t('settings.mfa.setup_otp')}}</h3> + + <recovery-codes :backup-codes="backupCodes" v-if="!setupOTPInProgress"/> + + + <button class="btn btn-default" @click="cancelSetup" v-if="canSetupOTP"> + {{$t('general.cancel')}} + </button> + + <button class="btn btn-default" v-if="canSetupOTP" @click="setupOTP"> + {{$t('settings.mfa.setup_otp')}} + </button> + + <template v-if="setupOTPInProgress"> + <i v-if="prepareOTP">{{$t('settings.mfa.wait_pre_setup_otp')}}</i> + + <div v-if="confirmOTP"> + <div class="setup-otp"> + <div class="qr-code"> + <h4>{{$t('settings.mfa.scan.title')}}</h4> + <p>{{$t('settings.mfa.scan.desc')}}</p> + <qrcode :value="otpSettings.provisioning_uri" :options="{ width: 200 }"></qrcode> + <p> + {{$t('settings.mfa.scan.secret_code')}}: + {{otpSettings.key}} + </p> + </div> + + <div class="verify"> + <h4>{{$t('general.verify')}}</h4> + <p>{{$t('settings.mfa.verify.desc')}}</p> + <input type="text" v-model="otpConfirmToken"> + + <p>{{$t('settings.enter_current_password_to_confirm')}}:</p> + <input type="password" v-model="currentPassword"> + <div class="confirm-otp-actions"> + <button class="btn btn-default" @click="doConfirmOTP"> + {{$t('settings.mfa.confirm_and_enable')}} + </button> + <button class="btn btn-default" @click="cancelSetup"> + {{$t('general.cancel')}} + </button> + </div> + <div class="alert error" v-if="error">{{error}}</div> + </div> + </div> + </div> + </template> + </div> + + </div> +</div> +</template> + +<script src="./mfa.js"></script> +<style lang="scss"> +@import '../../_variables.scss'; +.warning { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); +} +.mfa-settings { + .mfa-heading, .method-item { + overflow: hidden; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + } + + .setup-otp { + display: flex; + justify-content: center; + flex-wrap: wrap; + .qr-code { + flex: 1; + padding-right: 10px; + } + .verify { flex: 1; } + .error { margin: 4px 0 0 0; } + .confirm-otp-actions { + button { + width: 15em; + margin-top: 5px; + } + + } + } +} +</style> diff --git a/src/components/user_settings/mfa_backup_codes.js b/src/components/user_settings/mfa_backup_codes.js new file mode 100644 index 00000000..f0a984ec --- /dev/null +++ b/src/components/user_settings/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/user_settings/mfa_backup_codes.vue b/src/components/user_settings/mfa_backup_codes.vue new file mode 100644 index 00000000..c275bd63 --- /dev/null +++ b/src/components/user_settings/mfa_backup_codes.vue @@ -0,0 +1,22 @@ +<template> +<div> + <h4 v-if="displayTitle">{{$t('settings.mfa.recovery_codes')}}</h4> + <i v-if="inProgress">{{$t('settings.mfa.waiting_a_recovery_codes')}}</i> + <template v-if="ready"> + <p class="alert warning">{{$t('settings.mfa.recovery_codes_warning')}}</p> + <ul class="backup-codes"><li v-for="code in backupCodes.codes">{{code}}</li></ul> + </template> +</div> +</template> +<script src="./mfa_backup_codes.js"></script> +<style lang="scss"> +@import '../../_variables.scss'; + +.warning { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); +} +.backup-codes { + font-family: var(--postCodeFont, monospace); +} +</style> diff --git a/src/components/user_settings/mfa_totp.js b/src/components/user_settings/mfa_totp.js new file mode 100644 index 00000000..8408d8e9 --- /dev/null +++ b/src/components/user_settings/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/user_settings/mfa_totp.vue b/src/components/user_settings/mfa_totp.vue new file mode 100644 index 00000000..6b73c8f4 --- /dev/null +++ b/src/components/user_settings/mfa_totp.vue @@ -0,0 +1,23 @@ +<template> +<div> + <div class="method-item"> + <strong>{{$t('settings.mfa.otp')}}</strong> + <button class="btn btn-default" v-if="!isActivated" @click="doActivate"> + {{$t('general.enable')}} + </button> + + <button class="btn btn-default" :disabled="deactivate" @click="doDeactivate" + v-if="isActivated"> + {{$t('general.disable')}} + </button> + </div> + + <confirm @confirm="confirmDeactivate" @cancel="cancelDeactivate" + :disabled="inProgress" v-if="deactivate"> + {{$t('settings.enter_current_password_to_confirm')}}: + <input type="password" v-model="currentPassword"> + </confirm> + <div class="alert error" v-if="error">{{error}}</div> +</div> +</template> +<script src="./mfa_totp.js"></script> diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index 5cb23b97..69505806 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -1,33 +1,35 @@ -import { compose } from 'vue-compose' import unescape from 'lodash/unescape' import get from 'lodash/get' +import map from 'lodash/map' +import reject from 'lodash/reject' import TabSwitcher from '../tab_switcher/tab_switcher.js' import ImageCropper from '../image_cropper/image_cropper.vue' import StyleSwitcher from '../style_switcher/style_switcher.vue' +import ScopeSelector from '../scope_selector/scope_selector.vue' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' import BlockCard from '../block_card/block_card.vue' import MuteCard from '../mute_card/mute_card.vue' +import SelectableList from '../selectable_list/selectable_list.vue' +import ProgressButton from '../progress_button/progress_button.vue' import EmojiInput from '../emoji-input/emoji-input.vue' +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 withList from '../../hocs/with_list/with_list' +import userSearchApi from '../../services/new_api/user_search.js' +import Mfa from './mfa.vue' -const BlockList = compose( - withSubscription({ - fetch: (props, $store) => $store.dispatch('fetchBlocks'), - select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), - childPropName: 'entries' - }), - withList({ getEntryProps: userId => ({ userId }) }) -)(BlockCard) +const BlockList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchBlocks'), + select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), + childPropName: 'items' +})(SelectableList) -const MuteList = compose( - withSubscription({ - fetch: (props, $store) => $store.dispatch('fetchMutes'), - select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), - childPropName: 'entries' - }), - withList({ getEntryProps: userId => ({ userId }) }) -)(MuteCard) +const MuteList = withSubscription({ + fetch: (props, $store) => $store.dispatch('fetchMutes'), + select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), + childPropName: 'items' +})(SelectableList) const UserSettings = { data () { @@ -41,14 +43,9 @@ const UserSettings = { hideFollowers: this.$store.state.users.currentUser.hide_followers, showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, - followList: null, - followImportError: false, - followsImported: false, - enableFollowsExport: true, pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, - followListUploading: false, bannerPreview: null, backgroundPreview: null, bannerUploadError: null, @@ -59,7 +56,8 @@ const UserSettings = { changePasswordInputs: [ '', '', '' ], changedPassword: false, changePasswordError: false, - activeTab: 'profile' + activeTab: 'profile', + notificationSettings: this.$store.state.users.currentUser.notification_settings } }, created () { @@ -67,11 +65,19 @@ const UserSettings = { }, components: { StyleSwitcher, + ScopeSelector, TabSwitcher, ImageCropper, BlockList, MuteList, - EmojiInput + EmojiInput, + Autosuggest, + BlockCard, + MuteCard, + ProgressButton, + Importer, + Exporter, + Mfa }, computed: { user () { @@ -80,8 +86,8 @@ const UserSettings = { pleromaBackend () { return this.$store.state.instance.pleromaBackend }, - scopeOptionsEnabled () { - return this.$store.state.instance.scopeOptionsEnabled + minimalScopesMode () { + return this.$store.state.instance.minimalScopesMode }, vis () { return { @@ -106,39 +112,29 @@ const UserSettings = { }, methods: { updateProfile () { - const name = this.newName - const description = this.newBio - const locked = this.newLocked - // Backend notation. - /* eslint-disable camelcase */ - const default_scope = this.newDefaultScope - const no_rich_text = this.newNoRichText - const hide_follows = this.hideFollows - const hide_followers = this.hideFollowers - const show_role = this.showRole - - /* eslint-enable camelcase */ this.$store.state.api.backendInteractor .updateProfile({ params: { - name, - description, - locked, + note: this.newBio, + locked: this.newLocked, // Backend notation. /* eslint-disable camelcase */ - default_scope, - no_rich_text, - hide_follows, - hide_followers, - show_role + display_name: this.newName, + default_scope: this.newDefaultScope, + no_rich_text: this.newNoRichText, + hide_follows: this.hideFollows, + hide_followers: this.hideFollowers, + show_role: this.showRole /* eslint-enable camelcase */ }}).then((user) => { - if (!user.error) { - this.$store.commit('addNewUsers', [user]) - this.$store.commit('setCurrentUser', 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 }, @@ -156,23 +152,29 @@ const UserSettings = { reader.onload = ({target}) => { const img = target.result this[slot + 'Preview'] = img + this[slot] = file } reader.readAsDataURL(file) }, submitAvatar (cropper, file) { - let img - if (cropper) { - img = cropper.getCroppedCanvas().toDataURL(file.type) - } else { - img = 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)) + }) + } - return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => { - if (!user.error) { - this.$store.commit('addNewUsers', [user]) - this.$store.commit('setCurrentUser', user) + if (cropper) { + cropper.getCroppedCanvas().toBlob(updateAvatar, file.type) } else { - throw new Error(this.$t('upload.error.base') + user.error) + updateAvatar(file) } }) }, @@ -182,30 +184,17 @@ const UserSettings = { submitBanner () { if (!this.bannerPreview) { return } - let banner = this.bannerPreview - // eslint-disable-next-line no-undef - let imginfo = new Image() - /* eslint-disable camelcase */ - let offset_top, offset_left, width, height - imginfo.src = banner - width = imginfo.width - height = imginfo.height - offset_top = 0 - offset_left = 0 this.bannerUploading = true - this.$store.state.api.backendInteractor.updateBanner({params: {banner, offset_top, offset_left, width, height}}).then((data) => { - if (!data.error) { - let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser)) - clone.cover_photo = data.url - this.$store.commit('addNewUsers', [clone]) - this.$store.commit('setCurrentUser', clone) + this.$store.state.api.backendInteractor.updateBanner({banner: this.banner}) + .then((user) => { + this.$store.commit('addNewUsers', [user]) + this.$store.commit('setCurrentUser', user) this.bannerPreview = null - } else { - this.bannerUploadError = this.$t('upload.error.base') + data.error - } - this.bannerUploading = false - }) - /* eslint-enable camelcase */ + }) + .catch((err) => { + this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message + }) + .then(() => { this.bannerUploading = false }) }, submitBg () { if (!this.backgroundPreview) { return } @@ -232,62 +221,41 @@ const UserSettings = { this.backgroundUploading = false }) }, - importFollows () { - this.followListUploading = true - const followList = this.followList - this.$store.state.api.backendInteractor.followImport({params: followList}) + importFollows (file) { + return this.$store.state.api.backendInteractor.importFollows(file) .then((status) => { - if (status) { - this.followsImported = true - } else { - this.followImportError = true + if (!status) { + throw new Error('failed') } - this.followListUploading = false }) }, - /* This function takes an Array of Users - * and outputs a file with all the addresses for the user to download - */ - exportPeople (users, filename) { - // Get all the friends addresses - var UserAddresses = users.map(function (user) { + 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 - user.screen_name += '@' + location.hostname + return user.screen_name + '@' + location.hostname } return user.screen_name }).join('\n') - // Make the user download the file - var fileToDownload = document.createElement('a') - fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(UserAddresses)) - fileToDownload.setAttribute('download', filename) - fileToDownload.style.display = 'none' - document.body.appendChild(fileToDownload) - fileToDownload.click() - document.body.removeChild(fileToDownload) - }, - exportFollows () { - this.enableFollowsExport = false - this.$store.state.api.backendInteractor - .exportFriends({ - id: this.$store.state.users.currentUser.id - }) - .then((friendList) => { - this.exportPeople(friendList, 'friends.csv') - setTimeout(() => { this.enableFollowsExport = true }, 2000) - }) }, - followListChange () { - // eslint-disable-next-line no-undef - let formData = new FormData() - formData.append('list', this.$refs.followlist.files[0]) - this.followList = formData + getFollowsContent () { + return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id }) + .then(this.generateExportableUsersContent) }, - dismissImported () { - this.followsImported = false - this.followImportError = false + getBlocksContent () { + return this.$store.state.api.backendInteractor.fetchBlocks() + .then(this.generateExportableUsersContent) }, confirmDelete () { this.deletingAccount = true @@ -332,6 +300,40 @@ const UserSettings = { 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) + 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 userSearchApi.search({query, store: this.$store}) + .then((users) => { + this.$store.dispatch('addNewUsers', users) + return 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) + }, + identity (value) { + return value } } } diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 52df143c..bbe41f11 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -22,7 +22,7 @@ <div class="setting-item" > <h2>{{$t('settings.name_bio')}}</h2> <p>{{$t('settings.name')}}</p> - <EmojiInput + <EmojiInput type="text" v-model="newName" id="username" @@ -38,13 +38,14 @@ <input type="checkbox" v-model="newLocked" id="account-locked"> <label for="account-locked">{{$t('settings.lock_account_description')}}</label> </p> - <div v-if="scopeOptionsEnabled"> + <div> <label for="default-vis">{{$t('settings.default_vis')}}</label> <div id="default-vis" class="visibility-tray"> - <i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct" :title="$t('post_status.scope.direct')" ></i> - <i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private" :title="$t('post_status.scope.private')"></i> - <i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted" :title="$t('post_status.scope.unlisted')"></i> - <i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public" :title="$t('post_status.scope.public')"></i> + <scope-selector + :showAll="true" + :userDefault="newDefaultScope" + :initialScope="newDefaultScope" + :onScopeChange="changeVis"/> </div> </div> <p> @@ -151,7 +152,7 @@ </tbody> </table> </div> - + <mfa /> <div class="setting-item"> <h2>{{$t('settings.delete_account')}}</h2> <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p> @@ -167,43 +168,110 @@ </div> </div> + <div :label="$t('settings.notifications')" v-if="pleromaBackend"> + <div class="setting-item"> + <div class="select-multiple"> + <span class="label">{{$t('settings.notification_setting')}}</span> + <ul class="option-list"> + <li> + <input type="checkbox" id="notification-setting-follows" v-model="notificationSettings.follows"> + <label for="notification-setting-follows"> + {{$t('settings.notification_setting_follows')}} + </label> + </li> + <li> + <input type="checkbox" id="notification-setting-followers" v-model="notificationSettings.followers"> + <label for="notification-setting-followers"> + {{$t('settings.notification_setting_followers')}} + </label> + </li> + <li> + <input type="checkbox" id="notification-setting-non-follows" v-model="notificationSettings.non_follows"> + <label for="notification-setting-non-follows"> + {{$t('settings.notification_setting_non_follows')}} + </label> + </li> + <li> + <input type="checkbox" id="notification-setting-non-followers" v-model="notificationSettings.non_followers"> + <label for="notification-setting-non-followers"> + {{$t('settings.notification_setting_non_followers')}} + </label> + </li> + </ul> + </div> + <p>{{$t('settings.notification_mutes')}}</p> + <p>{{$t('settings.notification_blocks')}}</p> + <button class="btn btn-default" @click="updateNotificationSettings">{{$t('general.submit')}}</button> + </div> + </div> + <div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend"> <div class="setting-item"> <h2>{{$t('settings.follow_import')}}</h2> <p>{{$t('settings.import_followers_from_a_csv_file')}}</p> - <form> - <input type="file" ref="followlist" v-on:change="followListChange" /> - </form> - <i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i> - <button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button> - <div v-if="followsImported"> - <i class="icon-cross" @click="dismissImported"></i> - <p>{{$t('settings.follows_imported')}}</p> - </div> - <div v-else-if="followImportError"> - <i class="icon-cross" @click="dismissImported"></i> - <p>{{$t('settings.follow_import_error')}}</p> - </div> + <Importer :submitHandler="importFollows" :successMessage="$t('settings.follows_imported')" :errorMessage="$t('settings.follow_import_error')" /> </div> - <div class="setting-item" v-if="enableFollowsExport"> + <div class="setting-item"> <h2>{{$t('settings.follow_export')}}</h2> - <button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button> + <Exporter :getContent="getFollowsContent" filename="friends.csv" :exportButtonLabel="$t('settings.follow_export_button')" /> + </div> + <div class="setting-item"> + <h2>{{$t('settings.block_import')}}</h2> + <p>{{$t('settings.import_blocks_from_a_csv_file')}}</p> + <Importer :submitHandler="importBlocks" :successMessage="$t('settings.blocks_imported')" :errorMessage="$t('settings.block_import_error')" /> </div> - <div class="setting-item" v-else> - <h2>{{$t('settings.follow_export_processing')}}</h2> + <div class="setting-item"> + <h2>{{$t('settings.block_export')}}</h2> + <Exporter :getContent="getBlocksContent" filename="blocks.csv" :exportButtonLabel="$t('settings.block_export_button')" /> </div> </div> <div :label="$t('settings.blocks_tab')"> - <block-list :refresh="true"> + <div class="profile-edit-usersearch-wrapper"> + <Autosuggest :filter="filterUnblockedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')"> + <BlockCard slot-scope="row" :userId="row.item"/> + </Autosuggest> + </div> + <BlockList :refresh="true" :getKey="identity"> + <template slot="header" slot-scope="{selected}"> + <div class="profile-edit-bulk-actions"> + <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => blockUsers(selected)"> + {{ $t('user_card.block') }} + <template slot="progress">{{ $t('user_card.block_progress') }}</template> + </ProgressButton> + <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unblockUsers(selected)"> + {{ $t('user_card.unblock') }} + <template slot="progress">{{ $t('user_card.unblock_progress') }}</template> + </ProgressButton> + </div> + </template> + <template slot="item" slot-scope="{item}"><BlockCard :userId="item" /></template> <template slot="empty">{{$t('settings.no_blocks')}}</template> - </block-list> + </BlockList> </div> <div :label="$t('settings.mutes_tab')"> - <mute-list :refresh="true"> + <div class="profile-edit-usersearch-wrapper"> + <Autosuggest :filter="filterUnMutedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')"> + <MuteCard slot-scope="row" :userId="row.item"/> + </Autosuggest> + </div> + <MuteList :refresh="true" :getKey="identity"> + <template slot="header" slot-scope="{selected}"> + <div class="profile-edit-bulk-actions"> + <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => muteUsers(selected)"> + {{ $t('user_card.mute') }} + <template slot="progress">{{ $t('user_card.mute_progress') }}</template> + </ProgressButton> + <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unmuteUsers(selected)"> + {{ $t('user_card.unmute') }} + <template slot="progress">{{ $t('user_card.unmute_progress') }}</template> + </ProgressButton> + </div> + </template> + <template slot="item" slot-scope="{item}"><MuteCard :userId="item" /></template> <template slot="empty">{{$t('settings.no_mutes')}}</template> - </mute-list> + </MuteList> </div> </tab-switcher> </div> @@ -221,6 +289,10 @@ margin: 0; } + .visibility-tray { + padding-top: 5px; + } + input[type=file] { padding: 5px; height: auto; @@ -262,5 +334,19 @@ text-align: right; } } + + &-usersearch-wrapper { + padding: 1em; + } + + &-bulk-actions { + text-align: right; + padding: 0 1em; + min-height: 28px; + + button { + width: 10em; + } + } } </style> |
