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/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 ++++++++++++++ 14 files changed, 1326 insertions(+) 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/settings_modal') diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js new file mode 100644 index 00000000..1f4c038f --- /dev/null +++ b/src/components/settings_modal/settings_modal.js @@ -0,0 +1,39 @@ +import Modal from '../modal/modal.vue' +import TabSwitcher from '../tab_switcher/tab_switcher.js' + +import Profile from './tabs/profile.vue' +import Security from './tabs/security.vue' +import Notifications from './tabs/notifications.vue' +import DataImportExport from './tabs/data_import_export.vue' +import MutesAndBlocks from './tabs/mutes_and_blocks.vue' + +const SettingsModal = { + components: { + Modal, + TabSwitcher, + Profile, + Security, + Notifications, + DataImportExport, + MutesAndBlocks + }, + data () { + return { + resettingForm: false + } + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + modalActivated () { + return this.$store.state.interface.settingsModalState !== 'hidden' + } + }, + watch: { + }, + methods: { + } +} + +export default SettingsModal diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss new file mode 100644 index 00000000..8cea52d2 --- /dev/null +++ b/src/components/settings_modal/settings_modal.scss @@ -0,0 +1,59 @@ +@import '../../_variables.scss'; +.settings-modal { + .settings_tab-switcher { + height: 100%; + } + .settings-modal-panel { + width: 1000px; + max-width: 90vw; + height: 90vh; + } + .panel-body { + overflow-y: hidden; + } + .setting-item { + border-bottom: 2px solid var(--fg, $fallback--fg); + margin: 1em 1em 1.4em; + padding-bottom: 1.4em; + + > div { + margin-bottom: .5em; + &:last-child { + margin-bottom: 0; + } + } + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 1em; + } + + select { + min-width: 10em; + } + + + textarea { + width: 100%; + max-width: 100%; + height: 100px; + } + + .unavailable, + .unavailable i { + color: var(--cRed, $fallback--cRed); + color: $fallback--cRed; + } + + .btn { + min-height: 28px; + min-width: 10em; + padding: 0 2em; + } + + .number-input { + max-width: 6em; + } + } +} diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue new file mode 100644 index 00000000..9e35d3f6 --- /dev/null +++ b/src/components/settings_modal/settings_modal.vue @@ -0,0 +1,31 @@ + + + + + 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 @@ + + + + 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 @@ + + + + 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 @@ + + + + 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 @@ +