diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.js | 1 | ||||
| -rw-r--r-- | src/App.vue | 1 | ||||
| -rw-r--r-- | src/assets/pleromatan_apology.png | bin | 0 -> 405742 bytes | |||
| -rw-r--r-- | src/assets/pleromatan_apology_fox.png | bin | 0 -> 533320 bytes | |||
| -rw-r--r-- | src/components/update_notification/update_notification.js | 59 | ||||
| -rw-r--r-- | src/components/update_notification/update_notification.scss | 106 | ||||
| -rw-r--r-- | src/components/update_notification/update_notification.vue | 61 | ||||
| -rw-r--r-- | src/i18n/en.json | 9 | ||||
| -rw-r--r-- | src/lib/persisted_state.js | 1 | ||||
| -rw-r--r-- | src/main.js | 3 | ||||
| -rw-r--r-- | src/modules/serverSideStorage.js | 230 | ||||
| -rw-r--r-- | src/modules/users.js | 2 | ||||
| -rw-r--r-- | src/services/entity_normalizer/entity_normalizer.service.js | 3 |
13 files changed, 476 insertions, 0 deletions
@@ -32,6 +32,7 @@ export default { MobileNav, DesktopNav, SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')), + UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')), UserReportingModal, PostStatusModal, GlobalNoticeList diff --git a/src/App.vue b/src/App.vue index 0efadaf0..c741aa70 100644 --- a/src/App.vue +++ b/src/App.vue @@ -65,6 +65,7 @@ <UserReportingModal /> <PostStatusModal /> <SettingsModal /> + <UpdateNotification /> <div id="modal" /> <GlobalNoticeList /> <div id="popovers" /> diff --git a/src/assets/pleromatan_apology.png b/src/assets/pleromatan_apology.png Binary files differnew file mode 100644 index 00000000..36ad7aeb --- /dev/null +++ b/src/assets/pleromatan_apology.png diff --git a/src/assets/pleromatan_apology_fox.png b/src/assets/pleromatan_apology_fox.png Binary files differnew file mode 100644 index 00000000..17f87694 --- /dev/null +++ b/src/assets/pleromatan_apology_fox.png diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js new file mode 100644 index 00000000..172be889 --- /dev/null +++ b/src/components/update_notification/update_notification.js @@ -0,0 +1,59 @@ +import Modal from 'src/components/modal/modal.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import pleromaTan from 'src/assets/pleromatan_apology.png' +import pleromaTanFox from 'src/assets/pleromatan_apology_fox.png' + +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' +library.add( + faTimes +) + +export const CURRENT_UPDATE_COUNTER = 1 + +const UpdateNotification = { + data () { + return { + pleromaTanVariant: Math.random() > 0.5 ? pleromaTan : pleromaTanFox, + showingMore: false, + contentHeight: 0 + } + }, + components: { + Modal + }, + computed: { + pleromaTanStyles () { + return { + 'shape-outside': 'url(' + this.pleromaTanVariant + ')' + } + }, + shouldShow () { + return this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER && + !this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs + } + }, + methods: { + toggleShow () { + this.showingMore = !this.showingMore + }, + neverShowAgain () { + this.toggleShow() + this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) + this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 }) + this.$store.dispatch('pushServerSideStorage') + }, + dismiss () { + this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) + this.$store.dispatch('pushServerSideStorage') + } + }, + mounted () { + setTimeout(() => { + this.contentHeight = this.$refs.content.offsetHeight + }, 10) + } +} + +export default UpdateNotification diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss new file mode 100644 index 00000000..c55bb192 --- /dev/null +++ b/src/components/update_notification/update_notification.scss @@ -0,0 +1,106 @@ +@import 'src/_variables.scss'; +.UpdateNotification { + overflow: hidden; +} +.UpdateNotificationModal { + --__top-fringe: 18em; // how much pleroma-tan should stick her head above + --__bottom-fringe: 80em; // just reserving as much as we can, number is mostly irrelevant + --__right-fringe: 8em; + + font-size: 15px; + /* Explanation: + * Modal is positioned vertically centered. + * 100vh - 100% = Distance between modal's top+bottom boundaries and screen + * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen + * + 10% - we move modal completely off-screen, it's top boundary touches + * bottom of the screen + * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible + */ + position: relative; + transition: transform; + transition-timing-function: ease-in-out; + transition-duration: 500ms; + + .text { + width: 40em; + padding-left: 1em; + } + + @media all and (max-width: 800px) { + /* For mobile, the modal takes 100% of the available screen. + This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible. + */ + width: 100vw; + } + + @media all and (max-height: 600px) { + display: none; + } + + .content { + overflow: hidden; + margin-top: calc(-1 * var(--__top-fringe)); + margin-bottom: calc(-1 * var(--__bottom-fringe)); + margin-right: calc(-1 * var(--__right-fringe)); + } + + .panel-body { + border-width: 0 0 1px 0; + border-style: solid; + border-color: var(--border, $fallback--border); + } + + .panel-footer { + z-index: 22; + position: relative; + border-width: 0; + grid-template-columns: auto; + } + + .pleroma-tan { + object-fit: cover; + object-position: top; + transition: position, left, right, top, bottom, max-width, max-height; + transition-timing-function: ease-in-out; + transition-duration: 500ms; + width: 25em; + float: right; + z-index: 20; + position: relative; + shape-margin: 0.5em; + filter: drop-shadow(5px 5px 10px rgba(0,0,0,0.5)); + } + + .spacer-top { + min-height: var(--__top-fringe); + } + + .spacer-bottom { + min-height: var(--__bottom-fringe); + } + + .extra-info { + transition: max-height, padding, height; + transition-timing-function: ease-in-out; + transition-duration: 500ms; + max-height: auto; + height: auto; + } + + &.-peek { + transform: translateY(calc(((100vh - 100%) / 2))); + + .pleroma-tan { + float: right; + z-index: 10; + shape-image-threshold: 0.7; + } + + .extra-info { + max-height: 0; + height: 0; + display: none; + overflow: hidden; + } + } +} diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue new file mode 100644 index 00000000..07e91ec9 --- /dev/null +++ b/src/components/update_notification/update_notification.vue @@ -0,0 +1,61 @@ +<template> + <Modal + :is-open="shouldShow" + class="UpdateNotification" + :no-background="true" + > + <div + class="UpdateNotificationModal panel" + :class="{ '-peek': !showingMore }" + > + <div class="panel-heading"> + <span class="title"> + {{ $t('update.big_update_title') }} + </span> + </div> + <div class="panel-body"> + <div class="content" ref="content"> + <img class="pleroma-tan" :src="pleromaTanVariant" :style="pleromaTanStyles"/> + <div class="spacer-top"/> + <div class="text"> + <p> + {{ $t('update.big_update_content') }} + </p> + <p class="extra-info"> + {{ $t('update.update_bugs') }} + </p> + <p class="extra-info"> + {{ $t('update.update_changelog') }} + </p> + </div> + <div class="spacer-bottom"/> + </div> + </div> + <div class="panel-footer"> + <button + class="button-default" + @click.prevent="neverShowAgain" + > + {{ $t("general.never_show_again") }} + </button> + <button + class="button-default" + @click.prevent="toggleShow" + v-if="!showingMore" + > + {{ $t("general.show_more") }} + </button> + <button + class="button-default" + @click.prevent="dismiss" + > + {{ $t("general.dismiss") }} + </button> + </div> + </div> + </Modal> +</template> + +<script src="./update_notification.js"></script> + +<style src="./update_notification.scss" lang="scss"></style> diff --git a/src/i18n/en.json b/src/i18n/en.json index c54d4750..3945e6f9 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -71,6 +71,7 @@ "optional": "optional", "show_more": "Show more", "show_less": "Show less", + "never_show_again": "Never show again", "dismiss": "Dismiss", "cancel": "Cancel", "disable": "Disable", @@ -961,5 +962,13 @@ }, "display_date": { "today": "Today" + }, + "update": { + "big_update_title": "Please bear with us", + "big_update_content": "We haven't had a release in a while, so things might look and feel different than what you're used to.", + "update_bugs": "Please report any issues and bugs on {pleromaGitlab}, as we have changed a lot, and although we test thoroughly and use development versions ourselves, we may have missed some things. We welcome your feedback and suggestions on issues you might encounter, or how to improve Pleroma and Pleroma-FE.", + "update_bugs_gitlab": "Pleroma GitLab", + "update_changelog": "For more details on what's changed, see {theFullChangelog}", + "update_changelog_here": "here" } } diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index c73a38ec..6d59c595 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -17,6 +17,7 @@ const saveImmedeatelyActions = [ 'markNotificationsAsSeen', 'clearCurrentUser', 'setCurrentUser', + 'setServerSideStorage', 'setHighlight', 'setOption', 'setClientData', diff --git a/src/main.js b/src/main.js index eacd554c..fc798ed6 100644 --- a/src/main.js +++ b/src/main.js @@ -10,6 +10,7 @@ import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' import serverSideConfigModule from './modules/serverSideConfig.js' +import serverSideStorageModule from './modules/serverSideStorage.js' import shoutModule from './modules/shout.js' import oauthModule from './modules/oauth.js' import authFlowModule from './modules/auth_flow.js' @@ -42,6 +43,7 @@ messages.setLanguage(i18n, currentLocale) const persistedStateOptions = { paths: [ + 'serverSideStorage.cache', 'config', 'users.lastLoginName', 'oauth' @@ -73,6 +75,7 @@ const persistedStateOptions = { api: apiModule, config: configModule, serverSideConfig: serverSideConfigModule, + serverSideStorage: serverSideStorageModule, shout: shoutModule, oauth: oauthModule, authFlow: authFlowModule, diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js new file mode 100644 index 00000000..fd3fe781 --- /dev/null +++ b/src/modules/serverSideStorage.js @@ -0,0 +1,230 @@ +import { toRaw } from 'vue' +import { isEqual, cloneDeep } from 'lodash' +import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' + +export const VERSION = 1 +export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically + +export const COMMAND_TRIM_FLAGS = 1000 +export const COMMAND_TRIM_FLAGS_AND_RESET = 1001 + +export const defaultState = { + // do we need to update data on server? + dirty: false, + // storage of flags - stuff that can only be set and incremented + flagStorage: { + updateCounter: 0, // Counter for most recent update notification seen + // TODO move to prefsStorage when that becomes a thing since only way + // this can be reset is by complete reset of all flags + dontShowUpdateNotifs: 0, // if user chose to not show update notifications ever again + reset: 0 // special flag that can be used to force-reset all flags, debug purposes only + // special reset codes: + // 1000: trim keys to those known by currently running FE + // 1001: same as above + reset everything to 0 + }, + // raw data + raw: null, + // local cache + cache: null +} + +export const newUserFlags = { + ...defaultState.flagStorage, + updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification +} + +const _wrapData = (data) => ({ + ...data, + _timestamp: Date.now(), + _version: VERSION +}) + +const _checkValidity = (data) => data._timestamp > 0 && data._version > 0 + +export const _getRecentData = (cache, live) => { + const result = { recent: null, stale: null, needUpload: false } + const cacheValid = _checkValidity(cache || {}) + const liveValid = _checkValidity(live || {}) + if (!liveValid && cacheValid) { + result.needUpload = true + console.debug('Nothing valid stored on server, assuming cache to be source of truth') + result.recent = cache + result.stale = live + } else if (!cacheValid && liveValid) { + console.debug('Valid storage on server found, no local cache found, using live as source of truth') + result.recent = live + result.stale = cache + } else if (cacheValid && liveValid) { + console.debug('Both sources have valid data, figuring things out...') + if (live._timestamp === cache._timestamp && live._version === cache._version) { + console.debug('Same version/timestamp on both source, source of truth irrelevant') + result.recent = cache + result.stale = live + } else { + console.debug('Different timestamp, figuring out which one is more recent') + if (live._timestamp < cache._timestamp) { + result.recent = cache + result.stale = live + } else { + result.recent = live + result.stale = cache + } + } + } else { + console.debug('Both sources are invalid, start from scratch') + result.needUpload = true + } + return result +} + +export const _getAllFlags = (recent, stale) => { + return Array.from(new Set([ + ...Object.keys(toRaw((recent || {}).flagStorage || {})), + ...Object.keys(toRaw((stale || {}).flagStorage || {})) + ])) +} + +export const _mergeFlags = (recent, stale, allFlagKeys) => { + return Object.fromEntries(allFlagKeys.map(flag => { + const recentFlag = recent.flagStorage[flag] + const staleFlag = stale.flagStorage[flag] + // use flag that is of higher value + return [flag, Number((recentFlag > staleFlag ? recentFlag : staleFlag) || 0)] + })) +} + +export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => { + let result = { ...totalFlags } + const allFlagKeys = Object.keys(totalFlags) + // flag reset functionality + if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) { + console.debug('Received command to trim the flags') + const knownKeysSet = new Set(Object.keys(knownKeys)) + + // Trim + result = {} + allFlagKeys.forEach(flag => { + if (knownKeysSet.has(flag)) { + result[flag] = totalFlags[flag] + } + }) + + // Reset + if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) { + // 1001 - and reset everything to 0 + console.debug('Received command to reset the flags') + Object.keys(knownKeys).forEach(flag => { result[flag] = 0 }) + } + } else if (totalFlags.reset > 0 && totalFlags.reset < 9000) { + console.debug('Received command to reset the flags') + allFlagKeys.forEach(flag => { result[flag] = 0 }) + } + result.reset = 0 + return result +} + +export const _doMigrations = (cache) => { + if (!cache) return cache + + if (cache._version < VERSION) { + console.debug('Local cached 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) { + console.debug('Local cached 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 + if (window._PLEROMA_HOTPATCH) { + if (window._PLEROMA_HOTPATCH.reverseMigrations) { + console.debug('Found hotpatch migration, applying') + return window._PLEROMA_HOTPATCH.reverseMigrations('serverSideStorage', { from: cache._version, to: VERSION }, cache) + } + } + } + + return cache +} + +export const mutations = { + setServerSideStorage (state, userData) { + const live = userData.storage + state.raw = live + let cache = state.cache + + cache = _doMigrations(cache) + + let { recent, stale, needsUpload } = _getRecentData(cache, live) + + const userNew = userData.created_at > NEW_USER_DATE + const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage + let dirty = false + + if (recent === null) { + console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`) + recent = _wrapData({ + flagStorage: { ...flagsTemplate } + }) + } + + if (!needsUpload && recent && stale) { + console.debug('Checking if data needs merging...') + // discarding timestamps and versions + const { _timestamp: _0, _version: _1, ...recentData } = recent + const { _timestamp: _2, _version: _3, ...staleData } = stale + dirty = !isEqual(recentData, staleData) + console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`) + } + + const allFlagKeys = _getAllFlags(recent, stale) + let totalFlags + if (dirty) { + // Merge the flags + console.debug('Merging the flags...') + totalFlags = _mergeFlags(recent, stale, allFlagKeys) + } else { + totalFlags = recent.flagStorage + } + + totalFlags = _resetFlags(totalFlags) + + recent.flagStorage = totalFlags + + state.dirty = dirty || needsUpload + state.cache = recent + // set local timestamp to smaller one if we don't have any changes + if (stale && recent && !state.dirty) { + state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp) + } + state.flagStorage = state.cache.flagStorage + }, + setFlag (state, { flag, value }) { + state.flagStorage[flag] = value + state.dirty = true + } +} + +const serverSideStorage = { + state: { + ...cloneDeep(defaultState) + }, + mutations, + actions: { + pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) { + const needPush = state.dirty || force + if (!needPush) return + state.cache = _wrapData({ + flagStorage: toRaw(state.flagStorage) + }) + const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } } + rootState.api.backendInteractor + .updateProfile({ params }) + .then((user) => commit('setServerSideStorage', user)) + state.dirty = false + } + } +} + +export default serverSideStorage diff --git a/src/modules/users.js b/src/modules/users.js index 13d4e318..b6fb9746 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -525,6 +525,7 @@ const users = { user.muteIds = [] user.domainMutes = [] commit('setCurrentUser', user) + commit('setServerSideStorage', user) commit('addNewUsers', [user]) store.dispatch('fetchEmoji') @@ -534,6 +535,7 @@ const users = { // Set our new backend interactor commit('setBackendInteractor', backendInteractorService(accessToken)) + store.dispatch('pushServerSideStorage') if (user.token) { store.dispatch('setWsToken', user.token) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 7f831ed9..e6a87b63 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -90,6 +90,9 @@ export const parseUser = (data) => { output.bot = data.bot if (data.pleroma) { + if (data.pleroma.settings_store) { + output.storage = data.pleroma.settings_store['pleroma-fe'] + } const relationship = data.pleroma.relationship output.background_image = data.pleroma.background_image |
