UI for new filters
This commit is contained in:
parent
5d47ac04b0
commit
57aa8818a9
7 changed files with 339 additions and 39 deletions
|
@ -78,7 +78,6 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
draft: {
|
draft: {
|
||||||
// TODO allow passing shared draft object?
|
|
||||||
get () {
|
get () {
|
||||||
if (this.realSource === 'admin' || this.path == null) {
|
if (this.realSource === 'admin' || this.path == null) {
|
||||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
import { filter, trim, debounce } 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 BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
import UnitSetting from '../helpers/unit_setting.vue'
|
import UnitSetting from '../helpers/unit_setting.vue'
|
||||||
import IntegerSetting from '../helpers/integer_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'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
|
||||||
const FilteringTab = {
|
const FilteringTab = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'),
|
|
||||||
replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({
|
replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({
|
||||||
key: mode,
|
key: mode,
|
||||||
value: mode,
|
value: mode,
|
||||||
|
@ -21,26 +25,95 @@ const FilteringTab = {
|
||||||
BooleanSetting,
|
BooleanSetting,
|
||||||
ChoiceSetting,
|
ChoiceSetting,
|
||||||
UnitSetting,
|
UnitSetting,
|
||||||
IntegerSetting
|
IntegerSetting,
|
||||||
|
Checkbox,
|
||||||
|
Select
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
muteWordsString: {
|
...mapState(
|
||||||
get () {
|
useServerSideStorageStore,
|
||||||
return this.muteWordsStringLocal
|
{
|
||||||
},
|
muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters),
|
||||||
set (value) {
|
muteFiltersObject: store => store.prefsStorage.simple.muteFilters
|
||||||
this.muteWordsStringLocal = value
|
|
||||||
this.debouncedSetMuteWords(value)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']),
|
||||||
|
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 () {
|
checkRegexValid (id) {
|
||||||
return debounce((value) => {
|
const filter = this.muteFiltersObject[id]
|
||||||
this.$store.dispatch('setOption', {
|
if (filter.type !== 'regexp') return true
|
||||||
name: 'muteWords',
|
const { value } = filter
|
||||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
let valid = true
|
||||||
})
|
try {
|
||||||
}, 1000)
|
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
|
// 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: 20vh;
|
||||||
|
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>
|
<template>
|
||||||
<div :label="$t('settings.filtering')">
|
<div :label="$t('settings.filtering')" class="filtering-tab">
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.posts') }}</h2>
|
<h2>{{ $t('settings.posts') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
|
@ -68,13 +68,148 @@
|
||||||
{{ $t('settings.replies_in_timeline') }}
|
{{ $t('settings.replies_in_timeline') }}
|
||||||
</ChoiceSetting>
|
</ChoiceSetting>
|
||||||
<li>
|
<li>
|
||||||
<h3>{{ $t('settings.wordfilter') }}</h3>
|
<h3>{{ $t('settings.filter.mute_filter') }}</h3>
|
||||||
<textarea
|
<div class="muteFilterContainer">
|
||||||
id="muteWords"
|
<div
|
||||||
v-model="muteWordsString"
|
class="mute-filter"
|
||||||
class="input resize-height"
|
:style="{ order: filter[1].order }"
|
||||||
/>
|
v-for="filter in muteFilters"
|
||||||
<div>{{ $t('settings.filtering_explanation') }}</div>
|
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.target.checked)"
|
||||||
|
>
|
||||||
|
{{ $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.target.checked)"
|
||||||
|
>
|
||||||
|
{{ $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>
|
</li>
|
||||||
<h3>{{ $t('settings.attachments') }}</h3>
|
<h3>{{ $t('settings.attachments') }}</h3>
|
||||||
<li>
|
<li>
|
||||||
|
@ -130,3 +265,4 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script src="./filtering_tab.js"></script>
|
<script src="./filtering_tab.js"></script>
|
||||||
|
<style src="./filtering_tab.scss"></style>
|
||||||
|
|
|
@ -409,6 +409,23 @@
|
||||||
"visual_tweaks": "Minor visual tweaks",
|
"visual_tweaks": "Minor visual tweaks",
|
||||||
"theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
|
"theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
|
||||||
"scale_and_layout": "Interface scale and layout",
|
"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": {
|
"mfa": {
|
||||||
"otp": "OTP",
|
"otp": "OTP",
|
||||||
"setup_otp": "Setup OTP",
|
"setup_otp": "Setup OTP",
|
||||||
|
|
|
@ -623,35 +623,47 @@ const users = {
|
||||||
|
|
||||||
// Do server-side storage migrations
|
// 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
|
const { configMigration } = useServerSideStorageStore().flagStorage
|
||||||
|
|
||||||
// Wordfilter migration
|
// Wordfilter migration
|
||||||
if (configMigration < 1) {
|
if (configMigration < 1) {
|
||||||
// Debug snippet to clean up storage
|
|
||||||
/*
|
|
||||||
Object.keys(useServerSideStorageStore().prefsStorage.simple.muteFilters).forEach(key => {
|
|
||||||
useServerSideStorageStore().unsetPreference({ path: 'simple.muteFilters.' + key, value: null })
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Convert existing wordfilter into synced one
|
// Convert existing wordfilter into synced one
|
||||||
store.rootState.config.muteWords.forEach(word => {
|
store.rootState.config.muteWords.forEach((word, order) => {
|
||||||
const uniqueId = uuidv4()
|
const uniqueId = uuidv4()
|
||||||
|
|
||||||
useServerSideStorageStore().setPreference({
|
useServerSideStorageStore().setPreference({
|
||||||
path: 'simple.muteFilters.' + uniqueId,
|
path: 'simple.muteFilters.' + uniqueId,
|
||||||
value: {
|
value: {
|
||||||
type: 'word',
|
type: 'word',
|
||||||
value: word
|
value: word,
|
||||||
|
name: word,
|
||||||
|
enabled: true,
|
||||||
|
expires: null,
|
||||||
|
hide: false,
|
||||||
|
order
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (configMigration < CONFIG_MIGRATION) {
|
||||||
// Update the flag
|
// Update the flag
|
||||||
useServerSideStorageStore().setflag({ flag: 'configMigration', value: CONFIG_MIGRATION })
|
useServerSideStorageStore().setFlag({ flag: 'configMigration', value: CONFIG_MIGRATION })
|
||||||
useServerSideStorageStore().pushServerSideStorage()
|
useServerSideStorageStore().pushServerSideStorage()
|
||||||
|
}
|
||||||
|
|
||||||
if (user.token) {
|
if (user.token) {
|
||||||
dispatch('setWsToken', user.token)
|
dispatch('setWsToken', user.token)
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
|
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
|
||||||
|
|
||||||
export const VERSION = 1
|
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 NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
|
||||||
|
|
||||||
export const COMMAND_TRIM_FLAGS = 1000
|
export const COMMAND_TRIM_FLAGS = 1000
|
||||||
|
@ -28,6 +29,7 @@ export const defaultState = {
|
||||||
// storage of flags - stuff that can only be set and incremented
|
// storage of flags - stuff that can only be set and incremented
|
||||||
flagStorage: {
|
flagStorage: {
|
||||||
updateCounter: 0, // Counter for most recent update notification seen
|
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
|
reset: 0 // special flag that can be used to force-reset all flags, debug purposes only
|
||||||
// special reset codes:
|
// special reset codes:
|
||||||
// 1000: trim keys to those known by currently running FE
|
// 1000: trim keys to those known by currently running FE
|
||||||
|
|
Loading…
Add table
Reference in a new issue