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