Merge branch 'been-awhile' into 'develop'

Pleroma-tan apologizing for all the changes

See merge request pleroma/pleroma-fe!1581
This commit is contained in:
HJ 2022-08-09 21:30:53 +00:00
commit 346e806153
16 changed files with 704 additions and 0 deletions

View file

@ -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

View file

@ -65,6 +65,7 @@
<UserReportingModal />
<PostStatusModal />
<SettingsModal />
<UpdateNotification />
<div id="modal" />
<GlobalNoticeList />
<div id="popovers" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

View file

@ -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

View file

@ -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;
}
}
}

View file

@ -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>

View file

@ -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}"
}
}

View file

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

View file

@ -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,

View file

@ -41,6 +41,7 @@ const defaultState = {
logoMargin: '.2em',
logoMask: true,
logoLeft: false,
disableUpdateNotification: false,
minimalScopesMode: false,
nsfwCensorImage: undefined,
postContentType: 'text/plain',

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -14,6 +14,7 @@
"logoMask": true,
"logoLeft": false,
"minimalScopesMode": false,
"disableUpdateNotification": false,
"nsfwCensorImage": "",
"postContentType": "text/plain",
"redirectRootLogin": "/main/friends",

View file

@ -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 })
})
})
})
})