Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85824355
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
49 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline