Merge branch 'sss-objects' into 'develop'
Object/Map support for server-side storage + synchronized mutes See merge request pleroma/pleroma-fe!2104
This commit is contained in:
commit
4bdbdb3920
29 changed files with 1223 additions and 407 deletions
1
changelog.d/mutes-sync.add
Normal file
1
changelog.d/mutes-sync.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Synchronized mutes, advanced mute control (regexp, expiry, naming)
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
"querystring-es3": "0.2.1",
|
||||
"url": "0.11.4",
|
||||
"utf8": "3.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"vue": "3.5.13",
|
||||
"vue-i18n": "11",
|
||||
"vue-router": "4.5.0",
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Popover from 'components/popover/popover.vue'
|
|||
import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
|
||||
import ModifiedIndicator from '../helpers/modified_indicator.vue'
|
||||
import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
const EmojiTab = {
|
||||
components: {
|
||||
|
|
@ -232,7 +233,7 @@ const EmojiTab = {
|
|||
})
|
||||
},
|
||||
displayError (msg) {
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'admin_dash.emoji.error',
|
||||
messageArgs: [msg],
|
||||
level: 'error'
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import StringSetting from '../helpers/string_setting.vue'
|
|||
import GroupSetting from '../helpers/group_setting.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
|
@ -80,7 +81,7 @@ const FrontendsTab = {
|
|||
this.$store.dispatch('loadFrontendsStuff')
|
||||
if (response.error) {
|
||||
const reason = await response.error.json()
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'error',
|
||||
messageKey: 'admin_dash.frontend.failure_installing_frontend',
|
||||
messageArgs: {
|
||||
|
|
@ -90,7 +91,7 @@ const FrontendsTab = {
|
|||
timeout: 5000
|
||||
})
|
||||
} else {
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'success',
|
||||
messageKey: 'admin_dash.frontend.success_installing_frontend',
|
||||
messageArgs: {
|
||||
|
|
|
|||
46
src/components/settings_modal/helpers/help_indicator.vue
Normal file
46
src/components/settings_modal/helpers/help_indicator.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<span class="HelpIndicator">
|
||||
<Popover
|
||||
trigger="click"
|
||||
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
|
||||
>
|
||||
<template #trigger>
|
||||
|
||||
<FAIcon icon="circle-question" />
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="help-tooltip">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleQuestion
|
||||
)
|
||||
|
||||
export default {
|
||||
components: { Popover }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.HelpIndicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-tooltip {
|
||||
margin: 0.5em 1em;
|
||||
min-width: 10em;
|
||||
max-width: 30vw;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ const AppearanceTab = {
|
|||
},
|
||||
onImportFailure (result) {
|
||||
console.error('Failure importing theme:', result)
|
||||
this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||
},
|
||||
importValidator (parsed, filename) {
|
||||
if (filename.endsWith('.json')) {
|
||||
|
|
|
|||
|
|
@ -1,48 +1,207 @@
|
|||
import { filter, trim, debounce } from 'lodash'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
import {
|
||||
newImporter,
|
||||
newExporter
|
||||
} from 'src/services/export_import/export_import.js'
|
||||
|
||||
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 HelpIndicator from '../helpers/help_indicator.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 SUPPORTED_TYPES = new Set(['word', 'regexp', 'user', 'user_regexp'])
|
||||
|
||||
const FilteringTab = {
|
||||
data () {
|
||||
return {
|
||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'),
|
||||
replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
label: this.$t(`settings.reply_visibility_${mode}`)
|
||||
}))
|
||||
})),
|
||||
muteFiltersDraftObject: cloneDeep(useServerSideStorageStore().prefsStorage.simple.muteFilters),
|
||||
muteFiltersDraftDirty: Object.fromEntries(
|
||||
Object.entries(
|
||||
useServerSideStorageStore().prefsStorage.simple.muteFilters
|
||||
).map(([k]) => [k, false])
|
||||
),
|
||||
exportedFilter: null,
|
||||
filterImporter: newImporter({
|
||||
validator (parsed) {
|
||||
if (Array.isArray(parsed)) return false
|
||||
if (!SUPPORTED_TYPES.has(parsed.type)) return false
|
||||
return true
|
||||
},
|
||||
onImport: (data) => {
|
||||
const {
|
||||
enabled = true,
|
||||
expires = null,
|
||||
hide = false,
|
||||
name = '',
|
||||
value = ''
|
||||
} = data
|
||||
|
||||
this.createFilter({
|
||||
enabled,
|
||||
expires,
|
||||
hide,
|
||||
name,
|
||||
value
|
||||
})
|
||||
},
|
||||
onImportFailure (result) {
|
||||
console.error('Failure importing filter:', result)
|
||||
useInterfaceStore()
|
||||
.pushGlobalNotice({
|
||||
messageKey: 'settings.filter.import_failure',
|
||||
level: 'error'
|
||||
})
|
||||
}
|
||||
}),
|
||||
filterExporter: newExporter({
|
||||
filename: 'pleromafe_mute-filter',
|
||||
getExportedObject: () => this.exportedFilter
|
||||
})
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
UnitSetting,
|
||||
IntegerSetting
|
||||
IntegerSetting,
|
||||
Checkbox,
|
||||
Select,
|
||||
HelpIndicator
|
||||
},
|
||||
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
|
||||
}
|
||||
),
|
||||
muteFiltersDraft () {
|
||||
return Object.entries(this.muteFiltersDraftObject)
|
||||
},
|
||||
debouncedSetMuteWords () {
|
||||
return debounce((value) => {
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}, 1000)
|
||||
muteFiltersExpired () {
|
||||
const now = Date.now()
|
||||
return Object
|
||||
.entries(this.muteFiltersDraftObject)
|
||||
.filter(([, { expires }]) => expires != null && expires <= now)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']),
|
||||
getDatetimeLocal (timestamp) {
|
||||
const date = new Date(timestamp)
|
||||
let fmt = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 2})
|
||||
const datetime = [
|
||||
date.getFullYear(),
|
||||
'-',
|
||||
fmt.format(date.getMonth() + 1),
|
||||
'T',
|
||||
fmt.format(date.getHours()),
|
||||
':',
|
||||
fmt.format(date.getMinutes())
|
||||
].join('')
|
||||
return datetime
|
||||
},
|
||||
checkRegexValid (id) {
|
||||
const filter = this.muteFiltersObject[id]
|
||||
if (filter.type !== 'regexp') return true
|
||||
if (filter.type !== 'user_regexp') return true
|
||||
const { value } = filter
|
||||
let valid = true
|
||||
try {
|
||||
new RegExp(value)
|
||||
} catch {
|
||||
valid = false
|
||||
console.error('Invalid RegExp: ' + value)
|
||||
}
|
||||
return valid
|
||||
},
|
||||
createFilter (filter = {
|
||||
type: 'word',
|
||||
value: '',
|
||||
name: 'New Filter',
|
||||
enabled: true,
|
||||
expires: null,
|
||||
hide: false,
|
||||
}) {
|
||||
const newId = uuidv4()
|
||||
|
||||
filter.order = this.muteFilters.length + 2
|
||||
this.muteFiltersDraftObject[newId] = filter
|
||||
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
exportFilter(id) {
|
||||
this.exportedFilter = { ...this.muteFiltersDraftObject[id] }
|
||||
delete this.exportedFilter.order
|
||||
this.filterExporter.exportData()
|
||||
},
|
||||
importFilter() {
|
||||
this.filterImporter.importData()
|
||||
},
|
||||
copyFilter (id) {
|
||||
const filter = { ...this.muteFiltersDraftObject[id] }
|
||||
const newId = uuidv4()
|
||||
|
||||
this.muteFiltersDraftObject[newId] = filter
|
||||
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
deleteFilter (id) {
|
||||
delete this.muteFiltersDraftObject[id]
|
||||
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
purgeExpiredFilters () {
|
||||
this.muteFiltersExpired.forEach(([id]) => {
|
||||
console.log(id)
|
||||
delete this.muteFiltersDraftObject[id]
|
||||
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
|
||||
})
|
||||
this.pushServerSideStorage()
|
||||
},
|
||||
updateFilter(id, field, value) {
|
||||
const filter = { ...this.muteFiltersDraftObject[id] }
|
||||
if (field === 'expires-never') {
|
||||
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.muteFiltersDraftObject[id] = filter
|
||||
this.muteFiltersDraftDirty[id] = true
|
||||
},
|
||||
saveFilter(id) {
|
||||
this.setPreference({ path: 'simple.muteFilters.' + id , value: this.muteFiltersDraftObject[id] })
|
||||
this.pushServerSideStorage()
|
||||
this.muteFiltersDraftDirty[id] = false
|
||||
},
|
||||
},
|
||||
// Updating nested properties
|
||||
watch: {
|
||||
replyVisibility () {
|
||||
|
|
|
|||
89
src/components/settings_modal/tabs/filtering_tab.scss
Normal file
89
src/components/settings_modal/tabs/filtering_tab.scss
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
.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;
|
||||
align-items: baseline;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.never {
|
||||
margin-left: 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;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: inline-grid;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
max-width: 100%;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.muteFiltersActions,
|
||||
.muteFiltersActionsBottom {
|
||||
display: grid;
|
||||
align-items: baseline;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
.total {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.settings-modal.-mobile .filtering-tab {
|
||||
.filter-buttons {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,88 @@
|
|||
<template>
|
||||
<div :label="$t('settings.filtering')">
|
||||
<div
|
||||
:label="$t('settings.filtering')"
|
||||
class="filtering-tab"
|
||||
>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.posts') }}</h2>
|
||||
<h2>{{ $t('settings.filter.clutter') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
id="replyVisibility"
|
||||
path="replyVisibility"
|
||||
:options="replyVisibilityOptions"
|
||||
>
|
||||
{{ $t('settings.replies_in_timeline') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
expert="1"
|
||||
path="hidePostStats"
|
||||
>
|
||||
{{ $t('settings.hide_post_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
expert="1"
|
||||
path="hideUserStats"
|
||||
>
|
||||
{{ $t('settings.hide_user_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideBotIndication">
|
||||
{{ $t('settings.hide_actor_type_indication') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideScrobbles">
|
||||
{{ $t('settings.hide_scrobbles') }}
|
||||
</BooleanSetting>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
<UnitSetting
|
||||
key="hideScrobblesAfter"
|
||||
path="hideScrobblesAfter"
|
||||
:units="['m', 'h', 'd']"
|
||||
unit-set="time"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_scrobbles_after') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path="maxThumbnails"
|
||||
expert="1"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</IntegerSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.filter.mute_filter') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="hideFilteredStatuses">
|
||||
{{ $t('settings.hide_filtered_statuses') }}
|
||||
{{ $t('settings.hide_muted_statuses') }}
|
||||
</BooleanSetting>
|
||||
<ul class="setting-list suboptions">
|
||||
<li>
|
||||
|
|
@ -13,8 +90,9 @@
|
|||
parent-path="hideFilteredStatuses"
|
||||
:parent-invert="true"
|
||||
path="hideWordFilteredPosts"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_wordfiltered_statuses') }}
|
||||
{{ $t('settings.hide_filtered_statuses') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
|
|
@ -50,83 +128,218 @@
|
|||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideBotIndication">
|
||||
{{ $t('settings.hide_actor_type_indication') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
id="replyVisibility"
|
||||
path="replyVisibility"
|
||||
:options="replyVisibilityOptions"
|
||||
>
|
||||
{{ $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>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path="maxThumbnails"
|
||||
expert="1"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</IntegerSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachmentsInConv">
|
||||
{{ $t('settings.hide_attachments_in_convo') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideScrobbles">
|
||||
{{ $t('settings.hide_scrobbles') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<UnitSetting
|
||||
key="hideScrobblesAfter"
|
||||
path="hideScrobblesAfter"
|
||||
:units="['m', 'h', 'd']"
|
||||
unit-set="time"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_scrobbles_after') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
class="setting-item"
|
||||
>
|
||||
<h2>{{ $t('settings.user_profiles') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="hideUserStats">
|
||||
{{ $t('settings.hide_user_stats') }}
|
||||
</BooleanSetting>
|
||||
<h3>{{ $t('settings.filter.custom_filters') }}</h3>
|
||||
<p class="muteFiltersActions">
|
||||
<span class="total">
|
||||
{{ $t('settings.filter.total_count', { count: muteFiltersDraft.length }) }}
|
||||
</span>
|
||||
<button
|
||||
class="add-button button-default"
|
||||
type="button"
|
||||
@click="createFilter()"
|
||||
>
|
||||
{{ $t('settings.filter.new') }}
|
||||
</button>
|
||||
<button
|
||||
class="add-button button-default"
|
||||
type="button"
|
||||
@click="importFilter()"
|
||||
>
|
||||
{{ $t('settings.filter.import') }}
|
||||
</button>
|
||||
</p>
|
||||
<div class="muteFilterContainer">
|
||||
<div
|
||||
v-for="filter in muteFiltersDraft"
|
||||
:key="filter[0]"
|
||||
class="mute-filter"
|
||||
:style="{ order: filter[1].order }"
|
||||
>
|
||||
<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)"
|
||||
>
|
||||
<span
|
||||
v-if="filter[1].expires !== null && Date.now() > filter[1].expires"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ $t('settings.filter.expired') }}
|
||||
</span>
|
||||
</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]">
|
||||
<HelpIndicator>
|
||||
<p>
|
||||
{{ $t('settings.filter.help.word') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('settings.filter.help.user') }}
|
||||
</p>
|
||||
<i18n-t
|
||||
keypath="settings.filter.help.regexp"
|
||||
tag="p"
|
||||
>
|
||||
<template #link>
|
||||
<a
|
||||
:href="$t('settings.filter.help.regexp_url')"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('settings.filter.help.regexp_link') }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</HelpIndicator>
|
||||
{{ $t('settings.filter.type') }}
|
||||
</label>
|
||||
<Select
|
||||
:id="'filterType' + filter[0]"
|
||||
class="filter-field-value"
|
||||
:model-value="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>
|
||||
<option value="user">
|
||||
{{ $t('settings.filter.user') }}
|
||||
</option>
|
||||
<option value="user_regexp">
|
||||
{{ $t('settings.filter.user_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 never"
|
||||
@update:model-value="updateFilter(filter[0], 'expires-never', $event)"
|
||||
>
|
||||
{{ $t('settings.filter.never_expires') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!checkRegexValid(filter[0])"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('settings.filter.regexp_error') }}
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<button
|
||||
class="delete-button button-default -danger"
|
||||
type="button"
|
||||
@click="deleteFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.delete') }}
|
||||
</button>
|
||||
<button
|
||||
class="export-button button-default"
|
||||
type="button"
|
||||
@click="exportFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.export') }}
|
||||
</button>
|
||||
<button
|
||||
class="copy-button button-default"
|
||||
type="button"
|
||||
@click="copyFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.copy') }}
|
||||
</button>
|
||||
<button
|
||||
class="save-button button-default"
|
||||
:class="{ disabled: !muteFiltersDraftDirty[filter[0]] }"
|
||||
:disabled="!muteFiltersDraftDirty[filter[0]]"
|
||||
type="button"
|
||||
@click="saveFilter(filter[0])"
|
||||
>
|
||||
{{ $t('settings.filter.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muteFiltersActionsBottom">
|
||||
<span class="total">
|
||||
{{ $t('settings.filter.expired_count', { count: muteFiltersExpired.length }) }}
|
||||
</span>
|
||||
<button
|
||||
class="add-button button-default"
|
||||
type="button"
|
||||
:class="{ disabled: muteFiltersExpired.length === 0 }"
|
||||
:disabled="muteFiltersExpired.length === 0"
|
||||
@click="purgeExpiredFilters()"
|
||||
>
|
||||
{{ $t('settings.filter.purge_expired') }}
|
||||
</button>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./filtering_tab.js"></script>
|
||||
<style src="./filtering_tab.scss"></style>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ const saveImmedeatelyActions = [
|
|||
'markNotificationsAsSeen',
|
||||
'clearCurrentUser',
|
||||
'setCurrentUser',
|
||||
'setServerSideStorage',
|
||||
'setHighlight',
|
||||
'setOption',
|
||||
'setClientData',
|
||||
|
|
|
|||
42
src/modules/config_declaration.js
Normal file
42
src/modules/config_declaration.js
Normal file
|
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,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(_ => _)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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
Add a link
Reference in a new issue