Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
49 KB
Referenced Files
None
Subscribers
None
diff --git a/changelog.d/everything-instance-default.add b/changelog.d/everything-instance-default.add
new file mode 100644
index 0000000000..6c44759051
--- /dev/null
+++ b/changelog.d/everything-instance-default.add
@@ -0,0 +1 @@
+Make every configuration option default-overridable by instance admins
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index fd4c57229b..ad994af6a5 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -1,480 +1,446 @@
/* global process */
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core';
config.autoAddCss = false
import App from '../App.vue'
import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { applyConfig } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
import { useOAuthStore } from 'src/stores/oauth'
import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface'
import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow'
+import { staticOrApiConfigDefault, instanceDefaultConfig } from 'src/modules/default_config_state.js'
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 decodeUTF8Base64 = (data) => {
const rawData = atob(data)
const array = Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
const text = new TextDecoder().decode(array)
return text
}
const preloadFetch = async (request) => {
const data = parsedInitialResults()
if (!data || !data[request]) {
return window.fetch(request)
}
const decoded = decodeUTF8Base64(data[request])
const requestData = JSON.parse(decoded)
return {
ok: true,
json: () => requestData,
text: () => requestData
}
}
const getInstanceConfig = async ({ store }) => {
try {
const res = await preloadFetch('/api/v1/instance')
if (res.ok) {
const data = await res.json()
const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma })
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required })
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 })
if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
}
} else {
throw (res)
}
} catch (error) {
console.error('Could not load instance config, potentially fatal')
console.error(error)
}
// We should check for scrobbles support here but it requires userId
// so instead we check for it where it's fetched (statuses.js)
}
const getBackendProvidedConfig = async () => {
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)
}
}
const getStaticConfig = async () => {
try {
const res = await window.fetch('/static/config.json')
if (res.ok) {
return res.json()
} else {
throw (res)
}
} catch (error) {
console.warn('Failed to load static/config.json, continuing without it.')
console.warn(error)
return {}
}
}
const setSettings = async ({ apiConfig, staticConfig, store }) => {
const overrides = window.___pleromafe_dev_overrides || {}
const env = window.___pleromafe_mode.NODE_ENV
// This takes static config and overrides properties that are present in apiConfig
let config = {}
if (overrides.staticConfigPreference && env === 'development') {
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
config = Object.assign({}, apiConfig, staticConfig)
} else {
config = Object.assign({}, staticConfig, apiConfig)
}
const copyInstanceOption = (name) => {
- store.dispatch('setInstanceOption', { name, value: config[name] })
+ if (typeof config[name] !== 'undefined') {
+ store.dispatch('setInstanceOption', { name, value: config[name] })
+ }
}
- copyInstanceOption('theme')
- copyInstanceOption('style')
- copyInstanceOption('palette')
- copyInstanceOption('embeddedToS')
- copyInstanceOption('nsfwCensorImage')
- copyInstanceOption('background')
- copyInstanceOption('hidePostStats')
- copyInstanceOption('hideBotIndication')
- copyInstanceOption('hideUserStats')
- copyInstanceOption('hideFilteredStatuses')
- copyInstanceOption('logo')
-
- store.dispatch('setInstanceOption', {
- name: 'logoMask',
- value: typeof config.logoMask === 'undefined'
- ? true
- : config.logoMask
- })
+ Object.keys(staticOrApiConfigDefault).forEach(copyInstanceOption)
+ Object.keys(instanceDefaultConfig).forEach(copyInstanceOption)
- store.dispatch('setInstanceOption', {
- name: 'logoMargin',
- value: typeof config.logoMargin === 'undefined'
- ? 0
- : config.logoMargin
- })
- copyInstanceOption('logoLeft')
useAuthFlowStore().setInitialStrategy(config.loginMethod)
-
- copyInstanceOption('redirectRootNoLogin')
- copyInstanceOption('redirectRootLogin')
- copyInstanceOption('showInstanceSpecificPanel')
- copyInstanceOption('minimalScopesMode')
- copyInstanceOption('hideMutedPosts')
- copyInstanceOption('collapseMessageWithSubject')
- copyInstanceOption('scopeCopy')
- copyInstanceOption('subjectLineBehavior')
- copyInstanceOption('postContentType')
- copyInstanceOption('alwaysShowSubjectInput')
- copyInstanceOption('showFeaturesPanel')
- copyInstanceOption('hideSitename')
- copyInstanceOption('sidebarRight')
}
const getTOS = async ({ store }) => {
try {
const res = await window.fetch('/static/terms-of-service.html')
if (res.ok) {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load TOS\n", e)
}
}
const getInstancePanel = async ({ store }) => {
try {
const res = await preloadFetch('/instance/panel.html')
if (res.ok) {
const html = await res.text()
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load instance panel\n", e)
}
}
const getStickers = async ({ store }) => {
try {
const res = await window.fetch('/static/stickers.json')
if (res.ok) {
const values = await res.json()
const stickers = (await Promise.all(
Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json')
let meta = {}
if (resPack.ok) {
meta = await resPack.json()
}
return {
pack: name,
path,
meta
}
})
)).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title)
})
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load stickers\n", e)
}
}
const getAppSecret = async ({ store }) => {
const oauth = useOAuthStore()
if (oauth.userToken) {
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
}
}
const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop())
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
}
const getNodeInfo = async ({ store }) => {
try {
let res = await preloadFetch('/nodeinfo/2.1.json')
if (!res.ok) 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: 'shoutAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
store.dispatch('setInstanceOption', {
name: 'pleromaCustomEmojiReactionsAvailable',
value:
features.includes('pleroma_custom_emoji_reactions') ||
features.includes('custom_emoji_reactions')
})
store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
store.dispatch('setInstanceOption', { name: 'blockExpiration', value: features.includes('pleroma:block_expiration') })
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
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 })
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
const software = data.software
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
const federation = metadata.federation
store.dispatch('setInstanceOption', {
name: 'tagPolicyAvailable',
value: typeof federation.mrf_policies === 'undefined'
? false
: metadata.federation.mrf_policies.includes('TagPolicy')
})
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
store.dispatch('setInstanceOption', {
name: 'federating',
value: typeof federation.enabled === 'undefined'
? true
: federation.enabled
})
const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts })
} else {
throw (res)
}
} catch (e) {
console.warn('Could not load nodeinfo')
console.warn(e)
}
}
const setConfig = async ({ store }) => {
// apiConfig, staticConfig
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
const apiConfig = configInfos[0]
const staticConfig = configInfos[1]
getAppSecret({ store })
await setSettings({ store, apiConfig, staticConfig })
}
const checkOAuthToken = async ({ store }) => {
const oauth = useOAuthStore()
if (oauth.getUserToken) {
return store.dispatch('loginUser', oauth.getUserToken)
}
return Promise.resolve()
}
const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
const app = createApp(App)
// Must have app use pinia before we do anything that touches the store
// https://pinia.vuejs.org/core-concepts/plugins.html#Introduction
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
app.use(pinia)
const waitForAllStoresToLoad = async () => {
// the stores that do not persist technically do not need to be awaited here,
// but that involves either hard-coding the stores in some place (prone to errors)
// or writing another vite plugin to analyze which stores needs persisting (++load time)
const allStores = import.meta.glob('../stores/*.js', { eager: true })
if (process.env.NODE_ENV === 'development') {
// do some checks to avoid common errors
if (!Object.keys(allStores).length) {
throw new Error('No stores are available. Check the code in src/boot/after_store.js')
}
}
await Promise.all(
Object.entries(allStores)
.map(async ([name, mod]) => {
const isStoreName = name => name.startsWith('use')
if (process.env.NODE_ENV === 'development') {
if (Object.keys(mod).filter(isStoreName).length !== 1) {
throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/')
}
}
const storeFuncName = Object.keys(mod).find(isStoreName)
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
const p = mod[storeFuncName]().$persistLoaded
if (!(p instanceof Promise)) {
throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`)
}
await p
} else {
throw new Error(`Store module ${name} does not export a 'use...' function`)
}
}))
}
try {
await waitForAllStoresToLoad()
} catch (e) {
console.error('Cannot load stores:', e)
storageError = e
}
if (storageError) {
useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' })
}
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
FaviconService.initFaviconService()
initServiceWorker(store)
window.addEventListener('focus', () => updateFocus())
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 })
try {
await useInterfaceStore().applyTheme().catch((e) => { console.error('Error setting theme', e) })
} catch (e) {
window.splashError(e)
return Promise.reject(e)
}
applyConfig(store.state.config, i18n.global)
// 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 }),
getInstancePanel({ store }),
getNodeInfo({ store }),
getInstanceConfig({ store })
]).catch(e => Promise.reject(e))
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('loadDrafts')
useAnnouncementsStore().startFetchingAnnouncements()
getTOS({ store })
getStickers({ store })
const router = createRouter({
history: createWebHistory(),
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { left: 0, top: 0 }
}
})
useI18nStore().setI18n(i18n)
app.use(router)
app.use(store)
app.use(i18n)
// Little thing to get out of invalid theme state
window.resetThemes = () => {
useInterfaceStore().resetThemeV3()
useInterfaceStore().resetThemeV3Palette()
useInterfaceStore().resetThemeV2()
}
app.use(vClickOutside)
app.use(VBodyScrollLock)
app.use(VueVirtualScroller)
app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers)
// remove after vue 3.3
app.config.unwrapInjectedRef = true
app.mount('#app')
return app
}
export default afterStoreSetup
diff --git a/src/modules/config.js b/src/modules/config.js
index 92898a3de4..7b54225efa 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,186 +1,184 @@
import Cookies from 'js-cookie'
import { applyConfig } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages'
import { set } from 'lodash'
import localeService from '../services/locale/locale.service.js'
import { useI18nStore } from 'src/stores/i18n.js'
import { useInterfaceStore } from 'src/stores/interface.js'
-import { defaultState } from './default_config_state.js'
+import { instanceDefaultConfig, defaultState } from './default_config_state.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
const APPEARANCE_SETTINGS_KEYS = new Set([
'sidebarColumnWidth',
'contentColumnWidth',
'notifsColumnWidth',
'themeEditorMinWidth',
'textSize',
'navbarSize',
'panelHeaderSize',
'forcedRoundness',
'emojiSize',
'emojiReactionsScale'
])
/* TODO this is a bit messy.
* We need to declare settings with their types and also deal with
* instance-default settings in some way, hopefully try to avoid copy-pasta
* in general.
*/
export const multiChoiceProperties = [
'postContentType',
'subjectLineBehavior',
'conversationDisplay', // tree | linear
'conversationOtherRepliesButton', // below | inside
'mentionLinkDisplay', // short | full_for_remote | full
'userPopoverAvatarAction', // close | zoom | open
'unsavedPostAction' // save | discard | confirm
]
// caching the instance default properties
-export const instanceDefaultProperties = Object.entries(defaultState)
- .filter(([, value]) => value === undefined)
- .map(([key]) => key)
+export const instanceDefaultProperties = Object.keys(instanceDefaultConfig)
const config = {
state: { ...defaultState },
getters: {
defaultConfig (state, getters, rootState) {
const { instance } = rootState
return {
...defaultState,
...Object.fromEntries(
instanceDefaultProperties.map(key => [key, instance[key]])
)
}
},
mergedConfig (state, getters, rootState, rootGetters) {
const { defaultConfig } = rootGetters
return {
...defaultConfig,
// Do not override with undefined
...Object.fromEntries(Object.entries(state).filter(([, v]) => v !== undefined))
}
}
},
mutations: {
setOptionTemporarily (state, { name, value }) {
set(state, name, value)
applyConfig(state)
},
setOption (state, { name, value }) {
set(state, name, value)
},
setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user]
if (color || type) {
state.highlight[user] = { color: color || data.color, type: type || data.type }
} else {
delete state.highlight[user]
}
}
},
actions: {
loadSettings ({ dispatch }, data) {
const knownKeys = new Set(Object.keys(defaultState))
const presentKeys = new Set(Object.keys(data))
const intersection = new Set()
for (const elem of presentKeys) {
if (knownKeys.has(elem)) {
intersection.add(elem)
}
}
intersection.forEach(
name => dispatch('setOption', { name, value: data[name] })
)
},
setHighlight ({ commit }, { user, color, type }) {
commit('setHighlight', { user, color, type })
},
setOptionTemporarily ({ commit, dispatch, state }, { name, value }) {
if (useInterfaceStore().temporaryChangesTimeoutId !== null) {
console.warn('Can\'t track more than one temporary change')
return
}
const oldValue = state[name]
commit('setOptionTemporarily', { name, value })
const confirm = () => {
dispatch('setOption', { name, value })
useInterfaceStore().clearTemporaryChanges()
}
const revert = () => {
commit('setOptionTemporarily', { name, value: oldValue })
useInterfaceStore().clearTemporaryChanges()
}
useInterfaceStore().setTemporaryChanges({
timeoutId: setTimeout(revert, 10000),
confirm,
revert
})
},
setThemeV2 ({ commit, dispatch }, { customTheme, customThemeSource }) {
commit('setOption', { name: 'theme', value: 'custom' })
commit('setOption', { name: 'customTheme', value: customTheme })
commit('setOption', { name: 'customThemeSource', value: customThemeSource })
dispatch('setTheme', { themeData: customThemeSource, recompile: true })
},
setOption ({ commit, dispatch, state }, { name, value }) {
const exceptions = new Set([
'useStreamingApi'
])
if (exceptions.has(name)) {
switch (name) {
case 'useStreamingApi': {
const action = value ? 'enableMastoSockets' : 'disableMastoSockets'
dispatch(action).then(() => {
commit('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
dispatch('disableMastoSockets')
dispatch('setOption', { name: 'useStreamingApi', value: false })
})
break
}
}
} else {
commit('setOption', { name, value })
if (APPEARANCE_SETTINGS_KEYS.has(name)) {
applyConfig(state)
}
if (name.startsWith('theme3hacks')) {
dispatch('applyTheme', { recompile: true })
}
switch (name) {
case 'theme':
if (value === 'custom') break
dispatch('setTheme', { themeName: value, recompile: true, saveData: true })
break
case 'themeDebug': {
dispatch('setTheme', { recompile: true })
break
}
case 'interfaceLanguage':
messages.setLanguage(useI18nStore().i18n, value)
dispatch('loadUnicodeEmojiData', value)
Cookies.set(
BACKEND_LANGUAGE_COOKIE_NAME,
localeService.internalToBackendLocaleMulti(value)
)
break
case 'thirdColumnMode':
useInterfaceStore().setLayoutWidth(undefined)
break
}
}
}
}
}
export default config
diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js
index 045cb407af..46cbf9ab5c 100644
--- a/src/modules/default_config_state.js
+++ b/src/modules/default_config_state.js
@@ -1,175 +1,222 @@
const browserLocale = (window.navigator.language || 'en').split('-')[0]
-export const defaultState = {
- expertLevel: 0, // used to track which settings to show and hide
-
- // Theme stuff
- theme: undefined, // Very old theme store, stores preset name, still in use
-
- // V1
- colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
-
- // V2
- customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
- customThemeSource: undefined, // "source", stores original theme data
-
- // V3
- style: null,
- styleCustomData: null,
+/// Instance config entries provided by static config or pleroma api
+/// Put settings here only if it does not make sense for a normal user
+/// to override it.
+export const staticOrApiConfigDefault = {
+ theme: 'pleroma-dark',
palette: null,
- paletteCustomData: null,
- themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
- forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
- theme3hacks: { // Hacks, user overrides that are independent of theme used
- underlay: 'none',
- fonts: {
- interface: undefined,
- input: undefined,
- post: undefined,
- monospace: undefined
- }
- },
+ style: null,
+ defaultAvatar: '/images/avi.png',
+ defaultBanner: '/images/banner.png',
+ background: '/static/aurora_borealis.jpg',
+ embeddedToS: true,
+ logo: '/static/logo.svg',
+ logoMargin: '.2em',
+ logoMask: true,
+ logoLeft: false,
+ redirectRootLogin: '/main/friends',
+ redirectRootNoLogin: '/main/all',
+ hideSitename: false,
+ nsfwCensorImage: undefined,
+ showFeaturesPanel: true,
+ showInstanceSpecificPanel: false,
+}
+/// This object contains setting entries that makes sense
+/// at the user level. The defaults can also be overriden by
+/// instance admins in the frontend_configuration endpoint or static config.
+export const instanceDefaultConfig = {
+ expertLevel: 0, // used to track which settings to show and hide
hideISP: false,
hideInstanceWallpaper: false,
hideShoutbox: false,
// bad name: actually hides posts of muted USERS
- hideMutedPosts: undefined, // instance default
- hideMutedThreads: undefined, // instance default
- hideWordFilteredPosts: undefined, // instance default
- muteBotStatuses: undefined, // instance default
- muteSensitiveStatuses: undefined, // instance default
- collapseMessageWithSubject: undefined, // instance default
+ hideMutedPosts: false,
+ hideMutedThreads: true,
+ hideWordFilteredPosts: false,
+ muteBotStatuses: false,
+ muteSensitiveStatuses: false,
+ collapseMessageWithSubject: false,
padEmoji: true,
hideAttachments: false,
hideAttachmentsInConv: false,
hideScrobbles: false,
hideScrobblesAfter: '2d',
maxThumbnails: 16,
hideNsfw: true,
preloadImage: true,
loopVideo: true,
loopVideoSilentOnly: true,
+ /// This is not the streaming API configuration, but rather an option
+ /// for automatically loading new posts into the timeline without
+ /// the user clicking the Show New button.
streaming: false,
emojiReactionsOnTimeline: true,
alwaysShowNewPostButton: false,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: true,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
notificationVisibility: {
follows: true,
mentions: true,
statuses: true,
likes: true,
repeats: true,
moves: true,
emojiReactions: true,
followRequest: true,
reports: true,
chatMention: true,
polls: true
},
notificationNative: {
follows: true,
mentions: true,
statuses: true,
likes: false,
repeats: false,
moves: false,
emojiReactions: false,
followRequest: true,
reports: true,
chatMention: true,
polls: true
},
webPushNotifications: false,
webPushAlwaysShowNotifications: false,
- muteWords: [],
- highlight: {},
interfaceLanguage: browserLocale,
hideScopeNotice: false,
useStreamingApi: false,
- sidebarRight: undefined, // instance default
- scopeCopy: undefined, // instance default
- subjectLineBehavior: undefined, // instance default
- alwaysShowSubjectInput: undefined, // instance default
- postContentType: undefined, // instance default
- minimalScopesMode: undefined, // instance default
+ sidebarRight: false,
+ scopeCopy: true,
+ subjectLineBehavior: 'email',
+ alwaysShowSubjectInput: true,
+ postContentType: 'text/plain',
+ minimalScopesMode: false,
// This hides statuses filtered via a word filter
- hideFilteredStatuses: undefined, // instance default
+ hideFilteredStatuses: false,
// Confirmations
- modalOnRepeat: undefined, // instance default
- modalOnUnfollow: undefined, // instance default
- modalOnBlock: undefined, // instance default
- modalOnMute: undefined, // instance default
- modalOnMuteConversation: undefined, // instance default
- modalOnMuteDomain: undefined, // instance default
- modalOnDelete: undefined, // instance default
- modalOnLogout: undefined, // instance default
- modalOnApproveFollow: undefined, // instance default
- modalOnDenyFollow: undefined, // instance default
- modalOnRemoveUserFromFollowers: undefined, // instance default
+ modalOnRepeat: false,
+ modalOnUnfollow: false,
+ modalOnBlock: true,
+ modalOnMute: false,
+ modalOnMuteConversation: false,
+ modalOnMuteDomain: true,
+ modalOnDelete: true,
+ modalOnLogout: true,
+ modalOnApproveFollow: false,
+ modalOnDenyFollow: false,
+ modalOnRemoveUserFromFollowers: false,
// Expiry confirmations/default actions
onMuteDefaultAction: 'ask',
onBlockDefaultAction: 'ask',
- modalMobileCenter: undefined,
+ modalMobileCenter: false,
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: true,
disableStickyHeaders: false,
showScrollbars: false,
userPopoverAvatarAction: 'open',
userPopoverOverlay: false,
userCardLeftJustify: false,
userCardHidePersonalMarks: false,
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
- themeEditorMinWidth: undefined, // instance default
- emojiReactionsScale: undefined,
- textSize: undefined, // instance default
- emojiSize: undefined, // instance default
- navbarSize: undefined, // instance default
- panelHeaderSize: undefined, // instance default
- forcedRoundness: undefined, // instance default
+ themeEditorMinWidth: '0rem',
+ emojiReactionsScale: 0.5,
+ textSize: '1rem',
+ emojiSize: '2.2rem',
+ navbarSize: '3.5rem',
+ panelHeaderSize: '3.2rem',
+ forcedRoundness: -1,
navbarColumnStretch: false,
- greentext: undefined, // instance default
- mentionLinkDisplay: undefined, // instance default
- mentionLinkShowTooltip: undefined, // instance default
- mentionLinkShowAvatar: undefined, // instance default
- mentionLinkFadeDomain: undefined, // instance default
- mentionLinkShowYous: undefined, // instance default
- mentionLinkBoldenYou: undefined, // instance default
- hidePostStats: undefined, // instance default
- hideBotIndication: undefined, // instance default
- hideUserStats: undefined, // instance default
- virtualScrolling: undefined, // instance default
- sensitiveByDefault: undefined, // instance default
- conversationDisplay: undefined, // instance default
- conversationTreeAdvanced: undefined, // instance default
- conversationOtherRepliesButton: undefined, // instance default
- conversationTreeFadeAncestors: undefined, // instance default
- showExtraNotifications: undefined, // instance default
- showExtraNotificationsTip: undefined, // instance default
- showChatsInExtraNotifications: undefined, // instance default
- showAnnouncementsInExtraNotifications: undefined, // instance default
- showFollowRequestsInExtraNotifications: undefined, // instance default
- maxDepthInThread: undefined, // instance default
- autocompleteSelect: undefined, // instance default
- closingDrawerMarksAsSeen: undefined, // instance default
- unseenAtTop: undefined, // instance default
- ignoreInactionableSeen: undefined, // instance default
- unsavedPostAction: undefined, // instance default
- autoSaveDraft: undefined, // instance default
- useAbsoluteTimeFormat: undefined, // instance default
- absoluteTimeFormatMinAge: undefined, // instance default
- absoluteTime12h: undefined, // instance default
+ greentext: false,
+ mentionLinkDisplay: 'short',
+ mentionLinkShowTooltip: true,
+ mentionLinkShowAvatar: false,
+ mentionLinkFadeDomain: true,
+ mentionLinkShowYous: false,
+ mentionLinkBoldenYou: true,
+ hidePostStats: false,
+ hideBotIndication: false,
+ hideUserStats: false,
+ virtualScrolling: true,
+ sensitiveByDefault: false,
+ conversationDisplay: 'linear',
+ conversationTreeAdvanced: false,
+ conversationOtherRepliesButton: 'below',
+ conversationTreeFadeAncestors: false,
+ showExtraNotifications: true,
+ showExtraNotificationsTip: true,
+ showChatsInExtraNotifications: true,
+ showAnnouncementsInExtraNotifications: true,
+ showFollowRequestsInExtraNotifications: true,
+ maxDepthInThread: 6,
+ autocompleteSelect: false,
+ closingDrawerMarksAsSeen: true,
+ unseenAtTop: false,
+ ignoreInactionableSeen: false,
+ unsavedPostAction: 'confirm',
+ autoSaveDraft: false,
+ useAbsoluteTimeFormat: false,
+ absoluteTimeFormatMinAge: '0d',
+ absoluteTime12h: '24h',
imageCompression: true,
alwaysUseJpeg: false
}
+
+export const makeUndefined = c => Object.fromEntries(Object.keys(c).map(key => [key, undefined]))
+
+/// For properties with special processing or properties that does not
+/// make sense to be overriden on a instance-wide level.
+export const defaultState = {
+ // Set these to undefined so it does not interfere with default settings check
+ ...makeUndefined(instanceDefaultConfig),
+
+ // Special processing
+ // Theme stuff
+ theme: undefined, // Very old theme store, stores preset name, still in use
+
+ // V1
+ colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
+
+ // V2
+ customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
+ customThemeSource: undefined, // "source", stores original theme data
+
+ // V3
+ style: null,
+ styleCustomData: null,
+ palette: null,
+ paletteCustomData: null,
+ themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
+ forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
+ theme3hacks: { // Hacks, user overrides that are independent of theme used
+ underlay: 'none',
+ fonts: {
+ interface: undefined,
+ input: undefined,
+ post: undefined,
+ monospace: undefined
+ }
+ },
+
+ // Special handling: These fields are not of a primitive type, and
+ // might cause problems with current code because it specifically checks
+ // them in state.config (not getters.mergedConfig).
+
+ // Specifically, muteWords is now deprecated in favour of a server-side configuration.
+ muteWords: [],
+ highlight: {},
+
+ // If there are any configurations that does not make sense to
+ // have instance-wide default, put it here and explain why.
+}
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 7317debf81..1595dc6a4b 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -1,420 +1,346 @@
import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
import { ensureFinalFallback } from '../i18n/languages.js'
import { useInterfaceStore } from 'src/stores/interface.js'
// See build/emojis_plugin for more details
import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations'
+import { staticOrApiConfigDefault, instanceDefaultConfig } from './default_config_state.js';
const SORTED_EMOJI_GROUP_IDS = [
'smileys-and-emotion',
'people-and-body',
'animals-and-nature',
'food-and-drink',
'travel-and-places',
'activities',
'objects',
'symbols',
'flags'
]
const REGIONAL_INDICATORS = (() => {
const start = 0x1F1E6
const end = 0x1F1FF
const A = 'A'.codePointAt(0)
const res = new Array(end - start + 1)
for (let i = start; i <= end; ++i) {
const letter = String.fromCodePoint(A + i - start)
res[i - start] = {
replacement: String.fromCodePoint(i),
imageUrl: false,
displayText: 'regional_indicator_' + letter,
displayTextI18n: {
key: 'emoji.regional_indicator',
args: { letter }
}
}
}
return res
})()
const REMOTE_INTERACTION_URL = '/main/ostatus'
const defaultState = {
// Stuff from apiConfig
name: 'Pleroma FE',
registrationOpen: true,
server: 'http://localhost:4040/',
textlimit: 5000,
themesIndex: undefined,
stylesIndex: undefined,
palettesIndex: undefined,
themeData: undefined, // used for theme editor v2
vapidPublicKey: undefined,
// Stuff from static/config.json
- alwaysShowSubjectInput: true,
- defaultAvatar: '/images/avi.png',
- defaultBanner: '/images/banner.png',
- background: '/static/aurora_borealis.jpg',
- embeddedToS: true,
- collapseMessageWithSubject: false,
- greentext: false,
- mentionLinkDisplay: 'short',
- mentionLinkShowTooltip: true,
- mentionLinkShowAvatar: false,
- mentionLinkFadeDomain: true,
- mentionLinkShowYous: false,
- mentionLinkBoldenYou: true,
- hideFilteredStatuses: false,
- // bad name: actually hides posts of muted USERS
- hideMutedPosts: false,
- hideMutedThreads: true,
- hideWordFilteredPosts: false,
- hidePostStats: false,
- hideBotIndication: false,
- hideSitename: false,
- hideUserStats: false,
- muteBotStatuses: false,
- muteSensitiveStatuses: false,
- modalOnRepeat: false,
- modalOnUnfollow: false,
- modalOnBlock: true,
- modalOnMute: false,
- modalOnMuteConversation: false,
- modalOnMuteDomain: true,
- modalOnDelete: true,
- modalOnLogout: true,
- modalOnApproveFollow: false,
- modalOnDenyFollow: false,
- modalOnRemoveUserFromFollowers: false,
- modalMobileCenter: false,
loginMethod: 'password',
- logo: '/static/logo.svg',
- logoMargin: '.2em',
- logoMask: true,
- logoLeft: false,
disableUpdateNotification: false,
- minimalScopesMode: false,
- nsfwCensorImage: undefined,
- postContentType: 'text/plain',
- redirectRootLogin: '/main/friends',
- redirectRootNoLogin: '/main/all',
- scopeCopy: true,
- showFeaturesPanel: true,
- showInstanceSpecificPanel: false,
- sidebarRight: false,
- subjectLineBehavior: 'email',
- theme: 'pleroma-dark',
- palette: null,
- style: null,
- emojiReactionsScale: 0.5,
- textSize: '1rem',
- emojiSize: '2.2rem',
- navbarSize: '3.5rem',
- panelHeaderSize: '3.2rem',
- themeEditorMinWidth: '0rem',
- forcedRoundness: -1,
+
fontsOverride: {},
- virtualScrolling: true,
- sensitiveByDefault: false,
- conversationDisplay: 'linear',
- conversationTreeAdvanced: false,
- conversationOtherRepliesButton: 'below',
- conversationTreeFadeAncestors: false,
- showExtraNotifications: true,
- showExtraNotificationsTip: true,
- showChatsInExtraNotifications: true,
- showAnnouncementsInExtraNotifications: true,
- showFollowRequestsInExtraNotifications: true,
- maxDepthInThread: 6,
- autocompleteSelect: false,
- closingDrawerMarksAsSeen: true,
- unseenAtTop: false,
- ignoreInactionableSeen: false,
- unsavedPostAction: 'confirm',
- autoSaveDraft: false,
- useAbsoluteTimeFormat: false,
- absoluteTimeFormatMinAge: '0d',
- absoluteTime12h: '24h',
+
+ // Instance-wide configurations that should not be changed by individual users
+ ...staticOrApiConfigDefault,
+ // Instance admins can override default settings for the whole instance
+ ...instanceDefaultConfig,
// Nasty stuff
customEmoji: [],
customEmojiFetched: false,
emoji: {},
emojiFetched: false,
unicodeEmojiAnnotations: {},
pleromaExtensionsAvailable: true,
postFormats: [],
restrictedNicknames: [],
safeDM: true,
knownDomains: [],
birthdayRequired: false,
birthdayMinAge: 0,
// Feature-set, apparently, not everything here is reported...
shoutAvailable: false,
pleromaChatMessagesAvailable: false,
pleromaCustomEmojiReactionsAvailable: false,
pleromaBookmarkFoldersAvailable: false,
pleromaPublicFavouritesAvailable: true,
statusNotificationTypeAvailable: true,
gopherAvailable: false,
mediaProxyAvailable: false,
suggestionsEnabled: false,
suggestionsWeb: '',
quotingAvailable: false,
groupActorAvailable: false,
blockExpiration: false,
localBubbleInstances: [], // Akkoma
// Html stuff
instanceSpecificPanelContent: '',
tos: '',
// Version Information
backendVersion: '',
backendRepository: '',
frontendVersion: '',
pollsAvailable: false,
pollLimits: {
max_options: 4,
max_option_chars: 255,
min_expiration: 60,
max_expiration: 60 * 60 * 24
}
}
const loadAnnotations = (lang) => {
return annotationsLoader[lang]()
.then(k => k.default)
}
const injectAnnotations = (emoji, annotations) => {
const availableLangs = Object.keys(annotations)
return {
...emoji,
annotations: availableLangs.reduce((acc, cur) => {
acc[cur] = annotations[cur][emoji.replacement]
return acc
}, {})
}
}
const injectRegionalIndicators = groups => {
groups.symbols.push(...REGIONAL_INDICATORS)
return groups
}
const instance = {
state: defaultState,
mutations: {
setInstanceOption (state, { name, value }) {
if (typeof value !== 'undefined') {
state[name] = value
}
},
setKnownDomains (state, domains) {
state.knownDomains = domains
},
setUnicodeEmojiAnnotations (state, { lang, annotations }) {
state.unicodeEmojiAnnotations[lang] = annotations
}
},
getters: {
instanceDefaultConfig (state) {
return instanceDefaultProperties
.map(key => [key, state[key]])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
},
groupedCustomEmojis (state) {
const packsOf = emoji => {
const packs = emoji.tags
.filter(k => k.startsWith('pack:'))
.map(k => {
const packName = k.slice(5) // remove 'pack:' prefix
return {
id: `custom-${packName}`,
text: packName
}
})
if (!packs.length) {
return [{
id: 'unpacked'
}]
} else {
return packs
}
}
return state.customEmoji
.reduce((res, emoji) => {
packsOf(emoji).forEach(({ id: packId, text: packName }) => {
if (!res[packId]) {
res[packId] = ({
id: packId,
text: packName,
image: emoji.imageUrl,
emojis: []
})
}
res[packId].emojis.push(emoji)
})
return res
}, {})
},
standardEmojiList (state) {
return SORTED_EMOJI_GROUP_IDS
.map(groupId => (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations)))
.reduce((a, b) => a.concat(b), [])
},
standardEmojiGroupList (state) {
return SORTED_EMOJI_GROUP_IDS.map(groupId => ({
id: groupId,
emojis: (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations))
}))
},
instanceDomain (state) {
return new URL(state.server).hostname
},
remoteInteractionLink (state) {
const server = state.server.endsWith('/') ? state.server.slice(0, -1) : state.server
const link = server + REMOTE_INTERACTION_URL
return ({ statusId, nickname }) => {
if (statusId) {
return `${link}?status_id=${statusId}`
} else {
return `${link}?nickname=${nickname}`
}
}
}
},
actions: {
setInstanceOption ({ commit, dispatch }, { name, value }) {
commit('setInstanceOption', { name, value })
switch (name) {
case 'name':
useInterfaceStore().setPageTitle()
break
case 'shoutAvailable':
if (value) {
dispatch('initializeSocket')
}
break
}
},
async getStaticEmoji ({ commit }) {
try {
const values = (await import('/src/assets/emoji.json')).default
const emoji = Object.keys(values).reduce((res, groupId) => {
res[groupId] = values[groupId].map(e => ({
displayText: e.slug,
imageUrl: false,
replacement: e.emoji
}))
return res
}, {})
commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) })
} catch (e) {
console.warn("Can't load static emoji\n", e)
}
},
loadUnicodeEmojiData ({ commit, state }, language) {
const langList = ensureFinalFallback(language)
return Promise.all(
langList
.map(async lang => {
if (!state.unicodeEmojiAnnotations[lang]) {
try {
const annotations = await loadAnnotations(lang)
commit('setUnicodeEmojiAnnotations', { lang, annotations })
} catch (e) {
console.warn(`Error loading unicode emoji annotations for ${lang}: `, e)
// ignore
}
}
}))
},
async getCustomEmoji ({ commit, state }) {
try {
let res = await window.fetch('/api/v1/pleroma/emoji')
if (!res.ok) {
res = await window.fetch('/api/pleroma/emoji.json')
}
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
const caseInsensitiveStrCmp = (a, b) => {
const la = a.toLowerCase()
const lb = b.toLowerCase()
return la > lb ? 1 : (la < lb ? -1 : 0)
}
const noPackLast = (a, b) => {
const aNull = a === ''
const bNull = b === ''
if (aNull === bNull) {
return 0
} else if (aNull && !bNull) {
return 1
} else {
return -1
}
}
const byPackThenByName = (a, b) => {
const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
const packOfA = packOf(a)
const packOfB = packOf(b)
return noPackLast(packOfA, packOfB) || caseInsensitiveStrCmp(packOfA, packOfB) || caseInsensitiveStrCmp(a.displayText, b.displayText)
}
const emoji = Object.entries(values).map(([key, value]) => {
const imageUrl = value.image_url
return {
displayText: key,
imageUrl: imageUrl ? state.server + imageUrl : value,
tags: imageUrl ? value.tags.sort((a, b) => a > b ? 1 : 0) : ['utf'],
replacement: `:${key}: `
}
// Technically could use tags but those are kinda useless right now,
// should have been "pack" field, that would be more useful
}).sort(byPackThenByName)
commit('setInstanceOption', { name: 'customEmoji', value: emoji })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load custom emojis\n", e)
}
},
fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) {
state.customEmojiFetched = true
dispatch('getCustomEmoji')
}
if (!state.emojiFetched) {
state.emojiFetched = true
dispatch('getStaticEmoji')
}
},
async getKnownDomains ({ commit, rootState }) {
try {
const result = await apiService.fetchKnownDomains({
credentials: rootState.users.currentUser.credentials
})
commit('setKnownDomains', result)
} catch (e) {
console.warn("Can't load known domains\n", e)
}
}
}
}
export default instance

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 29, 2:50 PM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1798221
Default Alt Text
(49 KB)

Event Timeline