server-side storage for flags

This commit is contained in:
Henry Jameson 2022-08-04 01:56:52 +03:00
parent 5b7c653874
commit dbfca224d8
8 changed files with 326 additions and 34 deletions

View File

@ -10,15 +10,49 @@ library.add(
faTimes faTimes
) )
const SettingsModal = { const CURRENT_UPDATE_COUNTER = 1
const UpdateNotification = {
data () { data () {
return { return {
pleromaTanVariant: Math.random() > 0.5 ? pleromaTan : pleromaTanFox pleromaTanVariant: Math.random() > 0.5 ? pleromaTan : pleromaTanFox,
showingMore: true,
contentHeight: 0
} }
}, },
components: { components: {
Modal 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.$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 SettingsModal export default UpdateNotification

View File

@ -1,5 +1,13 @@
@import 'src/_variables.scss'; @import 'src/_variables.scss';
.UpdateNotification {
overflow: hidden;
}
.UpdateNotificationModal { .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: /* Explanation:
* Modal is positioned vertically centered. * Modal is positioned vertically centered.
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen * 100vh - 100% = Distance between modal's top+bottom boundaries and screen
@ -8,27 +16,90 @@
* bottom of the screen * bottom of the screen
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
*/ */
transform: translateY(calc(((100vh - 100%) / 2 + 5%)));
max-width: 90vh;
width: 30em;
position: relative; 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) { @media all and (max-width: 800px) {
/* For mobile, the modal takes 100% of the available screen. /* 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. This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
*/ */
transform: translateY(calc(100% - 50px)); width: 100vw;
} }
.panel-body > p {
width: calc(100% - 10em) @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 { .pleroma-tan {
max-width: 20em; object-fit: cover;
max-height: 40em; object-position: top;
position: absolute; transition: position, left, right, top, bottom, max-width, max-height;
right: -5em; transition-timing-function: ease-in-out;
top: -10em; transition-duration: 500ms;
width: 25em;
float: right;
z-index: 20;
position: relative;
shape-margin: 0.5em;
}
.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; z-index: 10;
shape-image-threshold: 0.7;
}
.extra-info {
max-height: 0;
height: 0;
display: none;
overflow: hidden;
}
} }
} }

View File

@ -1,36 +1,56 @@
<template> <template>
<Modal <Modal
:is-open="true" :is-open="shouldShow"
class="UpdateNotification" class="UpdateNotification"
:class="{ peek: modalPeeked }"
:no-background="true" :no-background="true"
> >
<div class="UpdateNotificationModal panel"> <div
class="UpdateNotificationModal panel"
:class="{ '-peek': !showingMore }"
>
<div class="panel-heading"> <div class="panel-heading">
<span class="title"> <span class="title">
{{ $t('update.big_update_title') }} {{ $t('update.big_update_title') }}
</span> </span>
</div> </div>
<div class="panel-body"> <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> <p>
{{ $t('update.big_update_content') }} {{ $t('update.big_update_content') }}
</p> </p>
<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 <button
class="button-unstyled -link tall-status-hider" class="button-default"
@click.prevent="toggleShowMore" @click.prevent="neverShowAgain"
>
{{ $t("general.show_more") }}
</button>
{{ ' ' }}
<button
class="button-unstyled -link tall-status-hider"
@click.prevent="toggleShowMore"
> >
{{ $t("general.never_show_again") }} {{ $t("general.never_show_again") }}
</button> </button>
</p> <button
<img class="pleroma-tan" :src="pleromaTanVariant"/> class="button-default"
@click.prevent="toggleShowMore"
v-if="!showingMore"
>
{{ $t("general.show_more") }}
</button>
<button
class="button-default"
@click.prevent="dismiss"
>
{{ $t("general.dismiss") }}
</button>
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@ -17,6 +17,7 @@ const saveImmedeatelyActions = [
'markNotificationsAsSeen', 'markNotificationsAsSeen',
'clearCurrentUser', 'clearCurrentUser',
'setCurrentUser', 'setCurrentUser',
'setServerSideStorage',
'setHighlight', 'setHighlight',
'setOption', 'setOption',
'setClientData', 'setClientData',

View File

@ -10,6 +10,7 @@ import usersModule from './modules/users.js'
import apiModule from './modules/api.js' import apiModule from './modules/api.js'
import configModule from './modules/config.js' import configModule from './modules/config.js'
import serverSideConfigModule from './modules/serverSideConfig.js' import serverSideConfigModule from './modules/serverSideConfig.js'
import serverSideStorageModule from './modules/serverSideStorage.js'
import shoutModule from './modules/shout.js' import shoutModule from './modules/shout.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import authFlowModule from './modules/auth_flow.js' import authFlowModule from './modules/auth_flow.js'
@ -42,6 +43,7 @@ messages.setLanguage(i18n, currentLocale)
const persistedStateOptions = { const persistedStateOptions = {
paths: [ paths: [
'serverSideStorage.cache',
'config', 'config',
'users.lastLoginName', 'users.lastLoginName',
'oauth' 'oauth'
@ -73,6 +75,7 @@ const persistedStateOptions = {
api: apiModule, api: apiModule,
config: configModule, config: configModule,
serverSideConfig: serverSideConfigModule, serverSideConfig: serverSideConfigModule,
serverSideStorage: serverSideStorageModule,
shout: shoutModule, shout: shoutModule,
oauth: oauthModule, oauth: oauthModule,
authFlow: authFlowModule, authFlow: authFlowModule,

View File

@ -0,0 +1,158 @@
import { toRaw } from 'vue'
const VERSION = 1
const NEW_USER_DATE = new Date('04-08-2022') // date of writing this, basically
const COMMAND_TRIM_FLAGS = 1000
const COMMAND_TRIM_FLAGS_AND_RESET = 1001
const defaultState = {
// last timestamp
timestamp: 0,
// need to update 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
}
const newUserFlags = {
...defaultState.flagStorage,
updateCounter: 1 // new users don't need to see update notification
}
const serverSideStorage = {
state: {
...defaultState
},
mutations: {
setServerSideStorage (state, userData) {
const live = userData.storage
const userNew = userData.created_at > NEW_USER_DATE
const flagsTemplate = userNew ? newUserFlags : defaultState.defaultState
state.raw = live
console.log(1111, live._timestamp)
let recent = null
const cache = state.cache || {}
const cacheValid = cache._timestamp > 0 && cache._version > 0
const liveValid = live._timestamp > 0 && live._version > 0
if (!liveValid) {
state.dirty = true
console.debug('Nothing valid stored on server, assuming cache to be source of truth')
if (cacheValid) {
recent = cache
} else {
console.debug(`Local cache is empty, initializing for ${userNew ? 'new' : 'existing'} user`)
recent = {
_timestamp: Date.now(),
_version: VERSION,
flagStorage: { ...flagsTemplate }
}
}
} else if (!cacheValid) {
console.debug('Valid storage on server found, no local cache found, using live as source of truth')
recent = live
} else {
console.debug('Both sources have valid data, figuring things out...')
console.log(live._timestamp, cache._timestamp)
if (live._timestamp === cache._timestamp && live._version === cache._version) {
console.debug('Same version/timestamp on both source, source of truth irrelevant')
recent = cache
} else {
state.dirty = true
console.debug('Different timestamp, figuring out which one is more recent')
let stale
if (live._timestamp < cache._timestamp) {
recent = cache
stale = live
} else {
recent = live
stale = cache
}
// Merge the flags
console.debug('Merging the flags...')
recent.flagStorage = recent.flagStorage || { ...flagsTemplate }
stale.flagStorage = stale.flagStorage || { ...flagsTemplate }
const allFlags = Array.from(new Set([
...Object.keys(toRaw(recent.flagStorage)),
...Object.keys(toRaw(stale.flagStorage))
]))
const totalFlags = Object.fromEntries(allFlags.map(flag => {
const recentFlag = recent.flagStorage[flag]
const staleFlag = stale.flagStorage[flag]
// use flag that is of higher value
return [flag, recentFlag > staleFlag ? recentFlag : staleFlag]
}))
console.debug('AAA', 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 knownKeys = new Set(Object.keys(defaultState.flagStorage))
allFlags.forEach(flag => {
if (!knownKeys.has(flag)) {
delete totalFlags[flag]
}
})
if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) {
// 1001 - and reset everything to 0
console.debug('Received command to reset the flags')
allFlags.forEach(flag => { totalFlags[flag] = 0 })
} else {
// reset the reset 0
totalFlags.reset = 0
}
} else if (totalFlags.reset > 0 && totalFlags.reset < 9000) {
console.debug('Received command to reset the flags')
allFlags.forEach(flag => { totalFlags[flag] = 0 })
// for good luck
totalFlags.reset = 0
}
console.log('AAAA', totalFlags)
state.cache.flagStorage = totalFlags
}
}
state.cache = recent
state.flagStorage = state.cache.flagStorage
},
setFlag (state, { flag, value }) {
state.flagStorage[flag] = value
state.dirty = true
}
},
actions: {
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
console.log('PUSH')
const needPush = state.dirty || force
if (!needPush) return
state.cache = {
_timestamp: Date.now(),
_version: VERSION,
flagStorage: toRaw(state.flagStorage)
}
console.log('YES')
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

View File

@ -525,6 +525,7 @@ const users = {
user.muteIds = [] user.muteIds = []
user.domainMutes = [] user.domainMutes = []
commit('setCurrentUser', user) commit('setCurrentUser', user)
commit('setServerSideStorage', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])
store.dispatch('fetchEmoji') store.dispatch('fetchEmoji')
@ -534,6 +535,7 @@ const users = {
// Set our new backend interactor // Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken)) commit('setBackendInteractor', backendInteractorService(accessToken))
store.dispatch('pushServerSideStorage')
if (user.token) { if (user.token) {
store.dispatch('setWsToken', user.token) store.dispatch('setWsToken', user.token)

View File

@ -90,6 +90,9 @@ export const parseUser = (data) => {
output.bot = data.bot output.bot = data.bot
if (data.pleroma) { if (data.pleroma) {
if (data.pleroma.settings_store) {
output.storage = data.pleroma.settings_store['pleroma-fe']
}
const relationship = data.pleroma.relationship const relationship = data.pleroma.relationship
output.background_image = data.pleroma.background_image output.background_image = data.pleroma.background_image