logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/
commit: 24663b2f042e1e384fc3ecd1e5f4759ca986bdc7
parent 9e3e4ed429e11594ffeccbe8000afeaebe066cb0
Author: Henry Jameson <me@hjkos.com>
Date:   Thu,  3 Oct 2024 23:06:47 +0300

Merge remote-tracking branch 'origin/develop' into themes3-grand-finale-maybe

Diffstat:

Achangelog.d/splashscreen.add1+
Mindex.html129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/App.js22++++++++++++++++++++++
Msrc/App.scss166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assets/pleromatan_apology.png2++
Asrc/assets/pleromatan_apology_fox.png2++
Msrc/boot/after_store.js21++++++++++++---------
Msrc/i18n/en.json12++++++++++++
Msrc/main.js112++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/services/style_setter/style_setter.js27++++++++++++++-------------
Rsrc/assets/pleromatan_apology.png -> static/pleromatan_apology.png0
Rsrc/assets/pleromatan_apology_fox.png -> static/pleromatan_apology_fox.png0
Astatic/pleromatan_orz.png0
Astatic/pleromatan_orz_fox.png0
14 files changed, 424 insertions(+), 70 deletions(-)

diff --git a/changelog.d/splashscreen.add 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 @@ -4,13 +4,138 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <link rel="icon" type="image/png" href="/favicon.png"> + <!-- putting styles here to avoid having to wait for styles to load up --> + <style id="splashscreen"> + #splash { + --scale: 1; + width: 100vw; + height: 100vh; + display: grid; + grid-template-rows: auto; + grid-template-columns: auto; + align-content: center; + align-items: center; + justify-content: center; + justify-items: center; + flex-direction: column; + background: #0f161e; + font-family: sans-serif; + color: #b9b9ba; + position: absolute; + z-index: 9999; + font-size: calc(1vw + 1vh + 1vmin); + } + + #splash-credit { + position: absolute; + font-size: 14px; + bottom: 16px; + right: 16px; + } + + #splash-container { + align-items: center; + } + + #mascot-container { + display: flex; + align-items: flex-end; + justify-content: center; + perspective: 60em; + perspective-origin: 0 -15em; + transform-style: preserve-3d; + } + + #mascot { + width: calc(10em * var(--scale)); + height: calc(10em * var(--scale)); + object-fit: contain; + object-position: bottom; + transform: translateZ(-2em); + } + + #throbber { + display: grid; + width: calc(5em * 0.5 * var(--scale)); + height: calc(8em * 0.5 * var(--scale)); + margin-left: 4.1em; + z-index: 2; + grid-template-rows: repeat(8, 1fr); + grid-template-columns: repeat(5, 1fr); + grid-template-areas: "P P . L L" + "P P . L L" + "P P . L L" + "P P . L L" + "P P . . ." + "P P . . ." + "P P . E E" + "P P . E E"; + } + + .chunk { + background-color: #e2b188; + box-shadow: 0.01em 0.01em 0.1em 0 #e2b188; + } + + #chunk-P { + grid-area: P; + border-top-left-radius: calc(var(--logoChunkSize) / 2); + } + + #chunk-L { + grid-area: L; + border-bottom-right-radius: calc(var(--logoChunkSize) / 2); + } + + #chunk-E { + grid-area: E; + border-bottom-right-radius: calc(var(--logoChunkSize) / 2); + } + + #status { + margin-top: 1em; + line-height: 2; + width: 100%; + text-align: center; + } + + @media (prefers-reduced-motion) { + #throbber { + animation: none !important; + } + } + </style> <style id="pleroma-eager-styles" type="text/css"></style> <style id="pleroma-lazy-styles" type="text/css"></style> <!--server-generated-meta--> </head> - <body class="hidden"> + <body style="margin: 0; padding: 0"> <noscript>To use Pleroma, please enable JavaScript.</noscript> - <div id="app"></div> + <div id="splash"> + <!-- we are hiding entire graphic so no point showing credit --> + <div aria-hidden="true" id="splash-credit"> + Art by pipivovott + </div> + <div id="splash-container"> + <div aria-hidden="true" id="mascot-container"> + <div id="throbber"> + <div class="chunk" id="chunk-P"> + </div> + <div class="chunk" id="chunk-L"> + </div> + <div class="chunk" id="chunk-E"> + </div> + </div> + <img id="mascot" src="/static/pleromatan_apology.png"> + </div> + <div id="status" class="css-ok"> + <!-- (。>﹏<) --> + <!-- it's a pseudographic, don't want screenreader read out nonsense --> + <span aria-hidden="true" class="initial-text">(。&gt;﹏&lt;)</span> + </div> + </div> + </div> + <div id="app" class="hidden"></div> <div id="modal"></div> <!-- built files will be auto injected --> <div id="popovers" /> diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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('applyTheme', { recompile: false }) + document.querySelector('#status').textContent = i18n.global.t('splash.theme') + try { + await store.dispatch('applyTheme').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 @@ -1495,5 +1495,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 @@ -58,55 +58,75 @@ const persistedStateOptions = { }; (async () => { - let storageError = false - const plugins = [pushNotifications] - try { - const persistedState = await createPersistedState(persistedStateOptions) - plugins.push(persistedState) - } catch (e) { - console.error(e) - storageError = true + 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) } - const store = createStore({ - modules: { - i18n: { - getters: { - i18n: () => i18n.global - } + + try { + 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 @@ -42,14 +42,13 @@ 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, debug @@ -144,12 +143,11 @@ export const tryLoadCache = () => { } } -export const applyTheme = async (input, onFinish = (data) => {}, debug) => { - console.log('INPUT', input) +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) { @@ -168,15 +166,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 = ({ @@ -221,7 +226,7 @@ const extractStyleConfig = ({ const defaultStyleConfig = extractStyleConfig(defaultState) -export const applyConfig = (input) => { +export const applyConfig = (input, i18n) => { const config = extractStyleConfig(input) if (config === defaultStyleConfig) { @@ -229,8 +234,6 @@ export const applyConfig = (input) => { } const head = document.head - const body = document.body - body.classList.add('hidden') const rules = Object .entries(config) @@ -251,8 +254,6 @@ export const applyConfig = (input) => { --roundness: var(--forcedRoundness) !important; }`, 'index-max') } - - body.classList.remove('hidden') } export const getResourcesIndex = async (url) => { diff --git a/src/assets/pleromatan_apology.png b/static/pleromatan_apology.png Binary files differ. diff --git a/src/assets/pleromatan_apology_fox.png b/static/pleromatan_apology_fox.png Binary files differ. diff --git a/static/pleromatan_orz.png b/static/pleromatan_orz.png Binary files differ. diff --git a/static/pleromatan_orz_fox.png b/static/pleromatan_orz_fox.png Binary files differ.