diff --git a/CHANGELOG.md b/CHANGELOG.md index feabbf06..887588f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 'Copy link' button for statuses (in the ellipsis menu) - Autocomplete domains from list of known instances - 'Bot' settings option and badge +- Added profile meta data fields that can be set in profile settings ### Changed - Registration page no longer requires email if the server is configured not to require it diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 0c213ea3..01074b8c 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -33,38 +33,39 @@ const preloadFetch = async (request) => { } } -const getStatusnetConfig = async ({ store }) => { +const getInstanceConfig = async ({ store }) => { try { - const res = await preloadFetch('/api/statusnet/config.json') + const res = await preloadFetch('/api/v1/instance') if (res.ok) { const data = await res.json() - const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site + const textlimit = data.max_toot_chars + const vapidPublicKey = data.pleroma.vapid_public_key - store.dispatch('setInstanceOption', { name: 'name', value: name }) - store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) - store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) - store.dispatch('setInstanceOption', { name: 'server', value: server }) - store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' }) - - // TODO: default values for this stuff, added if to not make it break on - // my dev config out of the box. - if (uploadlimit) { - store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) }) - store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) }) - store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) }) - store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) }) - } + store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) if (vapidPublicKey) { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) } - - return data.site.pleromafe } else { throw (res) } } catch (error) { - console.error('Could not load statusnet config, potentially fatal') + console.error('Could not load instance config, potentially fatal') + console.error(error) + } +} + +const getBackendProvidedConfig = async ({ store }) => { + try { + const res = await window.fetch('/api/pleroma/frontend_configurations') + if (res.ok) { + const data = await res.json() + return data.pleroma_fe + } else { + throw (res) + } + } catch (error) { + console.error('Could not load backend-provided frontend config, potentially fatal') console.error(error) } } @@ -225,13 +226,23 @@ const getNodeInfo = async ({ store }) => { const data = await res.json() const metadata = data.metadata const features = metadata.features + store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) + store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) + store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) + const uploadLimits = metadata.uploadLimits + store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) + store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) }) + store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) }) + store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) }) + store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits }) + store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) @@ -282,7 +293,7 @@ const getNodeInfo = async ({ store }) => { const setConfig = async ({ store }) => { // apiConfig, staticConfig - const configInfos = await Promise.all([getStatusnetConfig({ store }), getStaticConfig()]) + const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()]) const apiConfig = configInfos[0] const staticConfig = configInfos[1] @@ -305,6 +316,11 @@ const checkOAuthToken = async ({ store }) => { const afterStoreSetup = async ({ store, i18n }) => { const width = windowWidth() store.dispatch('setMobileLayout', width <= 800) + + const overrides = window.___pleromafe_dev_overrides || {} + const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin + store.dispatch('setInstanceOption', { name: 'server', value: server }) + await setConfig({ store }) const { customTheme, customThemeSource } = store.state.config @@ -329,7 +345,8 @@ const afterStoreSetup = async ({ store, i18n }) => { getTOS({ store }), getInstancePanel({ store }), getStickers({ store }), - getNodeInfo({ store }) + getNodeInfo({ store }), + getInstanceConfig({ store }) ]) // Start fetching things that don't need to block the UI diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index f4c3479c..7974a66d 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 56e956cd..e6db802d 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 4aab81eb..b3dcf42c 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 decdb389..0f9210a6 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 4553d25d..2840904f 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/i18n/it.json b/src/i18n/it.json index 6c8be351..7311f0b6 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -255,7 +255,8 @@ "top_bar": "Barra superiore", "panel_header": "Titolo pannello", "badge_notification": "Notifica", - "popover": "Suggerimenti, menù, sbalzi" + "popover": "Suggerimenti, menù, sbalzi", + "toggled": "Scambiato" }, "common_colors": { "rgbo": "Icone, accenti, medaglie", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index af728b6e..15ce5cbe 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -28,7 +28,12 @@ "enable": "Inschakelen", "confirm": "Bevestigen", "verify": "Verifiëren", - "generic_error": "Er is een fout opgetreden" + "generic_error": "Er is een fout opgetreden", + "peek": "Spiek", + "close": "Sluiten", + "retry": "Opnieuw proberen", + "error_retry": "Probeer het opnieuw", + "loading": "Laden…" }, "login": { "login": "Log in", @@ -90,7 +95,7 @@ "text/bbcode": "BBCode" }, "content_warning": "Onderwerp (optioneel)", - "default": "Zojuist geland in L.A.", + "default": "Tijd voor anime!", "direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.", "posting": "Plaatsen", "scope": { @@ -377,7 +382,7 @@ "button": "Knop", "text": "Nog een boel andere {0} en {1}", "mono": "inhoud", - "input": "Zojuist geland in L.A.", + "input": "Tijd voor anime!", "faint_link": "handige gebruikershandleiding", "fine_print": "Lees onze {0} om niets nuttig te leren!", "header_faint": "Alles komt goed", @@ -451,7 +456,7 @@ "user_mutes": "Gebruikers", "useStreamingApi": "Berichten en meldingen in real-time ontvangen", "useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)", - "type_domains_to_mute": "Voer domeinen in om te negeren", + "type_domains_to_mute": "Zoek domeinen om te negeren", "upload_a_photo": "Upload een foto", "fun": "Plezier", "greentext": "Meme pijlen", @@ -470,7 +475,8 @@ "frontend_version": "Frontend Versie", "backend_version": "Backend Versie", "title": "Versie" - } + }, + "mutes_and_blocks": "Negeringen en Blokkades" }, "timeline": { "collapse": "Inklappen", @@ -708,7 +714,9 @@ "unpin": "Van profiel losmaken", "delete": "Status verwijderen", "repeats": "Herhalingen", - "favorites": "Favorieten" + "favorites": "Favorieten", + "thread_muted_and_words": ", heeft woorden:", + "thread_muted": "Thread genegeerd" }, "time": { "years_short": "{0}j", diff --git a/src/modules/users.js b/src/modules/users.js index 5e32bb49..68d02931 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 670acfc8..dfa5684d 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', () => {