some initial work on moving instance settings to pinia

This commit is contained in:
Henry Jameson 2026-01-22 00:22:18 +02:00
commit 9452b3084a
10 changed files with 559 additions and 195 deletions

View file

@ -22,6 +22,7 @@ import UserReportingModal from './components/user_reporting_modal/user_reporting
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import { getOrCreateServiceWorker } from './services/sw/sw' import { getOrCreateServiceWorker } from './services/sw/sw'
import { windowHeight, windowWidth } from './services/window_utils/window_utils' import { windowHeight, windowWidth } from './services/window_utils/window_utils'
import { useInstanceStore } from './stores/instance'
import { useInterfaceStore } from './stores/interface' import { useInterfaceStore } from './stores/interface'
import { useShoutStore } from './stores/shout' import { useShoutStore } from './stores/shout'
@ -135,11 +136,6 @@ export default {
userBackground() { userBackground() {
return this.currentUser.background_image return this.currentUser.background_image
}, },
instanceBackground() {
return this.mergedConfig.hideInstanceWallpaper
? null
: this.$store.state.instance.background
},
background() { background() {
return this.userBackground || this.instanceBackground return this.userBackground || this.instanceBackground
}, },
@ -153,16 +149,6 @@ export default {
shout() { shout() {
return useShoutStore().joined return useShoutStore().joined
}, },
suggestionsEnabled() {
return this.$store.state.instance.suggestionsEnabled
},
showInstanceSpecificPanel() {
return (
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
)
},
isChats() { isChats() {
return this.$route.name === 'chat' || this.$route.name === 'chats' return this.$route.name === 'chat' || this.$route.name === 'chats'
}, },
@ -177,21 +163,12 @@ export default {
this.layoutType === 'mobile' this.layoutType === 'mobile'
) )
}, },
showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
editingAvailable() {
return this.$store.state.instance.editingAvailable
},
shoutboxPosition() { shoutboxPosition() {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
}, },
layoutType() { layoutType() {
return useInterfaceStore().layoutType return useInterfaceStore().layoutType
}, },
privateMode() {
return this.$store.state.instance.private
},
reverseLayout() { reverseLayout() {
const { thirdColumnMode, sidebarRight: reverseSetting } = const { thirdColumnMode, sidebarRight: reverseSetting } =
this.$store.getters.mergedConfig this.$store.getters.mergedConfig
@ -214,8 +191,22 @@ export default {
}, },
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState(useServerSideStorageStore, { ...mapState(useServerSideStorageStore, {
hideShoutbox: (store) => store.prefsStorage.simple.hideShoutbox, hideShoutbox: (store) => store.mergedConfig.hideShoutbox,
}), }),
...mapState(useInstanceStore, {
instanceBackground: (store) =>
this.mergedConfig.hideInstanceWallpaper ? null : store.background,
showInstanceSpecificPanel: (store) =>
store.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
store.instanceSpecificPanelContent,
}),
...mapState(useInstanceStore, [
'editingAvailable',
'showFeaturesPanel',
'private',
'suggestionsEnabled',
]),
}, },
methods: { methods: {
resizeHandler() { resizeHandler() {

View file

@ -1,6 +1,7 @@
/* global process */ /* global process */
import vClickOutside from 'click-outside-vue3' import vClickOutside from 'click-outside-vue3'
import { get, set } from 'lodash'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import VueVirtualScroller from 'vue-virtual-scroller' import VueVirtualScroller from 'vue-virtual-scroller'
@ -22,6 +23,7 @@ import {
import { useAnnouncementsStore } from 'src/stores/announcements' import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow' import { useAuthFlowStore } from 'src/stores/auth_flow'
import { useI18nStore } from 'src/stores/i18n' import { useI18nStore } from 'src/stores/i18n'
import { useInstanceStore } from 'src/stores/instance'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import { useOAuthStore } from 'src/stores/oauth' import { useOAuthStore } from 'src/stores/oauth'
import App from '../App.vue' import App from '../App.vue'
@ -78,30 +80,30 @@ const getInstanceConfig = async ({ store }) => {
const textlimit = data.max_toot_chars const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'pleromaExtensionsAvailable', path: 'featureSet.pleromaExtensionsAvailable',
value: data.pleroma, value: data.pleroma,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'textlimit', path: 'textlimit',
value: textlimit, value: textlimit,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'accountApprovalRequired', path: 'accountApprovalRequired',
value: data.approval_required, value: data.approval_required,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'birthdayRequired', path: 'birthdayRequired',
value: !!data.pleroma?.metadata.birthday_required, value: !!data.pleroma?.metadata.birthday_required,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'birthdayMinAge', path: 'birthdayMinAge',
value: data.pleroma?.metadata.birthday_min_age || 0, value: data.pleroma?.metadata.birthday_min_age || 0,
}) })
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'vapidPublicKey', path: 'vapidPublicKey',
value: vapidPublicKey, value: vapidPublicKey,
}) })
} }
@ -156,19 +158,32 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
let config = {} let config = {}
if (overrides.staticConfigPreference && env === 'development') { if (overrides.staticConfigPreference && env === 'development') {
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG') console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
config = Object.assign({}, apiConfig, staticConfig) config = { ...apiConfig, ...staticConfig }
} else { } else {
config = Object.assign({}, staticConfig, apiConfig) config = { ...staticConfig, ...apiConfig }
} }
console.trace(config)
const copyInstanceOption = (name) => { const copyInstanceIdentityOption = (path) => {
if (typeof config[name] !== 'undefined') { if (get(config, path) !== undefined) {
store.dispatch('setInstanceOption', { name, value: config[name] }) useInstanceStore().set({
path: `instanceIdentity.${path}`,
value: get(config, path),
})
} }
} }
Object.keys(staticOrApiConfigDefault).forEach(copyInstanceOption) const copyInstancePrefOption = (path) => {
Object.keys(instanceDefaultConfig).forEach(copyInstanceOption) if (get(config, path) !== undefined) {
useInstanceStore().set({
path: `prefsStorage.${path}`,
value: get(config, path),
})
}
}
Object.keys(staticOrApiConfigDefault).forEach(copyInstanceIdentityOption)
Object.keys(instanceDefaultConfig).forEach(copyInstancePrefOption)
useAuthFlowStore().setInitialStrategy(config.loginMethod) useAuthFlowStore().setInitialStrategy(config.loginMethod)
} }
@ -178,7 +193,7 @@ const getTOS = async ({ store }) => {
const res = await window.fetch('/static/terms-of-service.html') const res = await window.fetch('/static/terms-of-service.html')
if (res.ok) { if (res.ok) {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html }) useInstanceStore().set({ path: 'tos', value: html })
} else { } else {
throw res throw res
} }
@ -192,8 +207,8 @@ const getInstancePanel = async ({ store }) => {
const res = await preloadFetch('/instance/panel.html') const res = await preloadFetch('/instance/panel.html')
if (res.ok) { if (res.ok) {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'instanceSpecificPanelContent', path: 'instanceSpecificPanelContent',
value: html, value: html,
}) })
} else { } else {
@ -227,7 +242,7 @@ const getStickers = async ({ store }) => {
).sort((a, b) => { ).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title) return a.meta.title.localeCompare(b.meta.title)
}) })
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) useInstanceStore().set({ path: 'stickers', value: stickers })
} else { } else {
throw res throw res
} }
@ -248,8 +263,8 @@ const getAppSecret = async ({ store }) => {
const resolveStaffAccounts = ({ store, accounts }) => { const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map((uri) => uri.split('/').pop()) const nicknames = accounts.map((uri) => uri.split('/').pop())
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'staffAccounts', path: 'staffAccounts',
value: nicknames, value: nicknames,
}) })
} }
@ -262,160 +277,160 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
const features = metadata.features const features = metadata.features
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'name', path: 'name',
value: metadata.nodeName, value: metadata.nodeName,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'registrationOpen', path: 'registrationOpen',
value: data.openRegistrations, value: data.openRegistrations,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'mediaProxyAvailable', path: 'featureSet.mediaProxyAvailable',
value: features.includes('media_proxy'), value: features.includes('media_proxy'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'safeDM', path: 'featureSet.safeDM',
value: features.includes('safe_dm_mentions'), value: features.includes('safe_dm_mentions'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'shoutAvailable', path: 'featureSet.shoutAvailable',
value: features.includes('chat'), value: features.includes('chat'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'pleromaChatMessagesAvailable', path: 'featureSet.pleromaChatMessagesAvailable',
value: features.includes('pleroma_chat_messages'), value: features.includes('pleroma_chat_messages'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'pleromaCustomEmojiReactionsAvailable', path: 'featureSet.pleromaCustomEmojiReactionsAvailable',
value: value:
features.includes('pleroma_custom_emoji_reactions') || features.includes('pleroma_custom_emoji_reactions') ||
features.includes('custom_emoji_reactions'), features.includes('custom_emoji_reactions'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'pleromaBookmarkFoldersAvailable', path: 'featureSet.pleromaBookmarkFoldersAvailable',
value: features.includes('pleroma:bookmark_folders'), value: features.includes('pleroma:bookmark_folders'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'gopherAvailable', path: 'featureSet.gopherAvailable',
value: features.includes('gopher'), value: features.includes('gopher'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'pollsAvailable', path: 'featureSet.pollsAvailable',
value: features.includes('polls'), value: features.includes('polls'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'editingAvailable', path: 'featureSet.editingAvailable',
value: features.includes('editing'), value: features.includes('editing'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'pollLimits', path: 'pollLimits',
value: metadata.pollLimits, value: metadata.pollLimits,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'mailerEnabled', path: 'featureSet.mailerEnabled',
value: metadata.mailerEnabled, value: metadata.mailerEnabled,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'quotingAvailable', path: 'featureSet.quotingAvailable',
value: features.includes('quote_posting'), value: features.includes('quote_posting'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'groupActorAvailable', path: 'featureSet.groupActorAvailable',
value: features.includes('pleroma:group_actors'), value: features.includes('pleroma:group_actors'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'blockExpiration', path: 'featureSet.blockExpiration',
value: features.includes('pleroma:block_expiration'), value: features.includes('pleroma:block_expiration'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'localBubbleInstances', path: 'featureSet.localBubbleInstances',
value: metadata.localBubbleInstances ?? [], value: metadata.localBubbleInstances ?? [],
}) })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'uploadlimit', path: 'uploadlimit',
value: parseInt(uploadLimits.general), value: parseInt(uploadLimits.general),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'avatarlimit', path: 'avatarlimit',
value: parseInt(uploadLimits.avatar), value: parseInt(uploadLimits.avatar),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'backgroundlimit', path: 'backgroundlimit',
value: parseInt(uploadLimits.background), value: parseInt(uploadLimits.background),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'bannerlimit', path: 'bannerlimit',
value: parseInt(uploadLimits.banner), value: parseInt(uploadLimits.banner),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'fieldsLimits', path: 'fieldsLimits',
value: metadata.fieldsLimits, value: metadata.fieldsLimits,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'restrictedNicknames', path: 'restrictedNicknames',
value: metadata.restrictedNicknames, value: metadata.restrictedNicknames,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'postFormats', path: 'featureSet.postFormats',
value: metadata.postFormats, value: metadata.postFormats,
}) })
const suggestions = metadata.suggestions const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'suggestionsEnabled', path: 'featureSet.suggestionsEnabled',
value: suggestions.enabled, value: suggestions.enabled,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'suggestionsWeb', path: 'featureSet.suggestionsWeb',
value: suggestions.web, value: suggestions.web,
}) })
const software = data.software const software = data.software
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'backendVersion', path: 'backendVersion',
value: software.version, value: software.version,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'backendRepository', path: 'backendRepository',
value: software.repository, value: software.repository,
}) })
const priv = metadata.private const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv }) useInstanceStore().set({ path: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'frontendVersion', path: 'frontendVersion',
value: frontendVersion, value: frontendVersion,
}) })
const federation = metadata.federation const federation = metadata.federation
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'tagPolicyAvailable', path: 'featureSet.tagPolicyAvailable',
value: value:
typeof federation.mrf_policies === 'undefined' typeof federation.mrf_policies === 'undefined'
? false ? false
: metadata.federation.mrf_policies.includes('TagPolicy'), : metadata.federation.mrf_policies.includes('TagPolicy'),
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'federationPolicy', path: 'federationPolicy',
value: federation, value: federation,
}) })
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'federating', path: 'federating',
value: value:
typeof federation.enabled === 'undefined' ? true : federation.enabled, typeof federation.enabled === 'undefined' ? true : federation.enabled,
}) })
const accountActivationRequired = metadata.accountActivationRequired const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { useInstanceStore().set({
name: 'accountActivationRequired', path: 'accountActivationRequired',
value: accountActivationRequired, value: accountActivationRequired,
}) })
@ -526,7 +541,7 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
typeof overrides.target !== 'undefined' typeof overrides.target !== 'undefined'
? overrides.target ? overrides.target
: window.location.origin : window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server }) useInstanceStore().set({ path: 'server', value: server })
await setConfig({ store }) await setConfig({ store })
try { try {

View file

@ -299,13 +299,6 @@ const PostStatusForm = {
isOverLengthLimit() { isOverLengthLimit() {
return this.hasStatusLengthLimit && this.charactersLeft < 0 return this.hasStatusLengthLimit && this.charactersLeft < 0
}, },
minimalScopesMode() {
return useServerSideStorageStore().prefsStorage.simple.minimalScopesMode
},
alwaysShowSubject() {
return useServerSideStorageStore().prefsStorage.simple
.alwaysShowSubjectInput
},
postFormats() { postFormats() {
return this.$store.state.instance.postFormats || [] return this.$store.state.instance.postFormats || []
}, },
@ -412,6 +405,10 @@ const PostStatusForm = {
...mapState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (store) => store.mobileLayout, mobileLayout: (store) => store.mobileLayout,
}), }),
...mapState(useServerSideStorageStore, {
minimalScopesMode: (store) => store.mergedConfig.minimalScopesMode,
alwaysShowSubject: (store) => store.mergedConfig.alwaysShowSubjectInput,
}),
}, },
watch: { watch: {
newStatus: { newStatus: {

View file

@ -1,5 +1,6 @@
import { cloneDeep, get, isEqual, set } from 'lodash' import { cloneDeep, get, isEqual, set } from 'lodash'
import { useInstanceStore } from 'src/stores/instance'
import { useServerSideStorageStore } from 'src/stores/serverSideStorage' import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
import DraftButtons from './draft_buttons.vue' import DraftButtons from './draft_buttons.vue'
import ModifiedIndicator from './modified_indicator.vue' import ModifiedIndicator from './modified_indicator.vue'
@ -228,7 +229,7 @@ export default {
configSource() { configSource() {
switch (this.realSource) { switch (this.realSource) {
case 'server-side': case 'server-side':
return useServerSideStorageStore().prefsStorage return useServerSideStorageStore().mergedConfig
case 'profile': case 'profile':
return this.$store.state.profileConfig return this.$store.state.profileConfig
case 'admin': case 'admin':
@ -243,36 +244,10 @@ export default {
} }
switch (this.realSource) { switch (this.realSource) {
case 'server-side': { case 'server-side': {
return (path, value, operator) => { return (originalPath, value, operator) => {
const folder = path.split('.')[0] const path = `simple.${originalPath}`
if (folder === 'collections' || folder === 'objectCollections') { useServerSideStorageStore().setPreference({ path, value })
switch (operator) { useServerSideStorageStore().pushServerSideStorage()
case 'add':
useServerSideStorageStore().addCollectionPreference({
path,
value,
})
useServerSideStorageStore().pushServerSideStorage()
break
case 'remove':
useServerSideStorageStore().removeCollectionPreference({
path,
value,
})
useServerSideStorageStore().pushServerSideStorage()
break
default:
console.error(
`Unknown server-side collection operator ${operator}, ignoring`,
)
break
}
} else if (folder === 'simple') {
useServerSideStorageStore().setPreference({ path, value })
useServerSideStorageStore().pushServerSideStorage()
} else {
console.error(`Unknown server-side folder ${folder}, ignoring`)
}
} }
} }
case 'profile': case 'profile':
@ -299,10 +274,7 @@ export default {
case 'profile': case 'profile':
return {} return {}
case 'server-side': case 'server-side':
return get( return get(useInstanceStore().prefsStorage, this.path)
this.$store.getters.defaultConfig,
this.path.split(/\./g).slice(1),
)
default: default:
return get(this.$store.getters.defaultConfig, this.path) return get(this.$store.getters.defaultConfig, this.path)
} }

View file

@ -6,7 +6,7 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.alwaysShowSubjectInput" path="alwaysShowSubjectInput"
> >
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.subject_input_always_show') }}
</BooleanSetting> </BooleanSetting>
@ -14,7 +14,7 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.minimalScopesMode" path="minimalScopesMode"
> >
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting> </BooleanSetting>
@ -22,14 +22,14 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.hidePostStats" path="hidePostStats"
> >
{{ $t('settings.hide_post_stats') }} {{ $t('settings.hide_post_stats') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<BooleanSetting <BooleanSetting
path="simple.hideUserStats" path="hideUserStats"
source="server-side" source="server-side"
> >
{{ $t('settings.hide_user_stats') }} {{ $t('settings.hide_user_stats') }}
@ -38,7 +38,7 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.hideBotIndication" path="hideBotIndication"
> >
{{ $t('settings.hide_actor_type_indication') }} {{ $t('settings.hide_actor_type_indication') }}
</BooleanSetting> </BooleanSetting>
@ -46,7 +46,7 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.hideScrobbles" path="hideScrobbles"
> >
{{ $t('settings.hide_scrobbles') }} {{ $t('settings.hide_scrobbles') }}
</BooleanSetting> </BooleanSetting>
@ -55,7 +55,7 @@
<UnitSetting <UnitSetting
key="hideScrobblesAfter" key="hideScrobblesAfter"
source="server-side" source="server-side"
path="simple.hideScrobblesAfter" path="hideScrobblesAfter"
:units="['m', 'h', 'd']" :units="['m', 'h', 'd']"
unit-set="time" unit-set="time"
> >
@ -70,7 +70,7 @@
<li> <li>
<IntegerSetting <IntegerSetting
source="server-side" source="server-side"
path="simple.maxThumbnails" path="maxThumbnails"
:min="0" :min="0"
> >
{{ $t('settings.max_thumbnails') }} {{ $t('settings.max_thumbnails') }}
@ -79,7 +79,7 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.hideAttachments" path="hideAttachments"
> >
{{ $t('settings.hide_attachments_in_tl') }} {{ $t('settings.hide_attachments_in_tl') }}
</BooleanSetting> </BooleanSetting>
@ -87,7 +87,7 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.hideAttachmentsInConv" path="hideAttachmentsInConv"
> >
{{ $t('settings.hide_attachments_in_convo') }} {{ $t('settings.hide_attachments_in_convo') }}
</BooleanSetting> </BooleanSetting>
@ -95,7 +95,7 @@
<li> <li>
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.userCardHidePersonalMarks" path="userCardHidePersonalMarks"
> >
{{ $t('settings.user_card_hide_personal_marks') }} {{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting> </BooleanSetting>
@ -103,7 +103,7 @@
<li v-if="instanceShoutboxPresent"> <li v-if="instanceShoutboxPresent">
<BooleanSetting <BooleanSetting
source="server-side" source="server-side"
path="simple.hideShoutbox" path="hideShoutbox"
> >
{{ $t('settings.hide_shoutbox') }} {{ $t('settings.hide_shoutbox') }}
</BooleanSetting> </BooleanSetting>

View file

@ -125,7 +125,7 @@ export default {
} }
}, },
created() { created() {
const currentIndex = this.$store.state.instance.themesIndex const currentIndex = this.$store.state.instance.instanceThemesIndex
let promise let promise
if (currentIndex) { if (currentIndex) {
@ -134,8 +134,8 @@ export default {
promise = useInterfaceStore().fetchThemesIndex() promise = useInterfaceStore().fetchThemesIndex()
} }
promise.then((themesIndex) => { promise.then((instanceThemesIndex) => {
Object.values(themesIndex).forEach((themeFunc) => { Object.values(instanceThemesIndex).forEach((themeFunc) => {
themeFunc().then( themeFunc().then(
(themeData) => themeData && this.availableStyles.push(themeData), (themeData) => themeData && this.availableStyles.push(themeData),
) )

View file

@ -532,10 +532,9 @@ const Status = {
...mapState(useServerSideStorageStore, { ...mapState(useServerSideStorageStore, {
muteFilters: (store) => store.prefsStorage.simple.muteFilters, muteFilters: (store) => store.prefsStorage.simple.muteFilters,
hideBotIndicatior: (store) => store.prefsStorage.simple.hideBotIndicator, hideBotIndicatior: (store) => store.prefsStorage.simple.hideBotIndicator,
hidePostStats: (store) => store.prefsStorage.simple.hidePostStats, hidePostStats: (store) => store.mergedConfig.hidePostStats,
hideScrobbles: (store) => store.prefsStorage.simple.hideScrobbles, hideScrobbles: (store) => store.mergedConfig.hideScrobbles,
hideScrobblesAfter: (store) => hideScrobblesAfter: (store) => store.mergedConfig.hideScrobblesAfter,
store.prefsStorage.simple.hideScrobblesAfter,
}), }),
}, },
methods: { methods: {

View file

@ -93,7 +93,7 @@ export const instanceDefaultConfig = {
sidebarRight: false, sidebarRight: false,
scopeCopy: true, scopeCopy: true,
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
alwaysShowSubjectInput: true, alwaysShowSubjectInput: false,
postContentType: 'text/plain', postContentType: 'text/plain',
minimalScopesMode: false, minimalScopesMode: false,

387
src/stores/instance.js Normal file
View file

@ -0,0 +1,387 @@
import { get, set } from 'lodash'
import { defineStore } from 'pinia'
import { useInterfaceStore } from 'src/stores/interface.js'
import { ensureFinalFallback } from '../i18n/languages.js'
import { instanceDefaultProperties } from '../modules/config.js'
import {
instanceDefaultConfig,
staticOrApiConfigDefault,
} from '../modules/default_config_state.js'
import apiService from '../services/api/api.service.js'
import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations'
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,
bannerlimit: null,
avatarlimit: null,
backgroundlimit: null,
uploadlimit: null,
fieldsLimits: null,
private: false,
federating: true,
federationPolicy: null,
themesIndex: null,
stylesIndex: null,
palettesIndex: null,
themeData: null, // used for theme editor v2
vapidPublicKey: null,
// Stuff from static/config.json
loginMethod: 'password',
disableUpdateNotification: false,
// Instance-wide configurations that should not be changed by individual users
instanceIdentity: {
...staticOrApiConfigDefault,
},
// Instance admins can override default settings for the whole instance
prefsStorage: {
...instanceDefaultConfig,
},
// Custom emoji from server
customEmoji: [],
customEmojiFetched: false,
// Unicode emoji from bundle
emoji: {},
emojiFetched: false,
unicodeEmojiAnnotations: {},
// Known domains list for user's domain-muting
knownDomains: [],
// Moderation stuff
staffAccounts: [],
accountActivationRequired: null,
accountApprovalRequired: null,
birthdayRequired: false,
birthdayMinAge: 0,
restrictedNicknames: [],
// Feature-set, apparently, not everything here is reported...
featureSet: {
postFormats: [],
mailerEnabled: false,
safeDM: true,
shoutAvailable: false,
pleromaExtensionsAvailable: true,
pleromaChatMessagesAvailable: false,
pleromaCustomEmojiReactionsAvailable: false,
pleromaBookmarkFoldersAvailable: false,
pleromaPublicFavouritesAvailable: true,
statusNotificationTypeAvailable: true,
gopherAvailable: false,
editingAvailable: false,
mediaProxyAvailable: false,
suggestionsEnabled: false,
suggestionsWeb: '',
quotingAvailable: false,
groupActorAvailable: false,
blockExpiration: false,
tagPolicyAvailable: false,
pollsAvailable: 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
}
export const useInstanceStore = defineStore('instance', {
state: () => ({ ...defaultState }),
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 this.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) =>
(this.emoji[groupId] || []).map((k) =>
injectAnnotations(k, this.unicodeEmojiAnnotations),
),
).reduce((a, b) => a.concat(b), [])
},
standardEmojiGroupList(state) {
return SORTED_EMOJI_GROUP_IDS.map((groupId) => ({
id: groupId,
emojis: (this.emoji[groupId] || []).map((k) =>
injectAnnotations(k, this.unicodeEmojiAnnotations),
),
}))
},
instanceDomain(state) {
return new URL(this.server).hostname
},
remoteInteractionLink(state) {
const server = this.server.endsWith('/')
? this.server.slice(0, -1)
: this.server
const link = server + REMOTE_INTERACTION_URL
return ({ statusId, nickname }) => {
if (statusId) {
return `${link}?status_id=${statusId}`
} else {
return `${link}?nickname=${nickname}`
}
}
},
},
actions: {
set({ path, value }) {
if (get(defaultState, path) === undefined)
console.error(`Unknown instance option ${path}, value: ${value}`)
set(this, path, value)
switch (name) {
case 'name':
useInterfaceStore().setPageTitle()
break
case 'shoutAvailable':
if (value) {
window.vuex.dispatch('initializeSocket')
}
break
}
},
async getStaticEmoji() {
try {
// See build/emojis_plugin for more details
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
}, {})
this.emoji = injectRegionalIndicators(emoji)
} catch (e) {
console.warn("Can't load static emoji\n", e)
}
},
loadUnicodeEmojiData(language) {
const langList = ensureFinalFallback(language)
return Promise.all(
langList.map(async (lang) => {
if (!this.unicodeEmojiAnnotations[lang]) {
try {
const annotations = await loadAnnotations(lang)
this.unicodeEmojiAnnotations[lang] = annotations
} catch (e) {
console.warn(
`Error loading unicode emoji annotations for ${lang}: `,
e,
)
// ignore
}
}
}),
)
},
async getCustomEmoji() {
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 ? this.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)
this.customEmoji = emoji
} else {
throw res
}
} catch (e) {
console.warn("Can't load custom emojis\n", e)
}
},
fetchEmoji() {
if (!this.customEmojiFetched) {
this.customEmojiFetched = true
window.vuex.dispatch('getCustomEmoji')
}
if (!this.emojiFetched) {
this.emojiFetched = true
window.vuex.dispatch('getStaticEmoji')
}
},
async getKnownDomains() {
try {
this.knownDomains = await apiService.fetchKnownDomains({
credentials: window.vuex.state.users.currentUser.credentials,
})
} catch (e) {
console.warn("Can't load known domains\n", e)
}
},
},
})

View file

@ -20,8 +20,9 @@ import {
defaultState as configDefaultState, defaultState as configDefaultState,
instanceDefaultConfig, instanceDefaultConfig,
} from 'src/modules/default_config_state' } from 'src/modules/default_config_state'
import { useInstanceStore } from 'src/stores/instance'
export const VERSION = 1 export const VERSION = 2
export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
export const COMMAND_TRIM_FLAGS = 1000 export const COMMAND_TRIM_FLAGS = 1000
@ -45,11 +46,7 @@ export const defaultState = {
dontShowUpdateNotifs: false, dontShowUpdateNotifs: false,
collapseNav: false, collapseNav: false,
muteFilters: {}, muteFilters: {},
...{ ...configDefaultState,
// reverting all the undefined to their initial values
...configDefaultState,
...instanceDefaultConfig,
},
}, },
collections: { collections: {
pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'], pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
@ -377,21 +374,21 @@ export const _resetFlags = (
return result return result
} }
export const _doMigrations = (cache) => { export const _doMigrations = (cache, live) => {
if (!cache) return cache const data = cache ?? live
if (cache._version < VERSION) { if (data._version < VERSION) {
console.debug( console.debug(
'Local cached data has older version, seeing if there any migrations that can be applied', 'Data has older version, seeing if there any migrations that can be applied',
) )
// no migrations right now since we only have one version // no migrations right now since we only have one version
console.debug('No migrations found') console.debug('No migrations found')
} }
if (cache._version > VERSION) { if (data._version > VERSION) {
console.debug( console.debug(
'Local cached data has newer version, seeing if there any reverse migrations that can be applied', 'Data has newer version, seeing if there any reverse migrations that can be applied',
) )
// no reverse migrations right now but we leave a possibility of loading a hotpatch if need be // no reverse migrations right now but we leave a possibility of loading a hotpatch if need be
@ -401,8 +398,8 @@ export const _doMigrations = (cache) => {
return window._PLEROMA_HOTPATCH.reverseMigrations.call( return window._PLEROMA_HOTPATCH.reverseMigrations.call(
{}, {},
'serverSideStorage', 'serverSideStorage',
{ from: cache._version, to: VERSION }, { from: data._version, to: VERSION },
cache, data,
) )
} }
} }
@ -580,7 +577,7 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', {
cache = null cache = null
} }
cache = _doMigrations(cache) cache = _doMigrations(cache, live)
let { recent, stale, needUpload } = _getRecentData(cache, live) let { recent, stale, needUpload } = _getRecentData(cache, live)
@ -649,6 +646,12 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', {
}) })
}, },
}, },
getters: {
mergedConfig: (state) => ({
...useInstanceStore().prefsStorage,
...state.prefsStorage.simple,
}),
},
persist: { persist: {
afterLoad(state) { afterLoad(state) {
return state return state