From 9452b3084a51e41a520470834878aa4a37205597 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 22 Jan 2026 00:22:18 +0200 Subject: [PATCH] some initial work on moving instance settings to pinia --- src/App.js | 41 +- src/boot/after_store.js | 201 ++++----- .../post_status_form/post_status_form.js | 11 +- .../settings_modal/helpers/setting.js | 42 +- .../settings_modal/tabs/clutter_tab.vue | 24 +- .../tabs/old_theme_tab/old_theme_tab.js | 6 +- src/components/status/status.js | 7 +- src/modules/default_config_state.js | 2 +- src/stores/instance.js | 387 ++++++++++++++++++ src/stores/serverSideStorage.js | 33 +- 10 files changed, 559 insertions(+), 195 deletions(-) create mode 100644 src/stores/instance.js diff --git a/src/App.js b/src/App.js index 2af10cc8b..bfdeb2be4 100644 --- a/src/App.js +++ b/src/App.js @@ -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() { diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 1a2be5bd7..48dc42bc1 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -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 { diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index a073a5eb8..781b3214d 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -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: { diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index 06ed9b2f6..86e54caad 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -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) } diff --git a/src/components/settings_modal/tabs/clutter_tab.vue b/src/components/settings_modal/tabs/clutter_tab.vue index ef230dee0..59e942492 100644 --- a/src/components/settings_modal/tabs/clutter_tab.vue +++ b/src/components/settings_modal/tabs/clutter_tab.vue @@ -6,7 +6,7 @@
  • {{ $t('settings.subject_input_always_show') }} @@ -14,7 +14,7 @@
  • {{ $t('settings.minimal_scopes_mode') }} @@ -22,14 +22,14 @@
  • {{ $t('settings.hide_post_stats') }}
  • {{ $t('settings.hide_user_stats') }} @@ -38,7 +38,7 @@
  • {{ $t('settings.hide_actor_type_indication') }} @@ -46,7 +46,7 @@
  • {{ $t('settings.hide_scrobbles') }} @@ -55,7 +55,7 @@ @@ -70,7 +70,7 @@
  • {{ $t('settings.max_thumbnails') }} @@ -79,7 +79,7 @@
  • {{ $t('settings.hide_attachments_in_tl') }} @@ -87,7 +87,7 @@
  • {{ $t('settings.hide_attachments_in_convo') }} @@ -95,7 +95,7 @@
  • {{ $t('settings.user_card_hide_personal_marks') }} @@ -103,7 +103,7 @@
  • {{ $t('settings.hide_shoutbox') }} diff --git a/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.js b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.js index c79c72e70..3235264bc 100644 --- a/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.js +++ b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.js @@ -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), ) diff --git a/src/components/status/status.js b/src/components/status/status.js index 8cbc12c09..0fb9de9f0 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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: { diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js index d5daef3d3..c540dde7c 100644 --- a/src/modules/default_config_state.js +++ b/src/modules/default_config_state.js @@ -93,7 +93,7 @@ export const instanceDefaultConfig = { sidebarRight: false, scopeCopy: true, subjectLineBehavior: 'email', - alwaysShowSubjectInput: true, + alwaysShowSubjectInput: false, postContentType: 'text/plain', minimalScopesMode: false, diff --git a/src/stores/instance.js b/src/stores/instance.js new file mode 100644 index 000000000..7fcf1a92f --- /dev/null +++ b/src/stores/instance.js @@ -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) + } + }, + }, +}) diff --git a/src/stores/serverSideStorage.js b/src/stores/serverSideStorage.js index 23c027b1e..73bf795cf 100644 --- a/src/stores/serverSideStorage.js +++ b/src/stores/serverSideStorage.js @@ -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