+
-
{{ $t('settings.posts') }}
+
{{ $t('settings.filter.clutter') }}
+
+ -
+
+ {{ $t('settings.replies_in_timeline') }}
+
+
+ -
+
+ {{ $t('settings.hide_post_stats') }}
+
+
+ -
+
+ {{ $t('settings.hide_user_stats') }}
+
+
+ -
+
+ {{ $t('settings.hide_actor_type_indication') }}
+
+
+ -
+
+ {{ $t('settings.hide_scrobbles') }}
+
+
+ -
+
+ {{ $t('settings.hide_scrobbles_after') }}
+
+
+
+
+ {{ $t('settings.attachments') }}
+ -
+
+ {{ $t('settings.max_thumbnails') }}
+
+
+ -
+
+ {{ $t('settings.hide_attachments_in_tl') }}
+
+
+ -
+
+ {{ $t('settings.hide_attachments_in_convo') }}
+
+
+
+
+
+
{{ $t('settings.filter.mute_filter') }}
-
- {{ $t('settings.hide_filtered_statuses') }}
+ {{ $t('settings.hide_muted_statuses') }}
-
-
-
{{ $t('settings.user_profiles') }}
-
- -
-
- {{ $t('settings.hide_user_stats') }}
-
+
{{ $t('settings.filter.custom_filters') }}
+
+
+ {{ $t('settings.filter.total_count', { count: muteFiltersDraft.length }) }}
+
+
+
+
+
+
+
+
+ {{ ' ' }}
+
+
+ {{ $t('settings.filter.expired') }}
+
+
+
+
+ {{ $t('settings.filter.hide') }}
+
+ {{ ' ' }}
+
+ {{ $t('settings.enabled') }}
+
+
+
+
+
+
+
+
+ {{ ' ' }}
+
+
+
+
+ {{ ' ' }}
+
+
+ {{ ' ' }}
+
+ {{ $t('settings.filter.never_expires') }}
+
+
+
+
+ {{ $t('settings.filter.regexp_error') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.filter.expired_count', { count: muteFiltersExpired.length }) }}
+
+
+
+
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js
index d2162c30c..8324263cd 100644
--- a/src/components/settings_modal/tabs/style_tab/style_tab.js
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.js
@@ -643,7 +643,7 @@ export default {
parser (string) { return deserialize(string) },
onImportFailure (result) {
console.error('Failure importing style:', result)
- this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
+ useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
},
onImport
})
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 4f8ba31e4..5b566fceb 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -14,8 +14,9 @@ import MentionLink from 'src/components/mention_link/mention_link.vue'
import StatusActionButtons from 'src/components/status_action_buttons/status_action_buttons.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { muteWordHits } from '../../services/status_parser/status_parser.js'
+import { muteFilterHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
+import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -161,9 +162,6 @@ const Status = {
},
computed: {
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
- muteWords () {
- return this.mergedConfig.muteWords
- },
showReasonMutedThread () {
return (
this.status.thread_muted ||
@@ -221,8 +219,11 @@ const Status = {
loggedIn () {
return !!this.currentUser
},
- muteWordHits () {
- return muteWordHits(this.status, this.muteWords)
+ muteFilterHits () {
+ return muteFilterHits(
+ Object.values(useServerSideStorageStore().prefsStorage.simple.muteFilters),
+ this.status
+ )
},
botStatus () {
return this.status.user.actor_type === 'Service'
@@ -256,7 +257,7 @@ const Status = {
return [
this.userIsMuted ? 'user' : null,
this.status.thread_muted ? 'thread' : null,
- (this.muteWordHits.length > 0) ? 'wordfilter' : null,
+ (this.muteFilterHits.length > 0) ? 'filtered' : null,
(this.muteBotStatuses && this.botStatus) ? 'bot' : null,
(this.muteSensitiveStatuses && this.sensitiveStatus) ? 'nsfw' : null
].filter(_ => _)
@@ -267,14 +268,14 @@ const Status = {
switch (this.muteReasons[0]) {
case 'user': return this.$t('status.muted_user')
case 'thread': return this.$t('status.thread_muted')
- case 'wordfilter':
+ case 'filtered':
return this.$t(
- 'status.muted_words',
+ 'status.muted_filters',
{
- word: this.muteWordHits[0],
- numWordsMore: this.muteWordHits.length - 1
+ name: this.muteFilterHits[0].name,
+ filterMore: this.muteFilterHits.length - 1
},
- this.muteWordHits.length
+ this.muteFilterHits.length
)
case 'bot': return this.$t('status.bot_muted')
case 'nsfw': return this.$t('status.sensitive_muted')
@@ -326,7 +327,7 @@ const Status = {
// Don't mute statuses in muted conversation when said conversation is opened
(this.inConversation && status.thread_muted)
// No excuses if post has muted words
- ) && !this.muteWordHits.length > 0
+ ) && !this.muteFilterHits.length > 0
},
hideMutedUsers () {
return this.mergedConfig.hideMutedPosts
@@ -345,7 +346,8 @@ const Status = {
(this.muted && this.hideFilteredStatuses) ||
(this.userIsMuted && this.hideMutedUsers) ||
(this.status.thread_muted && this.hideMutedThreads) ||
- (this.muteWordHits.length > 0 && this.hideWordFilteredPosts)
+ (this.muteFilterHits.length > 0 && this.hideWordFilteredPosts) ||
+ (this.muteFilterHits.some(x => x.hide))
)
},
isFocused () {
diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js
index 6d4e4fbc0..6f84ce8b5 100644
--- a/src/components/status_action_buttons/status_action_buttons.js
+++ b/src/components/status_action_buttons/status_action_buttons.js
@@ -1,10 +1,12 @@
-import { mapState } from 'vuex'
+import { mapState } from 'pinia'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import ActionButtonContainer from './action_button_container.vue'
import Popover from 'src/components/popover/popover.vue'
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
+import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
+
import { BUTTONS } from './buttons_definitions.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -36,8 +38,8 @@ const StatusActionButtons = {
ActionButtonContainer
},
computed: {
- ...mapState({
- pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedStatusActions)
+ ...mapState(useServerSideStorageStore, {
+ pinnedItems: store => new Set(store.prefsStorage.collections.pinnedStatusActions)
}),
buttons () {
return BUTTONS.filter(x => x.if ? x.if(this.funcArg) : true)
@@ -101,12 +103,12 @@ const StatusActionButtons = {
return this.pinnedItems.has(button.name)
},
unpin (button) {
- this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
- this.$store.dispatch('pushServerSideStorage')
+ useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedStatusActions', value: button.name })
+ useServerSideStorageStore().pushServerSideStorage()
},
pin (button) {
- this.$store.commit('addCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
- this.$store.dispatch('pushServerSideStorage')
+ useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedStatusActions', value: button.name })
+ useServerSideStorageStore().pushServerSideStorage()
},
getComponent (button) {
if (!this.$store.state.users.currentUser && button.anonLink) {
diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js
index 308449698..1dbae0bba 100644
--- a/src/components/update_notification/update_notification.js
+++ b/src/components/update_notification/update_notification.js
@@ -3,6 +3,8 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import pleromaTanMask from 'src/assets/pleromatan_apology_mask.png'
import pleromaTanFoxMask from 'src/assets/pleromatan_apology_fox_mask.png'
+import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
+
import {
faTimes
} from '@fortawesome/free-solid-svg-icons'
@@ -36,8 +38,8 @@ const UpdateNotification = {
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.prefsStorage.simple.dontShowUpdateNotifs
+ useServerSideStorageStore().flagStorage.updateCounter < CURRENT_UPDATE_COUNTER &&
+ !useServerSideStorageStore().prefsStorage.simple.dontShowUpdateNotifs
}
},
methods: {
@@ -46,13 +48,13 @@ const UpdateNotification = {
},
neverShowAgain () {
this.toggleShow()
- this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
- this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true })
- this.$store.dispatch('pushServerSideStorage')
+ useServerSideStorageStore().setFlag({ flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
+ useServerSideStorageStore().setPreference({ path: 'simple.dontShowUpdateNotifs', value: true })
+ useServerSideStorageStore().pushServerSideStorage()
},
dismiss () {
- this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
- this.$store.dispatch('pushServerSideStorage')
+ useServerSideStorageStore().setFlag({ flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER })
+ useServerSideStorageStore().pushServerSideStorage()
}
},
mounted () {
diff --git a/src/i18n/en.json b/src/i18n/en.json
index acedee527..01de88223 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -410,6 +410,41 @@
"visual_tweaks": "Minor visual tweaks",
"theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
"scale_and_layout": "Interface scale and layout",
+ "enabled": "Enabled",
+ "filter": {
+ "clutter": "Remove clutter",
+ "mute_filter": "Mute Filters",
+ "type": "Filter type",
+ "regexp": "RegExp",
+ "plain": "Simple",
+ "user": "User (Simple)",
+ "user_regexp": "User (RegExp)",
+ "hide": "Hide completely",
+ "name": "Name",
+ "value": "Value",
+ "expires": "Expires",
+ "expired": "Expired",
+ "copy": "Duplicate",
+ "save": "Save",
+ "delete": "Remove",
+ "new": "Create new",
+ "import": "Import",
+ "export": "Export",
+ "regexp_error": "Invalid Regular Expression",
+ "never_expires": "Never",
+ "total_count": "Total {count} custom filter|Total {count} custom filters",
+ "expired_count": "{count} expired filter|{count} expired filters",
+ "custom_filters": "Custom filters",
+ "purge_expired": "Remove expired filters",
+ "import_failure": "The selected file is not a supported Pleroma filter.",
+ "help": {
+ "word": "Simple and RegExp filters test against post's content and subject.",
+ "user": "User filter matches full user handle (user@domain) in the following: author, reply-to and mentions",
+ "regexp": "Regex variants are more advanced and use {link} to match instead of simple substring search.",
+ "regexp_link": "Regular Expressions",
+ "regexp_url": "https://en.wikipedia.org/wiki/Regular_expression"
+ }
+ },
"mfa": {
"otp": "OTP",
"setup_otp": "Setup OTP",
@@ -575,6 +610,7 @@
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"hide_filtered_statuses": "Hide all filtered posts",
+ "hide_muted_statuses": "Completely hide all muted posts",
"hide_wordfiltered_statuses": "Hide word-filtered statuses",
"hide_muted_threads": "Hide muted threads",
"import_blocks_from_a_csv_file": "Import blocks from a csv file",
@@ -1265,6 +1301,7 @@
"copy_link": "Copy link to status",
"external_source": "External source",
"muted_words": "Wordfiltered: {word} | Wordfiltered: {word} and {numWordsMore} more words",
+ "muted_filters": "Filtered: {name} | Wordfiltered: {name} and {filtersMore} more words",
"multi_reason_mute": "{main} + one more reason | {main} + {numReasonsMore} more reasons",
"muted_user": "User muted",
"thread_muted": "Thread muted",
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index e6ed05f28..878d95aa8 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -18,7 +18,6 @@ const saveImmedeatelyActions = [
'markNotificationsAsSeen',
'clearCurrentUser',
'setCurrentUser',
- 'setServerSideStorage',
'setHighlight',
'setOption',
'setClientData',
diff --git a/src/modules/config_declaration.js b/src/modules/config_declaration.js
new file mode 100644
index 000000000..e3328d448
--- /dev/null
+++ b/src/modules/config_declaration.js
@@ -0,0 +1,42 @@
+export const CONFIG_MIGRATION = 1
+import { v4 as uuidv4 } from 'uuid';
+
+// for future use
+/*
+const simpleDeclaration = {
+ store: 'server-side',
+ migrationFlag: 'configMigration',
+ migration(serverside, rootState) {
+ serverside.setPreference({ path: 'simple.' + field, value: rootState.config[oldField ?? field] })
+ }
+}
+*/
+
+export const declarations = [
+ {
+ field: 'muteFilters',
+ store: 'server-side',
+ migrationFlag: 'configMigration',
+ migrationNum: 1,
+ description: 'Mute filters, wordfilter/regexp/etc',
+ valueType: 'complex',
+ migration (serverside, rootState) {
+ rootState.config.muteWords.forEach((word, order) => {
+ const uniqueId = uuidv4()
+
+ serverside.setPreference({
+ path: 'simple.muteFilters.' + uniqueId,
+ value: {
+ type: 'word',
+ value: word,
+ name: word,
+ enabled: true,
+ expires: null,
+ hide: false,
+ order
+ }
+ })
+ })
+ }
+ }
+]
diff --git a/src/modules/index.js b/src/modules/index.js
index e1c68aa67..5bcc1ca94 100644
--- a/src/modules/index.js
+++ b/src/modules/index.js
@@ -5,7 +5,6 @@ import users from './users.js'
import api from './api.js'
import config from './config.js'
import profileConfig from './profileConfig.js'
-import serverSideStorage from './serverSideStorage.js'
import adminSettings from './adminSettings.js'
import authFlow from './auth_flow.js'
import oauthTokens from './oauth_tokens.js'
@@ -20,7 +19,6 @@ export default {
api,
config,
profileConfig,
- serverSideStorage,
adminSettings,
authFlow,
oauthTokens,
diff --git a/src/modules/notifications.js b/src/modules/notifications.js
index 8556f76c5..c5c607922 100644
--- a/src/modules/notifications.js
+++ b/src/modules/notifications.js
@@ -1,4 +1,5 @@
import apiService from '../services/api/api.service.js'
+import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
import {
isStatusNotification,
@@ -112,7 +113,11 @@ export const notifications = {
commit('updateNotificationsMinMaxId', notification.id)
commit('addNewNotifications', { notifications: [notification] })
- maybeShowNotification(store, notification)
+ maybeShowNotification(
+ store,
+ Object.values(useServerSideStorageStore().prefsStorage.simple.muteFilters),
+ notification
+ )
} else if (notification.seen) {
state.idStore[notification.id].seen = true
}
diff --git a/src/modules/users.js b/src/modules/users.js
index f52de9597..01936c716 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,11 +1,16 @@
+import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
+
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import apiService from '../services/api/api.service.js'
import oauthApi from '../services/new_api/oauth.js'
-import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'
+
import { useInterfaceStore } from 'src/stores/interface.js'
import { useOAuthStore } from 'src/stores/oauth.js'
+import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
+
+import { declarations } from 'src/modules/config_declaration'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
@@ -605,7 +610,8 @@ const users = {
user.muteIds = []
user.domainMutes = []
commit('setCurrentUser', user)
- commit('setServerSideStorage', user)
+
+ useServerSideStorageStore().setServerSideStorage(user)
commit('addNewUsers', [user])
dispatch('fetchEmoji')
@@ -615,7 +621,35 @@ const users = {
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
- dispatch('pushServerSideStorage')
+
+ // Do server-side storage migrations
+
+ // Debug snippet to clean up storage and reset migrations
+ /*
+ // Reset wordfilter
+ Object.keys(
+ useServerSideStorageStore().prefsStorage.simple.muteFilters
+ ).forEach(key => {
+ useServerSideStorageStore().unsetPreference({ path: 'simple.muteFilters.' + key, value: null })
+ })
+
+ // Reset flag to 0 to re-run migrations
+ useServerSideStorageStore().setFlag({ flag: 'configMigration', value: 0 })
+ /**/
+
+ const { configMigration } = useServerSideStorageStore().flagStorage
+ declarations
+ .filter(x => {
+ return x.store === 'server-side' &&
+ x.migrationNum > 0 &&
+ x.migrationNum > configMigration
+ })
+ .toSorted((a, b) => a.configMigration - b.configMigration)
+ .forEach(value => {
+ value.migration(useServerSideStorageStore(), store.rootState)
+ useServerSideStorageStore().setFlag({ flag: 'configMigration', value: value.migrationNum })
+ useServerSideStorageStore().pushServerSideStorage()
+ })
if (user.token) {
dispatch('setWsToken', user.token)
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 4889aeb78..74fd91204 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,4 +1,4 @@
-import { muteWordHits } from '../status_parser/status_parser.js'
+import { muteFilterHits } from '../status_parser/status_parser.js'
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
import { useI18nStore } from 'src/stores/i18n.js'
import { useAnnouncementsStore } from 'src/stores/announcements'
@@ -58,10 +58,10 @@ const sortById = (a, b) => {
}
}
-const isMutedNotification = (store, notification) => {
- if (!notification.status) return
- const rootGetters = store.rootGetters || store.getters
- return notification.status.muted || muteWordHits(notification.status, rootGetters.mergedConfig.muteWords).length > 0
+const isMutedNotification = (notification) => {
+ if (!notification.status) return false
+ if (notification.status.muted) return true
+ return muteFilterHits(notification.status).length > 0
}
export const maybeShowNotification = (store, notification) => {
@@ -69,7 +69,7 @@ export const maybeShowNotification = (store, notification) => {
if (notification.seen) return
if (!visibleTypes(store).includes(notification.type)) return
- if (notification.type === 'mention' && isMutedNotification(store, notification)) return
+ if (notification.type === 'mention' && isMutedNotification(notification)) return
const notificationObject = prepareNotificationObject(notification, useI18nStore().i18n)
showDesktopNotification(rootState, notificationObject)
diff --git a/src/services/status_parser/status_parser.js b/src/services/status_parser/status_parser.js
index ed0f6d572..c9e15552d 100644
--- a/src/services/status_parser/status_parser.js
+++ b/src/services/status_parser/status_parser.js
@@ -1,11 +1,59 @@
-import { filter } from 'lodash'
-
-export const muteWordHits = (status, muteWords) => {
+export const muteFilterHits = (muteFilters, status) => {
const statusText = status.text.toLowerCase()
const statusSummary = status.summary.toLowerCase()
- const hits = filter(muteWords, (muteWord) => {
- return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
- })
+ const replyToUser = status.in_reply_to_screen_name?.toLowerCase()
+ const poster = status.user.screen_name.toLowerCase()
+ const mentions = (status.attentions || []).map(att => att.screen_name.toLowerCase())
- return hits
+
+ return muteFilters.toSorted((a,b) => b.order - a.order).map(filter => {
+ const { hide, expires, name, value, type, enabled} = filter
+ if (!enabled) return false
+ if (value === '') return false
+ if (expires !== null && expires < Date.now()) return false
+ switch (type) {
+ case 'word': {
+ if (statusText.includes(value) || statusSummary.includes(value)) {
+ return { hide, name }
+ }
+ break
+ }
+ case 'regexp': {
+ try {
+ const re = new RegExp(value, 'i')
+ if (re.test(statusText) || re.test(statusSummary)) {
+ return { hide, name }
+ }
+ return false
+ } catch {
+ return false
+ }
+ }
+ case 'user': {
+ if (
+ poster.includes(value) ||
+ replyToUser.includes(value) ||
+ mentions.some(mention => mention.includes(value))
+ ) {
+ return { hide, name }
+ }
+ break
+ }
+ case 'user_regexp': {
+ try {
+ const re = new RegExp(value, 'i')
+ if (
+ re.test(poster) ||
+ re.test(replyToUser) ||
+ mentions.some(mention => re.test(mention))
+ ) {
+ return { hide, name }
+ }
+ return false
+ } catch {
+ return false
+ }
+ }
+ }
+ }).filter(_ => _)
}
diff --git a/src/modules/serverSideStorage.js b/src/stores/serverSideStorage.js
similarity index 55%
rename from src/modules/serverSideStorage.js
rename to src/stores/serverSideStorage.js
index 9a8ea316a..9cfdac1a8 100644
--- a/src/modules/serverSideStorage.js
+++ b/src/stores/serverSideStorage.js
@@ -1,8 +1,10 @@
+import { defineStore } from 'pinia'
import { toRaw } from 'vue'
import {
isEqual,
cloneDeep,
set,
+ unset,
get,
clamp,
flatten,
@@ -26,6 +28,7 @@ export const defaultState = {
// storage of flags - stuff that can only be set and incremented
flagStorage: {
updateCounter: 0, // Counter for most recent update notification seen
+ configMigration: 0, // Counter for config -> server-side migrations
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
@@ -35,7 +38,8 @@ export const defaultState = {
_journal: [],
simple: {
dontShowUpdateNotifs: false,
- collapseNav: false
+ collapseNav: false,
+ muteFilters: {}
},
collections: {
pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
@@ -78,11 +82,16 @@ const _verifyPrefs = (state) => {
simple: {},
collections: {}
}
+
+ // Simple
Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => {
if (typeof v === 'number' || typeof v === 'boolean') return
+ if (typeof v === 'object' && v != null) return
console.warn(`Preference simple.${k} as invalid type, reinitializing`)
set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k])
})
+
+ // Collections
Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => {
if (Array.isArray(v)) return
console.warn(`Preference collections.${k} as invalid type, reinitializing`)
@@ -219,13 +228,27 @@ export const _mergePrefs = (recent, stale) => {
const totalJournal = _mergeJournal(staleJournal, recentJournal)
totalJournal.forEach(({ path, operation, args }) => {
if (path.startsWith('_')) {
- console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`)
- return
+ throw new Error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`)
}
switch (operation) {
case 'set':
+ if (path.startsWith('collections') || path.startsWith('objectCollections')) {
+ throw new Error('Illegal operation "set" on a collection')
+ }
+ if (path.split(/\./g).length <= 1) {
+ throw new Error(`Calling set on depth <= 1 (path: ${path}) is not allowed`)
+ }
set(resultOutput, path, args[0])
break
+ case 'unset':
+ if (path.startsWith('collections') || path.startsWith('objectCollections')) {
+ throw new Error('Illegal operation "unset" on a collection')
+ }
+ if (path.split(/\./g).length <= 2) {
+ throw new Error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`)
+ }
+ unset(resultOutput, path)
+ break
case 'addToCollection':
set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0])))
break
@@ -241,7 +264,7 @@ export const _mergePrefs = (recent, stale) => {
break
}
default:
- console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`)
+ throw new Error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`)
}
})
return { ...resultOutput, _journal: totalJournal }
@@ -302,164 +325,194 @@ export const _doMigrations = (cache) => {
return cache
}
-export const mutations = {
- clearServerSideStorage (state) {
- const blankState = { ...cloneDeep(defaultState) }
- Object.keys(state).forEach(k => {
- state[k] = blankState[k]
- })
+export const useServerSideStorageStore = defineStore('serverSideStorage', {
+ state() {
+ return cloneDeep(defaultState)
},
- setServerSideStorage (state, userData) {
- const live = userData.storage
- state.raw = live
- let cache = state.cache
- if (cache && cache._user !== userData.fqn) {
- console.warn('Cache belongs to another user! reinitializing local cache!')
- cache = null
- }
-
- cache = _doMigrations(cache)
-
- let { recent, stale, needUpload } = _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 },
- prefsStorage: { ...defaultState.prefsStorage }
- })
- }
-
- if (!needUpload && recent && stale) {
- console.debug('Checking if data needs merging...')
- // discarding timestamps and versions
- /* eslint-disable no-unused-vars */
- const { _timestamp: _0, _version: _1, ...recentData } = recent
- const { _timestamp: _2, _version: _3, ...staleData } = stale
- /* eslint-enable no-unused-vars */
- dirty = !isEqual(recentData, staleData)
- console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`)
- }
-
- const allFlagKeys = _getAllFlags(recent, stale)
- let totalFlags
- let totalPrefs
- if (dirty) {
- // Merge the flags
- console.debug('Merging the data...')
- totalFlags = _mergeFlags(recent, stale, allFlagKeys)
- _verifyPrefs(recent)
- _verifyPrefs(stale)
- totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage)
- } else {
- totalFlags = recent.flagStorage
- totalPrefs = recent.prefsStorage
- }
-
- totalFlags = _resetFlags(totalFlags)
-
- recent.flagStorage = { ...flagsTemplate, ...totalFlags }
- recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
-
- state.dirty = dirty || needUpload
- 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
- state.prefsStorage = state.cache.prefsStorage
- },
- setFlag (state, { flag, value }) {
- state.flagStorage[flag] = value
- state.dirty = true
- },
- setPreference (state, { path, value }) {
- if (path.startsWith('_')) {
- console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
- return
- }
- set(state.prefsStorage, path, value)
- state.prefsStorage._journal = [
- ...state.prefsStorage._journal,
- { operation: 'set', path, args: [value], timestamp: Date.now() }
- ]
- state.dirty = true
- },
- addCollectionPreference (state, { path, value }) {
- if (path.startsWith('_')) {
- console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
- return
- }
- const collection = new Set(get(state.prefsStorage, path))
- collection.add(value)
- set(state.prefsStorage, path, [...collection])
- state.prefsStorage._journal = [
- ...state.prefsStorage._journal,
- { operation: 'addToCollection', path, args: [value], timestamp: Date.now() }
- ]
- state.dirty = true
- },
- removeCollectionPreference (state, { path, value }) {
- if (path.startsWith('_')) {
- console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
- return
- }
- const collection = new Set(get(state.prefsStorage, path))
- collection.delete(value)
- set(state.prefsStorage, path, [...collection])
- state.prefsStorage._journal = [
- ...state.prefsStorage._journal,
- { operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() }
- ]
- state.dirty = true
- },
- reorderCollectionPreference (state, { path, value, movement }) {
- if (path.startsWith('_')) {
- console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
- return
- }
- const collection = get(state.prefsStorage, path)
- const newCollection = _moveItemInArray(collection, value, movement)
- set(state.prefsStorage, path, newCollection)
- state.prefsStorage._journal = [
- ...state.prefsStorage._journal,
- { operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() }
- ]
- state.dirty = true
- },
- updateCache (state, { username }) {
- state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal)
- state.cache = _wrapData({
- flagStorage: toRaw(state.flagStorage),
- prefsStorage: toRaw(state.prefsStorage)
- }, username)
- }
-}
-
-const serverSideStorage = {
- state: {
- ...cloneDeep(defaultState)
- },
- mutations,
actions: {
- pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
- const needPush = state.dirty || force
+ setFlag ({ flag, value }) {
+ this.flagStorage[flag] = value
+ this.dirty = true
+ },
+ setPreference ({ path, value }) {
+ if (path.startsWith('_')) {
+ throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`)
+ }
+ if (path.startsWith('collections') || path.startsWith('objectCollections')) {
+ throw new Error(`Invalid operation 'set' for collection field '${path}', ignoring.`)
+ }
+ if (path.split(/\./g).length <= 1) {
+ throw new Error(`Calling set on depth <= 1 (path: ${path}) is not allowed`)
+ }
+ if (path.split(/\./g).length > 3) {
+ throw new Error(`Calling set on depth > 3 (path: ${path}) is not allowed`)
+ }
+ set(this.prefsStorage, path, value)
+ this.prefsStorage._journal = [
+ ...this.prefsStorage._journal,
+ { operation: 'set', path, args: [value], timestamp: Date.now() }
+ ]
+ this.dirty = true
+ },
+ unsetPreference ({ path, value }) {
+ if (path.startsWith('_')) {
+ throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`)
+ }
+ if (path.startsWith('collections') || path.startsWith('objectCollections')) {
+ throw new Error(`Invalid operation 'unset' for collection field '${path}', ignoring.`)
+ }
+ if (path.split(/\./g).length <= 2) {
+ throw new Error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`)
+ }
+ if (path.split(/\./g).length > 3) {
+ throw new Error(`Calling unset on depth > 3 (path: ${path}) is not allowed`)
+ }
+ unset(this.prefsStorage, path, value)
+ this.prefsStorage._journal = [
+ ...this.prefsStorage._journal,
+ { operation: 'unset', path, args: [], timestamp: Date.now() }
+ ]
+ this.dirty = true
+ },
+ addCollectionPreference ({ path, value }) {
+ if (path.startsWith('_')) {
+ throw new Error(`tried to edit internal (starts with _) field '${path}'`)
+ }
+ if (path.startsWith('collections')) {
+ const collection = new Set(get(this.prefsStorage, path))
+ collection.add(value)
+ set(this.prefsStorage, path, [...collection])
+ } else if (path.startsWith('objectCollections')) {
+ const { _key } = value
+ if (!_key && typeof _key !== 'string') {
+ throw new Error('Object for storage is missing _key field!')
+ }
+ const collection = new Set(get(this.prefsStorage, path + '.index'))
+ collection.add(_key)
+ set(this.prefsStorage, path + '.index', [...collection])
+ set(this.prefsStorage, path + '.data.' + _key, value)
+ }
+ this.prefsStorage._journal = [
+ ...this.prefsStorage._journal,
+ { operation: 'addToCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ this.dirty = true
+ },
+ removeCollectionPreference ({ path, value }) {
+ if (path.startsWith('_')) {
+ throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ }
+ const collection = new Set(get(this.prefsStorage, path))
+ collection.delete(value)
+ set(this.prefsStorage, path, [...collection])
+ this.prefsStorage._journal = [
+ ...this.prefsStorage._journal,
+ { operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ this.dirty = true
+ },
+ reorderCollectionPreference ({ path, value, movement }) {
+ if (path.startsWith('_')) {
+ throw new Error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ }
+ const collection = get(this.prefsStorage, path)
+ const newCollection = _moveItemInArray(collection, value, movement)
+ set(this.prefsStorage, path, newCollection)
+ this.prefsStorage._journal = [
+ ...this.prefsStorage._journal,
+ { operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ this.dirty = true
+ },
+ updateCache ({ username }) {
+ this.prefsStorage._journal = _mergeJournal(this.prefsStorage._journal)
+ this.cache = _wrapData({
+ flagStorage: toRaw(this.flagStorage),
+ prefsStorage: toRaw(this.prefsStorage)
+ }, username)
+ },
+ clearServerSideStorage () {
+ const blankState = { ...cloneDeep(defaultState) }
+ Object.keys(this).forEach(k => {
+ this[k] = blankState[k]
+ })
+ },
+ setServerSideStorage (userData) {
+ const live = userData.storage
+ this.raw = live
+ let cache = this.cache
+ if (cache && cache._user !== userData.fqn) {
+ console.warn('Cache belongs to another user! reinitializing local cache!')
+ cache = null
+ }
+
+ cache = _doMigrations(cache)
+
+ let { recent, stale, needUpload } = _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 },
+ prefsStorage: { ...defaultState.prefsStorage }
+ })
+ }
+
+ if (!needUpload && recent && stale) {
+ console.debug('Checking if data needs merging...')
+ // discarding timestamps and versions
+ /* eslint-disable no-unused-vars */
+ const { _timestamp: _0, _version: _1, ...recentData } = recent
+ const { _timestamp: _2, _version: _3, ...staleData } = stale
+ /* eslint-enable no-unused-vars */
+ dirty = !isEqual(recentData, staleData)
+ console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`)
+ }
+
+ const allFlagKeys = _getAllFlags(recent, stale)
+ let totalFlags
+ let totalPrefs
+ if (dirty) {
+ // Merge the flags
+ console.debug('Merging the data...')
+ totalFlags = _mergeFlags(recent, stale, allFlagKeys)
+ _verifyPrefs(recent)
+ _verifyPrefs(stale)
+ totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage)
+ } else {
+ totalFlags = recent.flagStorage
+ totalPrefs = recent.prefsStorage
+ }
+
+ totalFlags = _resetFlags(totalFlags)
+
+ recent.flagStorage = { ...flagsTemplate, ...totalFlags }
+ recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
+
+ this.dirty = dirty || needUpload
+ this.cache = recent
+ // set local timestamp to smaller one if we don't have any changes
+ if (stale && recent && !this.dirty) {
+ this.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
+ }
+ this.flagStorage = this.cache.flagStorage
+ this.prefsStorage = this.cache.prefsStorage
+ },
+ pushServerSideStorage ({ force = false } = {}) {
+ const needPush = this.dirty || force
if (!needPush) return
- commit('updateCache', { username: rootState.users.currentUser.fqn })
- const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }
- rootState.api.backendInteractor
+ this.updateCache({ username: window.vuex.state.users.currentUser.fqn })
+ const params = { pleroma_settings_store: { 'pleroma-fe': this.cache } }
+ window.vuex.state.api.backendInteractor
.updateProfile({ params })
.then((user) => {
- commit('setServerSideStorage', user)
- state.dirty = false
+ this.setServerSideStorage(user)
+ this.dirty = false
})
}
}
-}
-
-export default serverSideStorage
+})
diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js
index 1d4021a7a..607bf9263 100644
--- a/test/unit/specs/modules/serverSideStorage.spec.js
+++ b/test/unit/specs/modules/serverSideStorage.spec.js
@@ -1,4 +1,5 @@
import { cloneDeep } from 'lodash'
+import { setActivePinia, createPinia } from 'pinia'
import {
VERSION,
@@ -10,49 +11,50 @@ import {
_mergeFlags,
_mergePrefs,
_resetFlags,
- mutations,
defaultState,
- newUserFlags
-} from 'src/modules/serverSideStorage.js'
+ newUserFlags,
+ useServerSideStorageStore,
+} from 'src/stores/serverSideStorage.js'
describe('The serverSideStorage module', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ })
+
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)
- expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage)
+ const store = useServerSideStorageStore()
+ store.setServerSideStorage(store, user)
+ expect(store.cache._version).to.eql(VERSION)
+ expect(store.cache._timestamp).to.be.a('number')
+ expect(store.cache.flagStorage).to.eql(defaultState.flagStorage)
+ expect(store.cache.prefsStorage).to.eql(defaultState.prefsStorage)
})
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)
- expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage)
+ const store = useServerSideStorageStore()
+ store.setServerSideStorage({ ...user, created_at: new Date() })
+ expect(store.cache._version).to.eql(VERSION)
+ expect(store.cache._timestamp).to.be.a('number')
+ expect(store.cache.flagStorage).to.eql(newUserFlags)
+ expect(store.cache.prefsStorage).to.eql(defaultState.prefsStorage)
})
it('should merge flags even if remote timestamp is older', () => {
- const state = {
- ...cloneDeep(defaultState),
- cache: {
- _timestamp: Date.now(),
- _version: VERSION,
- ...cloneDeep(defaultState)
- }
+ const store = useServerSideStorageStore()
+ store.cache = {
+ _timestamp: Date.now(),
+ _version: VERSION,
+ ...cloneDeep(defaultState)
}
- setServerSideStorage(
- state,
+
+ store.setServerSideStorage(
{
...user,
storage: {
@@ -68,19 +70,18 @@ describe('The serverSideStorage module', () => {
}
}
)
- expect(state.cache.flagStorage).to.eql({
+
+ expect(store.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,
+ const store = useServerSideStorageStore()
+ store.cache = null
+
+ store.setServerSideStorage(
{
...user,
storage: {
@@ -93,72 +94,95 @@ describe('The serverSideStorage module', () => {
}
}
)
- expect(state.cache._timestamp).to.eql(123)
- expect(state.flagStorage.updateCounter).to.eql(999)
- expect(state.cache.flagStorage.updateCounter).to.eql(999)
+ expect(store.cache._timestamp).to.eql(123)
+ expect(store.flagStorage.updateCounter).to.eql(999)
+ expect(store.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)
+ const store = useServerSideStorageStore()
+ store.setServerSideStorage(store, user)
+ expect(store.cache._version).to.eql(VERSION)
+ expect(store.cache._timestamp).to.be.a('number')
+ expect(store.cache.flagStorage).to.eql(defaultState.flagStorage)
})
})
describe('setPreference', () => {
- const { setPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations
-
it('should set preference and update journal log accordingly', () => {
- const state = cloneDeep(defaultState)
- setPreference(state, { path: 'simple.testing', value: 1 })
- expect(state.prefsStorage.simple.testing).to.eql(1)
- expect(state.prefsStorage._journal.length).to.eql(1)
- expect(state.prefsStorage._journal[0]).to.eql({
+ const store = useServerSideStorageStore()
+ store.setPreference({ path: 'simple.testing', value: 1 })
+ expect(store.prefsStorage.simple.testing).to.eql(1)
+ expect(store.prefsStorage._journal.length).to.eql(1)
+ expect(store.prefsStorage._journal[0]).to.eql({
path: 'simple.testing',
operation: 'set',
args: [1],
// should have A timestamp, we don't really care what it is
- timestamp: state.prefsStorage._journal[0].timestamp
+ timestamp: store.prefsStorage._journal[0].timestamp
})
})
it('should keep journal to a minimum', () => {
- const state = cloneDeep(defaultState)
- setPreference(state, { path: 'simple.testing', value: 1 })
- setPreference(state, { path: 'simple.testing', value: 2 })
- addCollectionPreference(state, { path: 'collections.testing', value: 2 })
- removeCollectionPreference(state, { path: 'collections.testing', value: 2 })
- updateCache(state, { username: 'test' })
- expect(state.prefsStorage.simple.testing).to.eql(2)
- expect(state.prefsStorage.collections.testing).to.eql([])
- expect(state.prefsStorage._journal.length).to.eql(2)
- expect(state.prefsStorage._journal[0]).to.eql({
+ const store = useServerSideStorageStore()
+ store.setPreference({ path: 'simple.testing', value: 1 })
+ store.setPreference({ path: 'simple.testing', value: 2 })
+ store.addCollectionPreference({ path: 'collections.testing', value: 2 })
+ store.removeCollectionPreference({ path: 'collections.testing', value: 2 })
+ store.updateCache({ username: 'test' })
+ expect(store.prefsStorage.simple.testing).to.eql(2)
+ expect(store.prefsStorage.collections.testing).to.eql([])
+ expect(store.prefsStorage._journal.length).to.eql(2)
+ expect(store.prefsStorage._journal[0]).to.eql({
path: 'simple.testing',
operation: 'set',
args: [2],
// should have A timestamp, we don't really care what it is
- timestamp: state.prefsStorage._journal[0].timestamp
+ timestamp: store.prefsStorage._journal[0].timestamp
})
- expect(state.prefsStorage._journal[1]).to.eql({
+ expect(store.prefsStorage._journal[1]).to.eql({
path: 'collections.testing',
operation: 'removeFromCollection',
args: [2],
// should have A timestamp, we don't really care what it is
- timestamp: state.prefsStorage._journal[1].timestamp
+ timestamp: store.prefsStorage._journal[1].timestamp
})
})
it('should remove duplicate entries from journal', () => {
- const state = cloneDeep(defaultState)
- setPreference(state, { path: 'simple.testing', value: 1 })
- setPreference(state, { path: 'simple.testing', value: 1 })
- addCollectionPreference(state, { path: 'collections.testing', value: 2 })
- addCollectionPreference(state, { path: 'collections.testing', value: 2 })
- updateCache(state, { username: 'test' })
- expect(state.prefsStorage.simple.testing).to.eql(1)
- expect(state.prefsStorage.collections.testing).to.eql([2])
- expect(state.prefsStorage._journal.length).to.eql(2)
+ const store = useServerSideStorageStore()
+ store.setPreference({ path: 'simple.testing', value: 1 })
+ store.setPreference({ path: 'simple.testing', value: 1 })
+ store.addCollectionPreference({ path: 'collections.testing', value: 2 })
+ store.addCollectionPreference({ path: 'collections.testing', value: 2 })
+ store.updateCache({ username: 'test' })
+ expect(store.prefsStorage.simple.testing).to.eql(1)
+ expect(store.prefsStorage.collections.testing).to.eql([2])
+ expect(store.prefsStorage._journal.length).to.eql(2)
+ })
+
+ it('should remove depth = 3 set/unset entries from journal', () => {
+ const store = useServerSideStorageStore()
+ store.setPreference({ path: 'simple.object.foo', value: 1 })
+ store.unsetPreference({ path: 'simple.object.foo' })
+ store.updateCache(store, { username: 'test' })
+ expect(store.prefsStorage.simple.object).to.not.have.property('foo')
+ expect(store.prefsStorage._journal.length).to.eql(1)
+ })
+
+ it('should not allow unsetting depth <= 2', () => {
+ const store = useServerSideStorageStore()
+ store.setPreference({ path: 'simple.object.foo', value: 1 })
+ expect(() => store.unsetPreference({ path: 'simple' })).to.throw()
+ expect(() => store.unsetPreference({ path: 'simple.object' })).to.throw()
+ })
+
+ it('should not allow (un)setting depth > 3', () => {
+ const store = useServerSideStorageStore()
+ store.setPreference({ path: 'simple.object', value: {} })
+ expect(() => store.setPreference({ path: 'simple.object.lv3', value: 1 })).to.not.throw()
+ expect(() => store.setPreference({ path: 'simple.object.lv3.lv4', value: 1})).to.throw()
+ expect(() => store.unsetPreference({ path: 'simple.object.lv3', value: 1 })).to.not.throw()
+ expect(() => store.unsetPreference({ path: 'simple.object.lv3.lv4', value: 1})).to.throw()
})
})
})
@@ -315,6 +339,58 @@ describe('The serverSideStorage module', () => {
]
})
})
+
+ it('should work with objects', () => {
+ expect(
+ _mergePrefs(
+ // RECENT
+ {
+ simple: { lv2: { lv3: 'foo' } },
+ _journal: [
+ { path: 'simple.lv2.lv3', operation: 'set', args: ['foo'], timestamp: 2 }
+ ]
+ },
+ // STALE
+ {
+ simple: { lv2: { lv3: 'bar' } },
+ _journal: [
+ { path: 'simple.lv2.lv3', operation: 'set', args: ['bar'], timestamp: 4 }
+ ]
+ }
+ )
+ ).to.eql({
+ simple: { lv2: { lv3: 'bar' } },
+ _journal: [
+ { path: 'simple.lv2.lv3', operation: 'set', args: ['bar'], timestamp: 4 }
+ ]
+ })
+ })
+
+ it('should work with unset', () => {
+ expect(
+ _mergePrefs(
+ // RECENT
+ {
+ simple: { lv2: { lv3: 'foo' } },
+ _journal: [
+ { path: 'simple.lv2.lv3', operation: 'set', args: ['foo'], timestamp: 2 }
+ ]
+ },
+ // STALE
+ {
+ simple: { lv2: {} },
+ _journal: [
+ { path: 'simple.lv2.lv3', operation: 'unset', args: [], timestamp: 4 }
+ ]
+ }
+ )
+ ).to.eql({
+ simple: { lv2: {} },
+ _journal: [
+ { path: 'simple.lv2.lv3', operation: 'unset', args: [], timestamp: 4 }
+ ]
+ })
+ })
})
describe('_resetFlags', () => {