diff options
| -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 | 66 | ||||
| -rw-r--r-- | src/components/update_notification/update_notification.scss | 107 | ||||
| -rw-r--r-- | src/components/update_notification/update_notification.vue | 100 | ||||
| -rw-r--r-- | src/i18n/en.json | 10 | ||||
| -rw-r--r-- | src/lib/persisted_state.js | 1 | ||||
| -rw-r--r-- | src/main.js | 3 | ||||
| -rw-r--r-- | src/modules/instance.js | 1 | ||||
| -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 | ||||
| -rw-r--r-- | static/config.json | 1 | ||||
| -rw-r--r-- | test/unit/specs/modules/serverSideStorage.spec.js | 178 |
16 files changed, 704 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..ba008d81 --- /dev/null +++ b/src/components/update_notification/update_notification.js @@ -0,0 +1,66 @@ +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 + ')' + } + }, + dynamicStyles () { + return { + '--____extraInfoGroupHeight': this.contentHeight + 'px' + } + }, + shouldShow () { + return !this.$store.state.instance.disableUpdateNotification && + this.$store.state.users.currentUser && + 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.animatedText.scrollHeight + }, 1000) + } +} + +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..8cad1bc7 --- /dev/null +++ b/src/components/update_notification/update_notification.scss @@ -0,0 +1,107 @@ +@import 'src/_variables.scss'; +.UpdateNotification { + overflow: hidden; +} + +.UpdateNotificationModal { + --__top-fringe: 15em; // 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; + position: relative; + transition: transform; + transition-timing-function: ease-in-out; + transition-duration: 500ms; + + .text { + max-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)); + pointer-events: none; + } + + .spacer-top { + min-height: var(--__top-fringe); + } + + .spacer-bottom { + min-height: var(--__bottom-fringe); + } + + .extra-info-group { + transition: max-height, padding, height; + transition-timing-function: ease-in-out; + transition-duration: 500ms; + max-height: calc(var(--____extraInfoGroupHeight) + 1em); // include bottom padding + mask: + linear-gradient(to top, white, transparent) bottom/100% 2px no-repeat, + linear-gradient(to top, white, white); + } + + .art-credit { + text-align: right; + } + + &.-peek { + /* Explanation: + * 100vh - 100% = Distance between modal's top+bottom boundaries and screen + * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen + */ + transform: translateY(calc(((100vh - 100%) / 2))); + + .pleroma-tan { + float: right; + z-index: 10; + shape-image-threshold: 0.7; + } + + .extra-info-group { + max-height: 0; + } + } +} diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue new file mode 100644 index 00000000..d0e2499c --- /dev/null +++ b/src/components/update_notification/update_notification.vue @@ -0,0 +1,100 @@ +<template> + <Modal + :is-open="!!shouldShow" + class="UpdateNotification" + :no-background="true" + > + <div + class="UpdateNotificationModal panel" + :class="{ '-peek': !showingMore }" + :style="dynamicStyles" + > + <div class="panel-heading"> + <span class="title"> + {{ $t('update.big_update_title') }} + </span> + </div> + <div class="panel-body"> + <div class="content"> + <img + class="pleroma-tan" + :src="pleromaTanVariant" + :style="pleromaTanStyles" + > + <div class="spacer-top" /> + <div class="text"> + <p> + {{ $t('update.big_update_content') }} + </p> + <div + ref="animatedText" + class="extra-info-group" + > + <i18n-t + keypath="update.update_bugs" + tag="p" + > + <template #pleromaGitlab> + <a + target="_blank" + href="https://git.pleroma.social/" + >{{ $t('update.update_bugs_gitlab') }}</a> + </template> + </i18n-t> + <i18n-t + keypath="update.update_changelog" + tag="p" + > + <template #theFullChangelog> + <a + target="_blank" + href="https://pleroma.social/announcements/" + >{{ $t('update.update_changelog_here') }}</a> + </template> + </i18n-t> + <p class="art-credit"> + <i18n-t + keypath="update.art_by" + tag="small" + > + <template #linkToArtist> + <a + target="_blank" + href="https://post.ebin.club/pipivovott" + >pipivovott</a> + </template> + </i18n-t> + </p> + </div> + </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 + v-if="!showingMore" + class="button-default" + @click.prevent="toggleShow" + > + {{ $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..ae3be443 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,14 @@ }, "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": "the full changelog", + "art_by": "Art by {linkToArtist}" } } 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/instance.js b/src/modules/instance.js index 220463ca..bfce6f38 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -41,6 +41,7 @@ const defaultState = { logoMargin: '.2em', logoMask: true, logoLeft: false, + disableUpdateNotification: false, minimalScopesMode: false, nsfwCensorImage: undefined, postContentType: 'text/plain', diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js new file mode 100644 index 00000000..e516a6e6 --- /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.call({}, '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 diff --git a/static/config.json b/static/config.json index 53a4be82..fb39ff77 100644 --- a/static/config.json +++ b/static/config.json @@ -14,6 +14,7 @@ "logoMask": true, "logoLeft": false, "minimalScopesMode": false, + "disableUpdateNotification": false, "nsfwCensorImage": "", "postContentType": "text/plain", "redirectRootLogin": "/main/friends", diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js new file mode 100644 index 00000000..e06c6ada --- /dev/null +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -0,0 +1,178 @@ +import { cloneDeep } from 'lodash' + +import { + VERSION, + COMMAND_TRIM_FLAGS, + COMMAND_TRIM_FLAGS_AND_RESET, + _getRecentData, + _getAllFlags, + _mergeFlags, + _resetFlags, + mutations, + defaultState, + newUserFlags +} from 'src/modules/serverSideStorage.js' + +describe('The serverSideStorage module', () => { + describe('mutations', () => { + describe('setServerSideStorage', () => { + const { setServerSideStorage } = mutations + const user = { + created_at: new Date('1999-02-09'), + storage: {} + } + + it('should initialize storage if none present', () => { + const state = cloneDeep(defaultState) + setServerSideStorage(state, user) + expect(state.cache._version).to.eql(VERSION) + expect(state.cache._timestamp).to.be.a('number') + expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + }) + + it('should initialize storage with proper flags for new users if none present', () => { + const state = cloneDeep(defaultState) + setServerSideStorage(state, { ...user, created_at: new Date() }) + expect(state.cache._version).to.eql(VERSION) + expect(state.cache._timestamp).to.be.a('number') + expect(state.cache.flagStorage).to.eql(newUserFlags) + }) + + it('should merge flags even if remote timestamp is older', () => { + const state = { + ...cloneDeep(defaultState), + cache: { + _timestamp: Date.now(), + _version: VERSION, + ...cloneDeep(defaultState) + } + } + setServerSideStorage( + state, + { + ...user, + storage: { + _timestamp: 123, + _version: VERSION, + flagStorage: { + ...defaultState.flagStorage, + updateCounter: 1 + } + } + } + ) + expect(state.cache.flagStorage).to.eql({ + ...defaultState.flagStorage, + updateCounter: 1 + }) + }) + + it('should reset local timestamp to remote if contents are the same', () => { + const state = { + ...cloneDeep(defaultState), + cache: null + } + setServerSideStorage( + state, + { + ...user, + storage: { + _timestamp: 123, + _version: VERSION, + flagStorage: { + ...defaultState.flagStorage, + updateCounter: 999 + } + } + } + ) + expect(state.cache._timestamp).to.eql(123) + expect(state.flagStorage.updateCounter).to.eql(999) + expect(state.cache.flagStorage.updateCounter).to.eql(999) + }) + + it('should remote version if local missing', () => { + const state = cloneDeep(defaultState) + setServerSideStorage(state, user) + expect(state.cache._version).to.eql(VERSION) + expect(state.cache._timestamp).to.be.a('number') + expect(state.cache.flagStorage).to.eql(defaultState.flagStorage) + }) + }) + }) + + describe('helper functions', () => { + describe('_getRecentData', () => { + it('should handle nulls correctly', () => { + expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true }) + }) + + it('doesn\'t choke on invalid data', () => { + expect(_getRecentData({ a: 1 }, { b: 2 })).to.eql({ recent: null, stale: null, needUpload: true }) + }) + + it('should prefer the valid non-null correctly, needUpload works properly', () => { + const nonNull = { _version: VERSION, _timestamp: 1 } + expect(_getRecentData(nonNull, null)).to.eql({ recent: nonNull, stale: null, needUpload: true }) + expect(_getRecentData(null, nonNull)).to.eql({ recent: nonNull, stale: null, needUpload: false }) + }) + + it('should prefer the one with higher timestamp', () => { + const a = { _version: VERSION, _timestamp: 1 } + const b = { _version: VERSION, _timestamp: 2 } + + expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false }) + expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false }) + }) + + it('case where both are same', () => { + const a = { _version: VERSION, _timestamp: 3 } + const b = { _version: VERSION, _timestamp: 3 } + + expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false }) + expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false }) + }) + }) + + describe('_getAllFlags', () => { + it('should handle nulls properly', () => { + expect(_getAllFlags(null, null)).to.eql([]) + }) + it('should output list of keys if passed single object', () => { + expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, null)).to.eql(['a', 'b', 'c']) + }) + it('should union keys of both objects', () => { + expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, { flagStorage: { c: 1, d: 1 } })).to.eql(['a', 'b', 'c', 'd']) + }) + }) + + describe('_mergeFlags', () => { + it('should handle merge two flag sets correctly picking higher numbers', () => { + expect( + _mergeFlags( + { flagStorage: { a: 0, b: 3 } }, + { flagStorage: { b: 1, c: 4, d: 9 } }, + ['a', 'b', 'c', 'd']) + ).to.eql({ a: 0, b: 3, c: 4, d: 9 }) + }) + }) + + describe('_resetFlags', () => { + it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => { + const totalFlags = { a: 0, b: 3, reset: 1 } + + expect(_resetFlags(totalFlags)).to.eql({ a: 0, b: 0, reset: 0 }) + }) + it('should trim all flags to known when reset is set to 1000', () => { + const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS } + + expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 3, reset: 0 }) + }) + it('should trim all flags to known and reset when reset is set to 1001', () => { + const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS_AND_RESET } + + expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 0, reset: 0 }) + }) + }) + }) +}) |
