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 { getOrCreateServiceWorker } from './services/sw/sw'
import { windowHeight, windowWidth } from './services/window_utils/window_utils'
import { useInstanceStore } from './stores/instance'
import { useInterfaceStore } from './stores/interface'
import { useShoutStore } from './stores/shout'
@ -135,11 +136,6 @@ export default {
userBackground() {
return this.currentUser.background_image
},
instanceBackground() {
return this.mergedConfig.hideInstanceWallpaper
? null
: this.$store.state.instance.background
},
background() {
return this.userBackground || this.instanceBackground
},
@ -153,16 +149,6 @@ export default {
shout() {
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() {
return this.$route.name === 'chat' || this.$route.name === 'chats'
},
@ -177,21 +163,12 @@ export default {
this.layoutType === 'mobile'
)
},
showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
editingAvailable() {
return this.$store.state.instance.editingAvailable
},
shoutboxPosition() {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
},
layoutType() {
return useInterfaceStore().layoutType
},
privateMode() {
return this.$store.state.instance.private
},
reverseLayout() {
const { thirdColumnMode, sidebarRight: reverseSetting } =
this.$store.getters.mergedConfig
@ -214,8 +191,22 @@ export default {
},
...mapGetters(['mergedConfig']),
...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: {
resizeHandler() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -93,7 +93,7 @@ export const instanceDefaultConfig = {
sidebarRight: false,
scopeCopy: true,
subjectLineBehavior: 'email',
alwaysShowSubjectInput: true,
alwaysShowSubjectInput: false,
postContentType: 'text/plain',
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,
instanceDefaultConfig,
} 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 COMMAND_TRIM_FLAGS = 1000
@ -45,11 +46,7 @@ export const defaultState = {
dontShowUpdateNotifs: false,
collapseNav: false,
muteFilters: {},
...{
// reverting all the undefined to their initial values
...configDefaultState,
...instanceDefaultConfig,
},
...configDefaultState,
},
collections: {
pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
@ -377,21 +374,21 @@ export const _resetFlags = (
return result
}
export const _doMigrations = (cache) => {
if (!cache) return cache
export const _doMigrations = (cache, live) => {
const data = cache ?? live
if (cache._version < VERSION) {
if (data._version < VERSION) {
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
console.debug('No migrations found')
}
if (cache._version > VERSION) {
if (data._version > VERSION) {
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
@ -401,8 +398,8 @@ export const _doMigrations = (cache) => {
return window._PLEROMA_HOTPATCH.reverseMigrations.call(
{},
'serverSideStorage',
{ from: cache._version, to: VERSION },
cache,
{ from: data._version, to: VERSION },
data,
)
}
}
@ -580,7 +577,7 @@ export const useServerSideStorageStore = defineStore('serverSideStorage', {
cache = null
}
cache = _doMigrations(cache)
cache = _doMigrations(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: {
afterLoad(state) {
return state