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 0db03547..1796eb1b 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -8,38 +8,64 @@ import backendInteractorService from '../services/backend_interactor_service/bac import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { applyTheme } from '../services/style_setter/style_setter.js' -const getStatusnetConfig = async ({ store }) => { +let staticInitialResults = null + +const parsedInitialResults = () => { + if (!document.getElementById('initial-results')) { + return null + } + if (!staticInitialResults) { + staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent) + } + return staticInitialResults +} + +const preloadFetch = async (request) => { + const data = parsedInitialResults() + if (!data || !data[request]) { + return window.fetch(request) + } + const requestData = atob(data[request]) + return { + ok: true, + json: () => JSON.parse(requestData), + text: () => requestData + } +} + +const getInstanceConfig = async ({ store }) => { try { - const res = await window.fetch('/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) } } @@ -132,7 +158,7 @@ const getTOS = async ({ store }) => { const getInstancePanel = async ({ store }) => { try { - const res = await window.fetch('/instance/panel.html') + const res = await preloadFetch('/instance/panel.html') if (res.ok) { const html = await res.text() store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) @@ -195,18 +221,28 @@ const resolveStaffAccounts = ({ store, accounts }) => { const getNodeInfo = async ({ store }) => { try { - const res = await window.fetch('/nodeinfo/2.0.json') + const res = await preloadFetch('/nodeinfo/2.0.json') if (res.ok) { 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 }) @@ -257,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] @@ -280,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 @@ -299,16 +340,18 @@ const afterStoreSetup = async ({ store, i18n }) => { } // Now we can try getting the server settings and logging in + // Most of these are preloaded into the index.html so blocking is minimized await Promise.all([ checkOAuthToken({ store }), - getTOS({ store }), getInstancePanel({ store }), - getStickers({ store }), - getNodeInfo({ store }) + getNodeInfo({ store }), + getInstanceConfig({ store }) ]) // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') + getTOS({ store }) + getStickers({ store }) const router = new VueRouter({ mode: 'history', 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') }}
+
' + this.postBodyHtml
- },
...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index efc2485e..3460c2fa 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -3,18 +3,57 @@