diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 0db035475f..87c92a8a7d 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -207,6 +207,8 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) + store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits }) + store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index f4c3479c7d..7974a66d93 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -431,6 +431,7 @@ const EmojiInput = { const offsetBottom = offsetTop + offsetHeight panel.style.top = offsetBottom + 'px' + if (!picker) return picker.$el.style.top = offsetBottom + 'px' picker.$el.style.bottom = 'auto' } diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 56e956cdc7..e6db802ddb 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -1,4 +1,5 @@ import unescape from 'lodash/unescape' +import merge from 'lodash/merge' 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' @@ -16,6 +17,7 @@ const ProfileTab = { newLocked: this.$store.state.users.currentUser.locked, newNoRichText: this.$store.state.users.currentUser.no_rich_text, newDefaultScope: this.$store.state.users.currentUser.default_scope, + newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })), 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, @@ -63,6 +65,18 @@ const ProfileTab = { ...this.$store.state.instance.emoji, ...this.$store.state.instance.customEmoji ] }) + }, + userSuggestor () { + return suggestor({ + users: this.$store.state.users.users, + updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) + }) + }, + fieldsLimits () { + return this.$store.state.instance.fieldsLimits + }, + maxFields () { + return this.fieldsLimits ? this.fieldsLimits.maxFields : 0 } }, methods: { @@ -75,6 +89,7 @@ const ProfileTab = { // Backend notation. /* eslint-disable camelcase */ display_name: this.newName, + fields_attributes: this.newFields.filter(el => el != null), default_scope: this.newDefaultScope, no_rich_text: this.newNoRichText, hide_follows: this.hideFollows, @@ -87,6 +102,8 @@ const ProfileTab = { show_role: this.showRole /* eslint-enable camelcase */ } }).then((user) => { + this.newFields.splice(user.fields.length) + merge(this.newFields, user.fields) this.$store.commit('addNewUsers', [user]) this.$store.commit('setCurrentUser', user) }) @@ -94,6 +111,16 @@ const ProfileTab = { changeVis (visibility) { this.newDefaultScope = visibility }, + addField () { + if (this.newFields.length < this.maxFields) { + this.newFields.push({ name: '', value: '' }) + return true + } + return false + }, + deleteField (index, event) { + this.$delete(this.newFields, index) + }, uploadFile (slot, e) { const file = e.target.files[0] if (!file) { return } diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss index 4aab81eb7e..b3dcf42c86 100644 --- a/src/components/settings_modal/tabs/profile_tab.scss +++ b/src/components/settings_modal/tabs/profile_tab.scss @@ -79,4 +79,21 @@ .setting-subitem { margin-left: 1.75em; } + + .profile-fields { + display: flex; + + &>.emoji-input { + flex: 1 1 auto; + margin: 0 .2em .5em; + } + + &>.icon-container { + width: 20px; + + &>.icon-cancel { + vertical-align: sub; + } + } + } } diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index decdb38962..0f9210a6d1 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -95,6 +95,54 @@ {{ $t('settings.discoverable') }}

+
+

{{ $t('settings.profile_fields.label') }}

+
+ + + + + + +
+ +
+
+ + + {{ $t("settings.profile_fields.add_field") }} + +

{{ $t('settings.bot') }} diff --git a/src/i18n/en.json b/src/i18n/en.json index 4553d25d56..2840904fc0 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -334,6 +334,12 @@ "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "mutes_tab": "Mutes", "play_videos_in_modal": "Play videos in a popup frame", + "profile_fields": { + "label": "Profile metadata", + "add_field": "Add Field", + "name": "Label", + "value": "Content" + }, "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", "name_bio": "Name & Bio", diff --git a/src/modules/users.js b/src/modules/users.js index 5e32bb49c0..68d029315c 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,6 +1,6 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import oauthApi from '../services/new_api/oauth.js' -import { compact, map, each, merge, last, concat, uniq } from 'lodash' +import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' import { set } from 'vue' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' @@ -10,7 +10,7 @@ export const mergeOrAdd = (arr, obj, item) => { const oldItem = obj[item.id] if (oldItem) { // We already have this, so only merge the new info. - merge(oldItem, item) + mergeWith(oldItem, item, mergeArrayLength) return { item: oldItem, new: false } } else { // This is a new item, prepare it @@ -23,6 +23,13 @@ export const mergeOrAdd = (arr, obj, item) => { } } +const mergeArrayLength = (oldValue, newValue) => { + if (isArray(oldValue) && isArray(newValue)) { + oldValue.length = newValue.length + return mergeWith(oldValue, newValue, mergeArrayLength) + } +} + const getNotificationPermission = () => { const Notification = window.Notification @@ -116,7 +123,7 @@ export const mutations = { }, setCurrentUser (state, user) { state.lastLoginName = user.screen_name - state.currentUser = merge(state.currentUser || {}, user) + state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength) }, clearCurrentUser (state) { state.currentUser = false diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js index 670acfc822..dfa5684d15 100644 --- a/test/unit/specs/modules/users.spec.js +++ b/test/unit/specs/modules/users.spec.js @@ -18,6 +18,42 @@ describe('The users module', () => { expect(state.users).to.eql([user]) expect(state.users[0].name).to.eql('Dude') }) + + it('merging array field in new information for old users', () => { + const state = cloneDeep(defaultState) + const user = { + id: '1', + fields: [ + { name: 'Label 1', value: 'Content 1' } + ] + } + const firstModUser = { + id: '1', + fields: [ + { name: 'Label 2', value: 'Content 2' }, + { name: 'Label 3', value: 'Content 3' } + ] + } + const secondModUser = { + id: '1', + fields: [ + { name: 'Label 4', value: 'Content 4' } + ] + } + + mutations.addNewUsers(state, [user]) + expect(state.users[0].fields).to.have.length(1) + expect(state.users[0].fields[0].name).to.eql('Label 1') + + mutations.addNewUsers(state, [firstModUser]) + expect(state.users[0].fields).to.have.length(2) + expect(state.users[0].fields[0].name).to.eql('Label 2') + expect(state.users[0].fields[1].name).to.eql('Label 3') + + mutations.addNewUsers(state, [secondModUser]) + expect(state.users[0].fields).to.have.length(1) + expect(state.users[0].fields[0].name).to.eql('Label 4') + }) }) describe('findUser', () => {