Merge branch 'sss-objects' into shigusegubu-themes3
This commit is contained in:
commit
add0e5f934
23 changed files with 1305 additions and 928 deletions
0
changelog.d/remove-opn-dependency.skip
Normal file
0
changelog.d/remove-opn-dependency.skip
Normal file
18
package.json
18
package.json
|
@ -17,7 +17,7 @@
|
|||
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.9",
|
||||
"@babel/runtime": "7.26.10",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
|
@ -39,13 +39,14 @@
|
|||
"js-cookie": "3.0.5",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.7.19",
|
||||
"phoenix": "1.7.20",
|
||||
"pinia": "^2.0.33",
|
||||
"punycode.js": "2.3.1",
|
||||
"qrcode": "1.5.4",
|
||||
"querystring-es3": "0.2.1",
|
||||
"url": "0.11.4",
|
||||
"utf8": "3.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"vue": "3.5.13",
|
||||
"vue-i18n": "10",
|
||||
"vue-router": "4.5.0",
|
||||
|
@ -53,9 +54,9 @@
|
|||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/eslint-parser": "7.26.8",
|
||||
"@babel/plugin-transform-runtime": "7.26.9",
|
||||
"@babel/core": "7.26.10",
|
||||
"@babel/eslint-parser": "7.26.10",
|
||||
"@babel/plugin-transform-runtime": "7.26.10",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/register": "7.25.9",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
|
@ -71,7 +72,7 @@
|
|||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "4.5.0",
|
||||
"chalk": "5.4.1",
|
||||
"chromedriver": "133.0.3",
|
||||
"chromedriver": "134.0.3",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
|
@ -89,8 +90,7 @@
|
|||
"iso-639-1": "3.1.5",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.7.3",
|
||||
"nightwatch": "3.11.1",
|
||||
"opn": "5.5.0",
|
||||
"nightwatch": "3.12.0",
|
||||
"ora": "0.4.1",
|
||||
"playwright": "1.49.1",
|
||||
"postcss": "8.5.3",
|
||||
|
@ -100,7 +100,7 @@
|
|||
"selenium-server": "3.141.59",
|
||||
"semver": "7.7.1",
|
||||
"serve-static": "1.16.2",
|
||||
"shelljs": "0.8.5",
|
||||
"shelljs": "0.9.1",
|
||||
"sinon": "15.2.0",
|
||||
"sinon-chai": "3.7.0",
|
||||
"stylelint": "14.16.1",
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
|
||||
import {
|
||||
unseenNotificationsFromStore,
|
||||
countExtraNotifications
|
||||
} from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
|
@ -18,7 +23,6 @@ import {
|
|||
faMinus,
|
||||
faCheckDouble
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
|
@ -71,10 +75,9 @@ const MobileNav = {
|
|||
return this.$route.name === 'chat'
|
||||
},
|
||||
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']),
|
||||
...mapGetters(['unreadChatCount']),
|
||||
chatsPinned () {
|
||||
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
},
|
||||
...mapState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
}),
|
||||
shouldConfirmLogout () {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
|
|
|
@ -7,7 +7,9 @@ import { filterNavigation } from 'src/components/navigation/filter.js'
|
|||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -76,19 +78,19 @@ const NavPanel = {
|
|||
this.editMode = !this.editMode
|
||||
},
|
||||
toggleCollapse () {
|
||||
this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed })
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().setPreference({ path: 'simple.collapseNav', value: !this.collapsed })
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
},
|
||||
isPinned (item) {
|
||||
return this.pinnedItems.has(item)
|
||||
},
|
||||
togglePin (item) {
|
||||
if (this.isPinned(item)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item })
|
||||
useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -96,14 +98,16 @@ const NavPanel = {
|
|||
unreadAnnouncementCount: 'unreadAnnouncementCount',
|
||||
supportsAnnouncements: store => store.supportsAnnouncements
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
collapsed: store => store.prefsStorage.simple.collapseNav,
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
followRequestCount: state => state.api.followRequests.length,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
|
||||
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav,
|
||||
bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable
|
||||
}),
|
||||
timelinesItems () {
|
||||
|
|
|
@ -3,8 +3,10 @@ import { routeTo } from 'src/components/navigation/navigation.js'
|
|||
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
||||
import { mapStores } from 'pinia'
|
||||
import { mapStores, mapState as mapPiniaState } from 'pinia'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
library.add(faThumbtack)
|
||||
|
||||
|
@ -19,11 +21,11 @@ const NavigationEntry = {
|
|||
},
|
||||
togglePin (value) {
|
||||
if (this.isPinned(value)) {
|
||||
this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value })
|
||||
} else {
|
||||
this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value })
|
||||
useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value })
|
||||
}
|
||||
this.$store.dispatch('pushServerSideStorage')
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -35,9 +37,11 @@ const NavigationEntry = {
|
|||
},
|
||||
...mapStores(useAnnouncementsStore),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
})
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { useListsStore } from 'src/stores/lists'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
library.add(
|
||||
faUsers,
|
||||
|
@ -54,15 +55,17 @@ const NavPanel = {
|
|||
supportsAnnouncements: store => store.supportsAnnouncements
|
||||
}),
|
||||
...mapPiniaState(useBookmarkFoldersStore, {
|
||||
bookmarks: getBookmarkFolderEntries
|
||||
bookmarks: getBookmarkFolderEntries,
|
||||
}),
|
||||
...mapPiniaState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
followRequestCount: state => state.api.followRequests.length,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
}),
|
||||
pinnedList () {
|
||||
if (!this.currentUser) {
|
||||
|
|
|
@ -363,12 +363,6 @@ const PostStatusForm = {
|
|||
}
|
||||
},
|
||||
safeToSaveDraft () {
|
||||
console.log('safe', (
|
||||
this.newStatus.status ||
|
||||
this.newStatus.spoilerText ||
|
||||
this.newStatus.files?.length ||
|
||||
this.newStatus.hasPoll
|
||||
) && this.saveable)
|
||||
return (
|
||||
this.newStatus.status ||
|
||||
this.newStatus.spoilerText ||
|
||||
|
|
|
@ -78,7 +78,6 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
draft: {
|
||||
// TODO allow passing shared draft object?
|
||||
get () {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import { filter, trim, debounce } from 'lodash'
|
||||
import { throttle } from 'lodash'
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import UnitSetting from '../helpers/unit_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
const FilteringTab = {
|
||||
data () {
|
||||
return {
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'),
|
||||
replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
|
@ -21,26 +26,97 @@ const FilteringTab = {
|
|||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
UnitSetting,
|
||||
IntegerSetting
|
||||
IntegerSetting,
|
||||
Checkbox,
|
||||
Select
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
muteWordsString: {
|
||||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.debouncedSetMuteWords(value)
|
||||
...mapState(
|
||||
useServerSideStorageStore,
|
||||
{
|
||||
muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters),
|
||||
muteFiltersObject: store => store.prefsStorage.simple.muteFilters
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useServerSideStorageStore, ['unsetPreference']),
|
||||
pushServerSideStorage: throttle(() => useServerSideStorageStore().pushServerSideStorage(), 500),
|
||||
setPreference: throttle(x => useServerSideStorageStore().setPreference(x), 500),
|
||||
getDatetimeLocal (timestamp) {
|
||||
const date = new Date(timestamp)
|
||||
const datetime = [
|
||||
date.getFullYear(),
|
||||
'-',
|
||||
date.getMonth() < 9 ? ('0' + (date.getMonth() + 1)) : (date.getMonth() + 1),
|
||||
'-',
|
||||
date.getDate() < 10 ? ('0' + date.getDate()) : date.getDate(),
|
||||
'T',
|
||||
date.getHours() < 10 ? ('0' + date.getHours()) : date.getHours(),
|
||||
':',
|
||||
date.getMinutes() < 10 ? ('0' + date.getMinutes()) : date.getMinutes(),
|
||||
].join('')
|
||||
return datetime
|
||||
},
|
||||
debouncedSetMuteWords () {
|
||||
return debounce((value) => {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}, 1000)
|
||||
checkRegexValid (id) {
|
||||
const filter = this.muteFiltersObject[id]
|
||||
if (filter.type !== 'regexp') return true
|
||||
const { value } = filter
|
||||
let valid = true
|
||||
try {
|
||||
new RegExp(value)
|
||||
} catch (e) {
|
||||
valid = false
|
||||
console.error('Invalid RegExp: ' + value)
|
||||
}
|
||||
return valid
|
||||
},
|
||||
createFilter () {
|
||||
const filter = {
|
||||
type: 'word',
|
||||
value: '',
|
||||
name: 'New Filter',
|
||||
enabled: true,
|
||||
expires: null,
|
||||
hide: false,
|
||||
order: this.muteFilters.length + 2
|
||||
}
|
||||
const newId = uuidv4()
|
||||
|
||||
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
copyFilter (id) {
|
||||
const filter = { ...this.muteFiltersObject[id] }
|
||||
const newId = uuidv4()
|
||||
|
||||
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
deleteFilter (id) {
|
||||
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
updateFilter(id, field, value) {
|
||||
const filter = { ...this.muteFiltersObject[id] }
|
||||
if (field === 'expires-never') {
|
||||
// filter[field] = value
|
||||
if (!value) {
|
||||
const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks
|
||||
const date = Date.now() + offset
|
||||
filter.expires = date
|
||||
} else {
|
||||
filter.expires = null
|
||||
}
|
||||
} else if (field === 'expires') {
|
||||
const parsed = Date.parse(value)
|
||||
filter.expires = parsed.valueOf()
|
||||
} else {
|
||||
filter[field] = value
|
||||
}
|
||||
this.setPreference({ path: 'simple.muteFilters.' + id , value: filter })
|
||||
this.pushServerSideStorage()
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
|
|
61
src/components/settings_modal/tabs/filtering_tab.scss
Normal file
61
src/components/settings_modal/tabs/filtering_tab.scss
Normal file
|
@ -0,0 +1,61 @@
|
|||
.filtering-tab {
|
||||
.muteFilterContainer {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
|
||||
height: 33vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mute-filter {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--roundness);
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: fit-content() 1fr fit-content();
|
||||
align-items: baseline;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.alert, .button-default {
|
||||
display: inline-block;
|
||||
line-height: 2;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.filter-enabled {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-template-rows: subgrid;
|
||||
grid-column: 1 / span 3;
|
||||
align-items: baseline;
|
||||
|
||||
label {
|
||||
grid-column: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filter-field-value {
|
||||
grid-column: 2 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
grid-column: 1 / span 3;
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :label="$t('settings.filtering')">
|
||||
<div :label="$t('settings.filtering')" class="filtering-tab">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.posts') }}</h2>
|
||||
<ul class="setting-list">
|
||||
|
@ -68,13 +68,148 @@
|
|||
{{ $t('settings.replies_in_timeline') }}
|
||||
</ChoiceSetting>
|
||||
<li>
|
||||
<h3>{{ $t('settings.wordfilter') }}</h3>
|
||||
<textarea
|
||||
id="muteWords"
|
||||
v-model="muteWordsString"
|
||||
class="input resize-height"
|
||||
/>
|
||||
<div>{{ $t('settings.filtering_explanation') }}</div>
|
||||
<h3>{{ $t('settings.filter.mute_filter') }}</h3>
|
||||
<div class="muteFilterContainer">
|
||||
<div
|
||||
class="mute-filter"
|
||||
:style="{ order: filter[1].order }"
|
||||
v-for="filter in muteFilters"
|
||||
key="filter[0]"
|
||||
>
|
||||
<div class="filter-name">
|
||||
<label
|
||||
:for="'filterName' + filter[0]"
|
||||
>
|
||||
{{ $t('settings.filter.name') }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="'filterName' + filter[0]"
|
||||
class="input"
|
||||
:value="filter[1].name"
|
||||
@input="updateFilter(filter[0], 'name', $event.target.value)"
|
||||
>
|
||||
</div>
|
||||
<div class="filter-enabled">
|
||||
<Checkbox
|
||||
:id="'filterHide' + filter[0]"
|
||||
:model-value="filter[1].hide"
|
||||
:name="'filterHide' + filter[0]"
|
||||
@update:model-value="updateFilter(filter[0], 'hide', $event)"
|
||||
>
|
||||
{{ $t('settings.filter.hide') }}
|
||||
</Checkbox>
|
||||
{{ ' ' }}
|
||||
<Checkbox
|
||||
:id="'filterEnabled' + filter[0]"
|
||||
:model-value="filter[1].enabled"
|
||||
:name="'filterEnabled' + filter[0]"
|
||||
@update:model-value="updateFilter(filter[0], 'enabled', $event)"
|
||||
>
|
||||
{{ $t('settings.enabled') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="filter-type filter-field">
|
||||
<label :for="'filterType' + filter[0]">
|
||||
{{ $t('settings.filter.type') }}
|
||||
</label>
|
||||
<Select
|
||||
:id="'filterType' + filter[0]"
|
||||
class="filter-field-value"
|
||||
:modelValue="filter[1].type"
|
||||
@update:model-value="updateFilter(filter[0], 'type', $event)"
|
||||
>
|
||||
<option value="word">
|
||||
{{ $t('settings.filter.plain') }}
|
||||
</option>
|
||||
<option value="regexp">
|
||||
{{ $t('settings.filter.regexp') }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="filter-value filter-field">
|
||||
<label
|
||||
:for="'filterValue' + filter[0]"
|
||||
>
|
||||
{{ $t('settings.filter.value') }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<input
|
||||
:id="'filterValue' + filter[0]"
|
||||
class="input filter-field-value"
|
||||
:value="filter[1].value"
|
||||
@input="updateFilter(filter[0], 'value', $event.target.value)"
|
||||
>
|
||||
</div>
|
||||
<div class="filter-expires filter-field">
|
||||
<label
|
||||
:for="'filterExpires' + filter[0]"
|
||||
>
|
||||
{{ $t('settings.filter.expires') }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<div class="filter-field-value">
|
||||
<input
|
||||
:id="'filterExpires' + filter[0]"
|
||||
class="input"
|
||||
:class="{ disabled: filter[1].expires === null }"
|
||||
type="datetime-local"
|
||||
:disabled="filter[1].expires === null"
|
||||
:value="filter[1].expires ? getDatetimeLocal(filter[1].expires) : null"
|
||||
@input="updateFilter(filter[0], 'expires', $event.target.value)"
|
||||
>
|
||||
{{ ' ' }}
|
||||
<Checkbox
|
||||
:id="'filterExpiresNever' + filter[0]"
|
||||
:model-value="filter[1].expires === null"
|
||||
:name="'filterExpiresNever' + filter[0]"
|
||||
class="input-inset input-boolean"
|
||||
@update:model-value="updateFilter(filter[0], 'expires-never', $event)"
|
||||
>
|
||||
{{ $t('settings.filter.never_expires') }}
|
||||
</Checkbox>
|
||||
<span
|
||||
v-if="filter[1].expires !== null && Date.now() > filter[1].expires"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ $t('settings.filter.expired') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<span
|
||||
v-if="!checkRegexValid(filter[0])"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('settings.filter.regexp_error') }}
|
||||
</span>
|
||||
<button
|
||||
class="copy-button button-default"
|
||||
type="button"
|
||||
@click="copyFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.copy') }}
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
class="delete-button button-default -danger"
|
||||
type="button"
|
||||
@click="deleteFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mute-filter">
|
||||
<button
|
||||
class="add-button button-default"
|
||||
type="button"
|
||||
@click="createFilter()"
|
||||
>
|
||||
{{ $t('settings.filter.new') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
|
@ -130,3 +265,4 @@
|
|||
</div>
|
||||
</template>
|
||||
<script src="./filtering_tab.js"></script>
|
||||
<style src="./filtering_tab.scss"></style>
|
||||
|
|
|
@ -14,7 +14,7 @@ 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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
@ -161,9 +161,6 @@ const Status = {
|
|||
},
|
||||
computed: {
|
||||
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
|
||||
muteWords () {
|
||||
return this.mergedConfig.muteWords
|
||||
},
|
||||
showReasonMutedThread () {
|
||||
return (
|
||||
this.status.thread_muted ||
|
||||
|
@ -221,8 +218,8 @@ const Status = {
|
|||
loggedIn () {
|
||||
return !!this.currentUser
|
||||
},
|
||||
muteWordHits () {
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
muteFilterHits () {
|
||||
return muteFilterHits(this.status)
|
||||
},
|
||||
botStatus () {
|
||||
return this.status.user.actor_type === 'Service'
|
||||
|
@ -256,7 +253,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 +264,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 +323,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 +342,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 () {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -409,6 +409,23 @@
|
|||
"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": {
|
||||
"mute_filter": "Mute Filters",
|
||||
"type": "Filter type",
|
||||
"regexp": "RegExp",
|
||||
"plain": "Simple",
|
||||
"hide": "Hide completely",
|
||||
"name": "Name",
|
||||
"value": "Value",
|
||||
"expires": "Expires",
|
||||
"expired": "Expired",
|
||||
"copy": "Copy filter",
|
||||
"delete": "Remove filter",
|
||||
"new": "Create filter",
|
||||
"regexp_error": "Invalid Regular Expression",
|
||||
"never_expires": "Never"
|
||||
},
|
||||
"mfa": {
|
||||
"otp": "OTP",
|
||||
"setup_otp": "Setup OTP",
|
||||
|
@ -1264,6 +1281,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",
|
||||
|
|
|
@ -18,7 +18,6 @@ const saveImmedeatelyActions = [
|
|||
'markNotificationsAsSeen',
|
||||
'clearCurrentUser',
|
||||
'setCurrentUser',
|
||||
'setServerSideStorage',
|
||||
'setHighlight',
|
||||
'setOption',
|
||||
'setClientData',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
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, CONFIG_MIGRATION } from 'src/stores/serverSideStorage'
|
||||
|
||||
// TODO: Unify with mergeOrAdd in statuses.js
|
||||
export const mergeOrAdd = (arr, obj, item) => {
|
||||
|
@ -605,7 +609,8 @@ const users = {
|
|||
user.muteIds = []
|
||||
user.domainMutes = []
|
||||
commit('setCurrentUser', user)
|
||||
commit('setServerSideStorage', user)
|
||||
|
||||
useServerSideStorageStore().setServerSideStorage(user)
|
||||
commit('addNewUsers', [user])
|
||||
|
||||
dispatch('fetchEmoji')
|
||||
|
@ -615,7 +620,50 @@ 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
|
||||
|
||||
// Wordfilter migration
|
||||
if (configMigration < 1) {
|
||||
// Convert existing wordfilter into synced one
|
||||
store.rootState.config.muteWords.forEach((word, order) => {
|
||||
const uniqueId = uuidv4()
|
||||
|
||||
useServerSideStorageStore().setPreference({
|
||||
path: 'simple.muteFilters.' + uniqueId,
|
||||
value: {
|
||||
type: 'word',
|
||||
value: word,
|
||||
name: word,
|
||||
enabled: true,
|
||||
expires: null,
|
||||
hide: false,
|
||||
order
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (configMigration < CONFIG_MIGRATION) {
|
||||
// Update the flag
|
||||
useServerSideStorageStore().setFlag({ flag: 'configMigration', value: CONFIG_MIGRATION })
|
||||
useServerSideStorageStore().pushServerSideStorage()
|
||||
}
|
||||
|
||||
if (user.token) {
|
||||
dispatch('setWsToken', user.token)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,11 +1,31 @@
|
|||
import { filter } from 'lodash'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
|
||||
export const muteWordHits = (status, muteWords) => {
|
||||
export const muteFilterHits = (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())
|
||||
})
|
||||
|
||||
return hits
|
||||
const muteFilters = Object.values(useServerSideStorageStore().prefsStorage.simple.muteFilters)
|
||||
|
||||
return muteFilters.toSorted((a,b) => b.order - a.order).map(filter => {
|
||||
const { hide, expires, name, value, type, enabled} = filter
|
||||
if (!enabled) return false
|
||||
if (expires !== null && expires < Date.now()) return false
|
||||
switch (type) {
|
||||
case 'word': {
|
||||
if (statusText.includes(value) || statusSummary.includes(value)) {
|
||||
return { hide, name }
|
||||
}
|
||||
}
|
||||
case 'regexp': {
|
||||
try {
|
||||
const re = new RegExp(value, 'i')
|
||||
if (re.test(statusText) || re.test(statusSummary)) {
|
||||
return { hide, name }
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}).filter(_ => _)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { toRaw } from 'vue'
|
||||
import {
|
||||
isEqual,
|
||||
cloneDeep,
|
||||
set,
|
||||
unset,
|
||||
get,
|
||||
clamp,
|
||||
flatten,
|
||||
|
@ -15,6 +17,7 @@ import {
|
|||
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
|
||||
|
||||
export const VERSION = 1
|
||||
export const CONFIG_MIGRATION = 1
|
||||
export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
|
||||
|
||||
export const COMMAND_TRIM_FLAGS = 1000
|
||||
|
@ -26,6 +29,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 +39,8 @@ export const defaultState = {
|
|||
_journal: [],
|
||||
simple: {
|
||||
dontShowUpdateNotifs: false,
|
||||
collapseNav: false
|
||||
collapseNav: false,
|
||||
muteFilters: {}
|
||||
},
|
||||
collections: {
|
||||
pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
|
||||
|
@ -78,11 +83,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 +229,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 +265,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 +326,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
|
||||
})
|
|
@ -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', () => {
|
||||
|
|
Loading…
Add table
Reference in a new issue