diff --git a/changelog.d/splashscreen.add b/changelog.d/splashscreen.add
new file mode 100644
index 00000000..f1f56551
--- /dev/null
+++ b/changelog.d/splashscreen.add
@@ -0,0 +1 @@
+Splash screen + loading indicator to make process of identifying initialization issues and load performance
diff --git a/index.html b/index.html
index 6d9c4ce5..461f5acc 100644
--- a/index.html
+++ b/index.html
@@ -4,13 +4,138 @@
+
+
-
+
-
+
+
+
+ Art by pipivovott
+
+
+
+
+
+
+
+
+
+ (。>﹏<)
+
+
+
+
diff --git a/src/App.js b/src/App.js
index b7eb2f72..e87108dd 100644
--- a/src/App.js
+++ b/src/App.js
@@ -44,16 +44,29 @@ export default {
data: () => ({
mobileActivePanel: 'timeline'
}),
+ watch: {
+ themeApplied (value) {
+ this.removeSplash()
+ }
+ },
created () {
// Load the locale from the storage
const val = this.$store.getters.mergedConfig.interfaceLanguage
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
window.addEventListener('resize', this.updateMobileState)
},
+ mounted () {
+ if (this.$store.state.interface.themeApplied) {
+ this.removeSplash()
+ }
+ },
unmounted () {
window.removeEventListener('resize', this.updateMobileState)
},
computed: {
+ themeApplied () {
+ return this.$store.state.interface.themeApplied
+ },
classes () {
return [
{
@@ -130,6 +143,15 @@ export default {
updateMobileState () {
this.$store.dispatch('setLayoutWidth', windowWidth())
this.$store.dispatch('setLayoutHeight', windowHeight())
+ },
+ removeSplash () {
+ document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
+ const splashscreenRoot = document.querySelector('#splash')
+ splashscreenRoot.addEventListener('transitionend', () => {
+ splashscreenRoot.remove()
+ })
+ splashscreenRoot.classList.add('hidden')
+ document.querySelector('#app').classList.remove('hidden')
}
}
}
diff --git a/src/App.scss b/src/App.scss
index 9d1ce77a..f52ba06b 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -914,3 +914,169 @@ option {
color: var(--selectionText);
background-color: var(--selectionBackground);
}
+
+#splash {
+ pointer-events: none;
+ transition: opacity 2s;
+ opacity: 1;
+
+ &.hidden {
+ opacity: 0;
+ }
+
+ #status {
+ &.css-ok {
+ &::before {
+ display: inline-block;
+ content: "CSS OK";
+ }
+ }
+
+ .initial-text {
+ display: none;
+ }
+ }
+
+ #throbber {
+ animation-duration: 3s;
+ animation-name: bounce;
+ animation-iteration-count: infinite;
+ animation-direction: normal;
+ transform-origin: bottom center;
+
+ &.dead {
+ animation-name: dead;
+ animation-duration: 2s;
+ animation-iteration-count: 1;
+ transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
+ }
+
+ @keyframes dead {
+ 0% {
+ transform: rotateX(0) rotateY(0) rotateZ(0);
+ }
+
+ 5% {
+ transform: rotateX(0) rotateY(0) rotateZ(1deg);
+ }
+
+ 10% {
+ transform: rotateX(0) rotateY(0) rotateZ(-2deg);
+ }
+
+ 15% {
+ transform: rotateX(0) rotateY(0) rotateZ(3deg);
+ }
+
+ 20% {
+ transform: rotateX(0) rotateY(0) rotateZ(0);
+ }
+
+ 25% {
+ transform: rotateX(0) rotateY(0) rotateZ(0);
+ }
+
+ 30% {
+ transform: rotateX(10deg) rotateY(0) rotateZ(0);
+ }
+
+ 35% {
+ transform: rotateX(-10deg) rotateY(0) rotateZ(0);
+ }
+
+ 40% {
+ transform: rotateX(10deg) rotateY(0) rotateZ(0);
+ }
+
+ 45% {
+ transform: rotateX(-10deg) rotateY(0) rotateZ(0);
+ }
+
+ 50% {
+ transform: rotateX(10deg) rotateY(0) rotateZ(0);
+ }
+
+ 100% {
+ transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
+ transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); /* easeInQuint */
+ }
+ }
+
+ @keyframes bounce {
+ 0% {
+ scale: 1 1;
+ translate: 0 0;
+ animation-timing-function: ease-out;
+ }
+
+ 10% {
+ scale: 1.2 0.8;
+ translate: 0 0;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-out;
+ }
+
+ 30% {
+ scale: 0.9 1.1;
+ translate: 0 -40%;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-in;
+ }
+
+ 40% {
+ scale: 1.1 0.9;
+ translate: 0 -50%;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-in;
+ }
+
+ 45% {
+ scale: 0.9 1.1;
+ translate: 0 -45%;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-in;
+ }
+
+ 50% {
+ scale: 1.05 0.95;
+ translate: 0 -40%;
+ animation-timing-function: ease-in;
+ }
+
+ 55% {
+ scale: 0.985 1.025;
+ translate: 0 -35%;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-in;
+ }
+
+ 60% {
+ scale: 1.0125 0.9985;
+ translate: 0 -30%;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-in;
+ }
+
+ 80% {
+ scale: 1.0063 0.9938;
+ translate: 0 -10%;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-in-ou;
+ }
+
+ 90% {
+ scale: 1.2 0.8;
+ translate: 0 0;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-out;
+ }
+
+ 100% {
+ scale: 1 1;
+ translate: 0 0;
+ transform: rotateZ(var(--defaultZ));
+ animation-timing-function: ease-out;
+ }
+ }
+ }
+}
diff --git a/src/assets/pleromatan_apology.png b/src/assets/pleromatan_apology.png
deleted file mode 100644
index 36ad7aeb..00000000
Binary files a/src/assets/pleromatan_apology.png and /dev/null differ
diff --git a/src/assets/pleromatan_apology.png b/src/assets/pleromatan_apology.png
new file mode 120000
index 00000000..a7f6191f
--- /dev/null
+++ b/src/assets/pleromatan_apology.png
@@ -0,0 +1 @@
+../../static/pleromatan_apology.png
\ No newline at end of file
diff --git a/src/assets/pleromatan_apology_fox.png b/src/assets/pleromatan_apology_fox.png
deleted file mode 100644
index 17f87694..00000000
Binary files a/src/assets/pleromatan_apology_fox.png and /dev/null differ
diff --git a/src/assets/pleromatan_apology_fox.png b/src/assets/pleromatan_apology_fox.png
new file mode 120000
index 00000000..b3db4af3
--- /dev/null
+++ b/src/assets/pleromatan_apology_fox.png
@@ -0,0 +1 @@
+../../static/pleromatan_apology_fox.png
\ No newline at end of file
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 6cad05f6..55dd01a9 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -327,11 +327,7 @@ const setConfig = async ({ store }) => {
const checkOAuthToken = async ({ store }) => {
if (store.getters.getUserToken()) {
- try {
- await store.dispatch('loginUser', store.getters.getUserToken())
- } catch (e) {
- console.error(e)
- }
+ return store.dispatch('loginUser', store.getters.getUserToken())
}
return Promise.resolve()
}
@@ -349,19 +345,26 @@ const afterStoreSetup = async ({ store, i18n }) => {
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server })
+ document.querySelector('#status').textContent = i18n.global.t('splash.settings')
await setConfig({ store })
- await store.dispatch('setTheme')
+ document.querySelector('#status').textContent = i18n.global.t('splash.theme')
+ try {
+ await store.dispatch('setTheme').catch((e) => { console.error('Error setting theme', e) })
+ } catch (e) {
+ return Promise.reject(e)
+ }
- applyConfig(store.state.config)
+ 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
+ document.querySelector('#status').textContent = i18n.global.t('splash.instance')
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')
@@ -395,9 +398,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
// remove after vue 3.3
app.config.unwrapInjectedRef = true
+ document.querySelector('#status').textContent = i18n.global.t('splash.almost')
app.mount('#app')
-
return app
}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 423ce65e..9e17a81f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1405,5 +1405,17 @@
},
"unicode_domain_indicator": {
"tooltip": "This domain contains non-ascii characters."
+ },
+ "splash": {
+ "loading": "Loading...",
+ "theme": "Applying theme, please wait warmly...",
+ "instance": "Getting instance info...",
+ "settings": "Applying settings...",
+ "almost": "Reticulating splines...",
+ "fun_1": "Drink more water",
+ "fun_2": "Take it easy!",
+ "fun_3": "Suya...",
+ "fun_4": "My Pleroma machine is full power!",
+ "error": "Something went wrong"
}
}
diff --git a/src/main.js b/src/main.js
index 85eb1f4c..81fa09db 100644
--- a/src/main.js
+++ b/src/main.js
@@ -58,55 +58,75 @@ const persistedStateOptions = {
};
(async () => {
- let storageError = false
- const plugins = [pushNotifications]
+ const isFox = Math.floor(Math.random() * 2) > 0 ? '_fox' : ''
+
+ const splashError = (i18n, e) => {
+ const throbber = document.querySelector('#throbber')
+ throbber.addEventListener('animationend', () => {
+ document.querySelector('#mascot').src = `/static/pleromatan_orz${isFox}.png`
+ })
+ throbber.classList.add('dead')
+ document.querySelector('#status').textContent = i18n.global.t('splash.error')
+ console.error('PleromaFE failed to initialize: ', e)
+ }
+
try {
- const persistedState = await createPersistedState(persistedStateOptions)
- plugins.push(persistedState)
- } catch (e) {
- console.error(e)
- storageError = true
- }
- const store = createStore({
- modules: {
- i18n: {
- getters: {
- i18n: () => i18n.global
- }
+ let storageError
+ const plugins = [pushNotifications]
+ try {
+ const persistedState = await createPersistedState(persistedStateOptions)
+ plugins.push(persistedState)
+ } catch (e) {
+ console.error('Storage error', e)
+ storageError = e
+ }
+ document.querySelector('#mascot').src = `/static/pleromatan_apology${isFox}.png`
+ document.querySelector('#status').removeAttribute('class')
+ document.querySelector('#status').textContent = i18n.global.t('splash.loading')
+ document.querySelector('#splash-credit').textContent = i18n.global.t('update.art_by', { linkToArtist: 'pipivovott' })
+ const store = createStore({
+ modules: {
+ i18n: {
+ getters: {
+ i18n: () => i18n.global
+ }
+ },
+ interface: interfaceModule,
+ instance: instanceModule,
+ // TODO refactor users/statuses modules, they depend on each other
+ users: usersModule,
+ statuses: statusesModule,
+ notifications: notificationsModule,
+ lists: listsModule,
+ api: apiModule,
+ config: configModule,
+ profileConfig: profileConfigModule,
+ serverSideStorage: serverSideStorageModule,
+ adminSettings: adminSettingsModule,
+ shout: shoutModule,
+ oauth: oauthModule,
+ authFlow: authFlowModule,
+ mediaViewer: mediaViewerModule,
+ oauthTokens: oauthTokensModule,
+ reports: reportsModule,
+ polls: pollsModule,
+ postStatus: postStatusModule,
+ editStatus: editStatusModule,
+ statusHistory: statusHistoryModule,
+ chats: chatsModule,
+ announcements: announcementsModule
},
- interface: interfaceModule,
- instance: instanceModule,
- // TODO refactor users/statuses modules, they depend on each other
- users: usersModule,
- statuses: statusesModule,
- notifications: notificationsModule,
- lists: listsModule,
- api: apiModule,
- config: configModule,
- profileConfig: profileConfigModule,
- serverSideStorage: serverSideStorageModule,
- adminSettings: adminSettingsModule,
- shout: shoutModule,
- oauth: oauthModule,
- authFlow: authFlowModule,
- mediaViewer: mediaViewerModule,
- oauthTokens: oauthTokensModule,
- reports: reportsModule,
- polls: pollsModule,
- postStatus: postStatusModule,
- editStatus: editStatusModule,
- statusHistory: statusHistoryModule,
- chats: chatsModule,
- announcements: announcementsModule
- },
- plugins,
- strict: false // Socket modifies itself, let's ignore this for now.
- // strict: process.env.NODE_ENV !== 'production'
- })
- if (storageError) {
- store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
+ plugins,
+ strict: false // Socket modifies itself, let's ignore this for now.
+ // strict: process.env.NODE_ENV !== 'production'
+ })
+ if (storageError) {
+ store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
+ }
+ return await afterStoreSetup({ store, i18n })
+ } catch (e) {
+ splashError(i18n, e)
}
- afterStoreSetup({ store, i18n })
})()
// These are inlined by webpack's DefinePlugin
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index e54a95bf..c1603f39 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -43,16 +43,16 @@ const adoptStyleSheets = (styles) => {
// is nothing to do here.
}
-export const generateTheme = async (inputRuleset, callbacks, debug) => {
+export const generateTheme = (inputRuleset, callbacks, debug) => {
const {
onNewRule = (rule, isLazy) => {},
onLazyFinished = () => {},
onEagerFinished = () => {}
} = callbacks
- // Assuming that "worst case scenario background" is panel background since it's the most likely one
const themes3 = init({
inputRuleset,
+ // Assuming that "worst case scenario background" is panel background since it's the most likely one
ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(),
debug
})
@@ -146,11 +146,11 @@ export const tryLoadCache = () => {
}
}
-export const applyTheme = async (input, onFinish = (data) => {}, debug) => {
+export const applyTheme = (input, onFinish = (data) => {}, debug) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
- const { lazyProcessFunc } = await generateTheme(
+ const { lazyProcessFunc } = generateTheme(
input,
{
onNewRule (rule, isLazy) {
@@ -169,15 +169,22 @@ export const applyTheme = async (input, onFinish = (data) => {}, debug) => {
adoptStyleSheets([eagerStyles, lazyStyles])
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
onFinish(cache)
- localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
+ try {
+ localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
+ } catch (e) {
+ localStorage.removeItem('pleroma-fe-theme-cache')
+ try {
+ localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
+ } catch (e) {
+ console.warn('cannot save cache!', e)
+ }
+ }
}
},
debug
)
setTimeout(lazyProcessFunc, 0)
-
- return Promise.resolve()
}
const extractStyleConfig = ({
@@ -222,7 +229,7 @@ const extractStyleConfig = ({
const defaultStyleConfig = extractStyleConfig(defaultState)
-export const applyConfig = (input) => {
+export const applyConfig = (input, i18n) => {
const config = extractStyleConfig(input)
if (config === defaultStyleConfig) {
@@ -230,8 +237,6 @@ export const applyConfig = (input) => {
}
const head = document.head
- const body = document.body
- body.classList.add('hidden')
const rules = Object
.entries(config)
@@ -252,8 +257,6 @@ export const applyConfig = (input) => {
--roundness: var(--forcedRoundness) !important;
}`, 'index-max')
}
-
- body.classList.remove('hidden')
}
export const getThemes = () => {
diff --git a/static/pleromatan_apology.png b/static/pleromatan_apology.png
new file mode 100644
index 00000000..36ad7aeb
Binary files /dev/null and b/static/pleromatan_apology.png differ
diff --git a/static/pleromatan_apology_fox.png b/static/pleromatan_apology_fox.png
new file mode 100644
index 00000000..17f87694
Binary files /dev/null and b/static/pleromatan_apology_fox.png differ
diff --git a/static/pleromatan_orz.png b/static/pleromatan_orz.png
new file mode 100644
index 00000000..aa54411f
Binary files /dev/null and b/static/pleromatan_orz.png differ
diff --git a/static/pleromatan_orz_fox.png b/static/pleromatan_orz_fox.png
new file mode 100644
index 00000000..936e42f1
Binary files /dev/null and b/static/pleromatan_orz_fox.png differ