pleroma-fe/src/boot/after_store.js

599 lines
18 KiB
JavaScript
Raw Normal View History

2025-03-11 18:48:55 -04:00
/* global process */
2026-01-06 16:23:17 +02:00
import vClickOutside from 'click-outside-vue3'
2022-03-29 00:58:17 +03:00
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
2022-12-24 13:48:36 -05:00
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
2026-01-06 16:23:17 +02:00
import { config } from '@fortawesome/fontawesome-svg-core'
2026-01-06 16:22:52 +02:00
import {
FontAwesomeIcon,
FontAwesomeLayers,
} from '@fortawesome/vue-fontawesome'
2026-01-06 16:23:17 +02:00
2025-06-28 15:46:38 +03:00
config.autoAddCss = false
import VBodyScrollLock from 'src/directives/body_scroll_lock'
2026-01-06 16:22:52 +02:00
import {
2026-01-06 16:23:17 +02:00
instanceDefaultConfig,
staticOrApiConfigDefault,
} from 'src/modules/default_config_state.js'
import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow'
import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface'
import { useOAuthStore } from 'src/stores/oauth'
import App from '../App.vue'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
2020-11-02 15:46:49 +02:00
import FaviconService from '../services/favicon_service/favicon_service.js'
2026-01-06 16:23:17 +02:00
import { applyConfig } from '../services/style_setter/style_setter.js'
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
2026-01-06 16:22:52 +02:00
import {
2026-01-06 16:23:17 +02:00
windowHeight,
windowWidth,
} from '../services/window_utils/window_utils'
import routes from './routes'
2023-04-04 14:40:12 -06:00
let staticInitialResults = null
const parsedInitialResults = () => {
if (!document.getElementById('initial-results')) {
return null
}
if (!staticInitialResults) {
2026-01-06 16:22:52 +02:00
staticInitialResults = JSON.parse(
document.getElementById('initial-results').textContent,
)
}
return staticInitialResults
}
const decodeUTF8Base64 = (data) => {
const rawData = atob(data)
const array = Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
const text = new TextDecoder().decode(array)
return text
}
const preloadFetch = async (request) => {
const data = parsedInitialResults()
if (!data || !data[request]) {
return window.fetch(request)
}
const decoded = decodeUTF8Base64(data[request])
const requestData = JSON.parse(decoded)
return {
ok: true,
json: () => requestData,
2026-01-06 16:22:52 +02:00
text: () => requestData,
}
}
const getInstanceConfig = async ({ store }) => {
2019-03-13 11:57:30 +01:00
try {
2020-06-27 12:26:19 +03:00
const res = await preloadFetch('/api/v1/instance')
2019-03-13 12:41:39 +01:00
if (res.ok) {
const data = await res.json()
const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key
2018-10-26 15:16:23 +02:00
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'pleromaExtensionsAvailable',
value: data.pleroma,
})
store.dispatch('setInstanceOption', {
name: 'textlimit',
value: textlimit,
})
store.dispatch('setInstanceOption', {
name: 'accountApprovalRequired',
value: data.approval_required,
})
store.dispatch('setInstanceOption', {
name: 'birthdayRequired',
value: !!data.pleroma?.metadata.birthday_required,
})
store.dispatch('setInstanceOption', {
name: 'birthdayMinAge',
value: data.pleroma?.metadata.birthday_min_age || 0,
})
2018-12-18 18:26:14 +00:00
2018-12-10 22:36:25 +07:00
if (vapidPublicKey) {
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'vapidPublicKey',
value: vapidPublicKey,
})
2018-12-10 22:36:25 +07:00
}
2019-03-13 12:41:39 +01:00
} else {
2026-01-06 16:22:52 +02:00
throw res
2019-03-13 12:41:39 +01:00
}
2019-03-13 11:57:30 +01:00
} catch (error) {
console.error('Could not load instance config, potentially fatal')
2019-03-13 11:57:30 +01:00
console.error(error)
}
2025-06-17 09:54:58 +03:00
// We should check for scrobbles support here but it requires userId
// so instead we check for it where it's fetched (statuses.js)
2019-03-13 11:57:30 +01:00
}
2018-10-26 15:16:23 +02:00
2025-02-04 15:23:21 +02:00
const getBackendProvidedConfig = async () => {
try {
const res = await window.fetch('/api/pleroma/frontend_configurations')
if (res.ok) {
const data = await res.json()
return data.pleroma_fe
2019-03-13 12:41:39 +01:00
} else {
2026-01-06 16:22:52 +02:00
throw res
2019-03-13 12:41:39 +01:00
}
2019-03-13 11:57:30 +01:00
} catch (error) {
2026-01-06 16:22:52 +02:00
console.error(
'Could not load backend-provided frontend config, potentially fatal',
)
2019-03-13 11:57:30 +01:00
console.error(error)
}
}
2018-10-26 15:16:23 +02:00
2019-03-13 11:57:30 +01:00
const getStaticConfig = async () => {
try {
const res = await window.fetch('/static/config.json')
2019-03-13 12:41:39 +01:00
if (res.ok) {
return res.json()
} else {
2026-01-06 16:22:52 +02:00
throw res
2019-03-13 12:41:39 +01:00
}
2019-03-13 11:57:30 +01:00
} catch (error) {
console.warn('Failed to load static/config.json, continuing without it.')
console.warn(error)
return {}
}
}
2019-01-24 21:03:13 +03:00
2019-03-13 11:57:30 +01:00
const setSettings = async ({ apiConfig, staticConfig, store }) => {
const overrides = window.___pleromafe_dev_overrides || {}
const env = window.___pleromafe_mode.NODE_ENV
// This takes static config and overrides properties that are present in apiConfig
let config = {}
if (overrides.staticConfigPreference && env === 'development') {
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
config = Object.assign({}, apiConfig, staticConfig)
} else {
config = Object.assign({}, staticConfig, apiConfig)
}
const copyInstanceOption = (name) => {
if (typeof config[name] !== 'undefined') {
store.dispatch('setInstanceOption', { name, value: config[name] })
}
2019-03-13 11:57:30 +01:00
}
Object.keys(staticOrApiConfigDefault).forEach(copyInstanceOption)
Object.keys(instanceDefaultConfig).forEach(copyInstanceOption)
2019-03-13 11:57:30 +01:00
useAuthFlowStore().setInitialStrategy(config.loginMethod)
2019-03-13 11:57:30 +01:00
}
2018-10-26 15:16:23 +02:00
const getTOS = async ({ store }) => {
try {
const res = await window.fetch('/static/terms-of-service.html')
if (res.ok) {
const html = await res.text()
2018-10-26 15:16:23 +02:00
store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else {
2026-01-06 16:22:52 +02:00
throw res
}
} catch (e) {
console.warn("Can't load TOS\n", e)
}
}
const getInstancePanel = async ({ store }) => {
try {
const res = await preloadFetch('/instance/panel.html')
if (res.ok) {
const html = await res.text()
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'instanceSpecificPanelContent',
value: html,
})
} else {
2026-01-06 16:22:52 +02:00
throw res
}
} catch (e) {
console.warn("Can't load instance panel\n", e)
}
}
2018-10-26 15:16:23 +02:00
const getStickers = async ({ store }) => {
try {
const res = await window.fetch('/static/stickers.json')
if (res.ok) {
const values = await res.json()
2026-01-06 16:22:52 +02:00
const stickers = (
await Promise.all(
Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json')
let meta = {}
if (resPack.ok) {
meta = await resPack.json()
}
return {
pack: name,
path,
meta,
}
}),
)
).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title)
})
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
} else {
2026-01-06 16:22:52 +02:00
throw res
}
} catch (e) {
console.warn("Can't load stickers\n", e)
}
}
const getAppSecret = async ({ store }) => {
2025-03-11 18:48:55 -04:00
const oauth = useOAuthStore()
if (oauth.userToken) {
2026-01-06 16:22:52 +02:00
store.commit(
'setBackendInteractor',
backendInteractorService(oauth.getToken),
)
}
}
const resolveStaffAccounts = ({ store, accounts }) => {
2026-01-06 16:22:52 +02:00
const nicknames = accounts.map((uri) => uri.split('/').pop())
store.dispatch('setInstanceOption', {
name: 'staffAccounts',
value: nicknames,
})
2019-11-08 23:21:07 -06:00
}
const getNodeInfo = async ({ store }) => {
try {
let res = await preloadFetch('/nodeinfo/2.1.json')
if (!res.ok) res = await preloadFetch('/nodeinfo/2.0.json')
if (res.ok) {
const data = await res.json()
2018-10-26 15:16:23 +02:00
const metadata = data.metadata
const features = metadata.features
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'name',
value: metadata.nodeName,
})
store.dispatch('setInstanceOption', {
name: 'registrationOpen',
value: data.openRegistrations,
})
store.dispatch('setInstanceOption', {
name: 'mediaProxyAvailable',
value: features.includes('media_proxy'),
})
store.dispatch('setInstanceOption', {
name: 'safeDM',
value: features.includes('safe_dm_mentions'),
})
store.dispatch('setInstanceOption', {
name: 'shoutAvailable',
value: features.includes('chat'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaChatMessagesAvailable',
value: features.includes('pleroma_chat_messages'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaCustomEmojiReactionsAvailable',
value:
features.includes('pleroma_custom_emoji_reactions') ||
2026-01-06 16:22:52 +02:00
features.includes('custom_emoji_reactions'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaBookmarkFoldersAvailable',
value: features.includes('pleroma:bookmark_folders'),
})
store.dispatch('setInstanceOption', {
name: 'gopherAvailable',
value: features.includes('gopher'),
})
store.dispatch('setInstanceOption', {
name: 'pollsAvailable',
value: features.includes('polls'),
})
store.dispatch('setInstanceOption', {
name: 'editingAvailable',
value: features.includes('editing'),
})
store.dispatch('setInstanceOption', {
name: 'pollLimits',
value: metadata.pollLimits,
})
store.dispatch('setInstanceOption', {
name: 'mailerEnabled',
value: metadata.mailerEnabled,
})
store.dispatch('setInstanceOption', {
name: 'quotingAvailable',
value: features.includes('quote_posting'),
})
store.dispatch('setInstanceOption', {
name: 'groupActorAvailable',
value: features.includes('pleroma:group_actors'),
})
store.dispatch('setInstanceOption', {
name: 'blockExpiration',
value: features.includes('pleroma:block_expiration'),
})
store.dispatch('setInstanceOption', {
name: 'localBubbleInstances',
value: metadata.localBubbleInstances ?? [],
})
2018-10-26 15:16:23 +02:00
const uploadLimits = metadata.uploadLimits
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'uploadlimit',
value: parseInt(uploadLimits.general),
})
store.dispatch('setInstanceOption', {
name: 'avatarlimit',
value: parseInt(uploadLimits.avatar),
})
store.dispatch('setInstanceOption', {
name: 'backgroundlimit',
value: parseInt(uploadLimits.background),
})
store.dispatch('setInstanceOption', {
name: 'bannerlimit',
value: parseInt(uploadLimits.banner),
})
store.dispatch('setInstanceOption', {
name: 'fieldsLimits',
value: metadata.fieldsLimits,
})
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'restrictedNicknames',
value: metadata.restrictedNicknames,
})
store.dispatch('setInstanceOption', {
name: 'postFormats',
value: metadata.postFormats,
})
2018-10-26 15:16:23 +02:00
const suggestions = metadata.suggestions
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'suggestionsEnabled',
value: suggestions.enabled,
})
store.dispatch('setInstanceOption', {
name: 'suggestionsWeb',
value: suggestions.web,
})
const software = data.software
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'backendVersion',
value: software.version,
})
store.dispatch('setInstanceOption', {
name: 'backendRepository',
value: software.repository,
})
const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'frontendVersion',
value: frontendVersion,
})
2019-11-08 23:21:07 -06:00
2019-11-09 00:09:32 -06:00
const federation = metadata.federation
store.dispatch('setInstanceOption', {
name: 'tagPolicyAvailable',
2026-01-06 16:22:52 +02:00
value:
typeof federation.mrf_policies === 'undefined'
? false
: metadata.federation.mrf_policies.includes('TagPolicy'),
})
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'federationPolicy',
value: federation,
})
store.dispatch('setInstanceOption', {
name: 'federating',
2026-01-06 16:22:52 +02:00
value:
typeof federation.enabled === 'undefined' ? true : federation.enabled,
})
2019-11-09 00:09:32 -06:00
const accountActivationRequired = metadata.accountActivationRequired
2026-01-06 16:22:52 +02:00
store.dispatch('setInstanceOption', {
name: 'accountActivationRequired',
value: accountActivationRequired,
})
2019-11-08 23:21:07 -06:00
const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts })
} else {
2026-01-06 16:22:52 +02:00
throw res
}
} catch (e) {
console.warn('Could not load nodeinfo')
console.warn(e)
}
}
const setConfig = async ({ store }) => {
// apiConfig, staticConfig
2026-01-06 16:22:52 +02:00
const configInfos = await Promise.all([
getBackendProvidedConfig({ store }),
getStaticConfig(),
])
const apiConfig = configInfos[0]
const staticConfig = configInfos[1]
2025-03-11 18:48:55 -04:00
getAppSecret({ store })
await setSettings({ store, apiConfig, staticConfig })
}
2019-04-01 12:32:13 -07:00
const checkOAuthToken = async ({ store }) => {
2025-03-11 18:48:55 -04:00
const oauth = useOAuthStore()
if (oauth.getUserToken) {
return store.dispatch('loginUser', oauth.getUserToken)
}
return Promise.resolve()
2019-04-01 12:32:13 -07:00
}
2023-04-05 21:06:37 -06:00
const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
2023-04-04 21:17:54 -06:00
const app = createApp(App)
2025-03-11 18:48:55 -04:00
// Must have app use pinia before we do anything that touches the store
// https://pinia.vuejs.org/core-concepts/plugins.html#Introduction
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
2023-04-04 21:17:54 -06:00
app.use(pinia)
2025-03-11 18:48:55 -04:00
const waitForAllStoresToLoad = async () => {
// the stores that do not persist technically do not need to be awaited here,
// but that involves either hard-coding the stores in some place (prone to errors)
// or writing another vite plugin to analyze which stores needs persisting (++load time)
const allStores = import.meta.glob('../stores/*.js', { eager: true })
if (process.env.NODE_ENV === 'development') {
// do some checks to avoid common errors
if (!Object.keys(allStores).length) {
2026-01-06 16:22:52 +02:00
throw new Error(
'No stores are available. Check the code in src/boot/after_store.js',
)
2025-03-11 18:48:55 -04:00
}
}
await Promise.all(
2026-01-06 16:22:52 +02:00
Object.entries(allStores).map(async ([name, mod]) => {
const isStoreName = (name) => name.startsWith('use')
if (process.env.NODE_ENV === 'development') {
if (Object.keys(mod).filter(isStoreName).length !== 1) {
throw new Error(
'Each store file must export exactly one store as a named export. Check your code in src/stores/',
)
2025-03-11 18:48:55 -04:00
}
2026-01-06 16:22:52 +02:00
}
const storeFuncName = Object.keys(mod).find(isStoreName)
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
const p = mod[storeFuncName]().$persistLoaded
if (!(p instanceof Promise)) {
throw new Error(
`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`,
)
2025-03-11 18:48:55 -04:00
}
2026-01-06 16:22:52 +02:00
await p
} else {
throw new Error(
`Store module ${name} does not export a 'use...' function`,
)
}
}),
)
2025-03-11 18:48:55 -04:00
}
try {
await waitForAllStoresToLoad()
} catch (e) {
console.error('Cannot load stores:', e)
storageError = e
}
2023-04-05 21:06:37 -06:00
if (storageError) {
2026-01-06 16:22:52 +02:00
useInterfaceStore().pushGlobalNotice({
messageKey: 'errors.storage_unavailable',
level: 'error',
})
2023-04-05 21:06:37 -06:00
}
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
2020-11-02 15:46:49 +02:00
FaviconService.initFaviconService()
2023-11-16 20:41:41 +02:00
initServiceWorker(store)
window.addEventListener('focus', () => updateFocus())
2020-11-02 15:46:49 +02:00
const overrides = window.___pleromafe_dev_overrides || {}
2026-01-06 16:22:52 +02:00
const server =
typeof overrides.target !== 'undefined'
? overrides.target
: window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server })
await setConfig({ store })
try {
2026-01-06 16:22:52 +02:00
await useInterfaceStore()
.applyTheme()
.catch((e) => {
console.error('Error setting theme', e)
})
} catch (e) {
window.splashError(e)
return Promise.reject(e)
}
2019-03-23 22:21:57 +02:00
2024-09-16 02:34:02 +03:00
applyConfig(store.state.config, i18n.global)
// Now we can try getting the server settings and logging in
2020-06-27 12:32:01 +03:00
// Most of these are preloaded into the index.html so blocking is minimized
2019-04-01 12:32:13 -07:00
await Promise.all([
checkOAuthToken({ store }),
getInstancePanel({ store }),
getNodeInfo({ store }),
2026-01-06 16:22:52 +02:00
getInstanceConfig({ store }),
]).catch((e) => Promise.reject(e))
2019-03-13 11:57:30 +01:00
2020-04-21 23:27:51 +03:00
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('loadDrafts')
2023-04-06 16:32:21 -06:00
useAnnouncementsStore().startFetchingAnnouncements()
2020-06-27 12:32:01 +03:00
getTOS({ store })
getStickers({ store })
2020-04-21 23:27:51 +03:00
const router = createRouter({
history: createWebHistory(),
2019-03-13 11:57:30 +01:00
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
2026-01-06 16:22:52 +02:00
if (to.matched.some((m) => m.meta.dontScroll)) {
2019-03-13 11:57:30 +01:00
return false
}
return savedPosition || { left: 0, top: 0 }
2026-01-06 16:22:52 +02:00
},
2019-03-13 11:57:30 +01:00
})
2023-04-04 14:40:12 -06:00
useI18nStore().setI18n(i18n)
app.use(router)
app.use(store)
app.use(i18n)
2024-11-12 23:24:28 +02:00
// Little thing to get out of invalid theme state
window.resetThemes = () => {
2025-02-03 00:14:44 +02:00
useInterfaceStore().resetThemeV3()
useInterfaceStore().resetThemeV3Palette()
useInterfaceStore().resetThemeV2()
2024-11-12 23:24:28 +02:00
}
2022-03-17 09:28:19 +02:00
app.use(vClickOutside)
app.use(VBodyScrollLock)
2022-12-24 13:48:36 -05:00
app.use(VueVirtualScroller)
app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers)
// remove after vue 3.3
app.config.unwrapInjectedRef = true
app.mount('#app')
return app
2018-10-26 15:16:23 +02:00
}
export default afterStoreSetup