UI for new filters

This commit is contained in:
Henry Jameson 2025-03-25 19:01:32 +02:00
parent 5d47ac04b0
commit 57aa8818a9
7 changed files with 339 additions and 39 deletions

View file

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

View file

@ -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 ChoiceSetting from '../helpers/choice_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const FilteringTab = {
data () {
return {
muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'),
replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({
key: mode,
value: mode,
@ -21,26 +25,95 @@ const FilteringTab = {
BooleanSetting,
ChoiceSetting,
UnitSetting,
IntegerSetting
IntegerSetting,
Checkbox,
Select
},
computed: {
...SharedComputedObject(),
muteWordsString: {
get () {
return this.muteWordsStringLocal
},
set (value) {
this.muteWordsStringLocal = value
this.debouncedSetMuteWords(value)
...mapState(
useServerSideStorageStore,
{
muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters),
muteFiltersObject: store => store.prefsStorage.simple.muteFilters
}
)
},
methods: {
...mapActions(useServerSideStorageStore, ['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 () {
return debounce((value) => {
this.$store.dispatch('setOption', {
name: 'muteWords',
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}, 1000)
checkRegexValid (id) {
const filter = this.muteFiltersObject[id]
if (filter.type !== 'regexp') return true
const { value } = filter
let valid = true
try {
new RegExp(value)
} catch (e) {
valid = false
console.error('Invalid RegExp: ' + value)
}
return valid
},
createFilter () {
const filter = {
type: 'word',
value: '',
name: 'New Filter',
enabled: true,
expires: null,
hide: false,
order: this.muteFilters.length + 2
}
const newId = uuidv4()
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
this.pushServerSideStorage()
},
copyFilter (id) {
const filter = { ...this.muteFiltersObject[id] }
const newId = uuidv4()
this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
this.pushServerSideStorage()
},
deleteFilter (id) {
this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
this.pushServerSideStorage()
},
updateFilter(id, field, value) {
const filter = { ...this.muteFiltersObject[id] }
if (field === 'expires-never') {
// filter[field] = value
if (!value) {
const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks
const date = Date.now() + offset
filter.expires = date
} else {
filter.expires = null
}
} else if (field === 'expires') {
const parsed = Date.parse(value)
filter.expires = parsed.valueOf()
} else {
filter[field] = value
}
this.setPreference({ path: 'simple.muteFilters.' + id , value: filter })
this.pushServerSideStorage()
}
},
// Updating nested properties

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

View file

@ -1,5 +1,5 @@
<template>
<div :label="$t('settings.filtering')">
<div :label="$t('settings.filtering')" class="filtering-tab">
<div class="setting-item">
<h2>{{ $t('settings.posts') }}</h2>
<ul class="setting-list">
@ -68,13 +68,148 @@
{{ $t('settings.replies_in_timeline') }}
</ChoiceSetting>
<li>
<h3>{{ $t('settings.wordfilter') }}</h3>
<textarea
id="muteWords"
v-model="muteWordsString"
class="input resize-height"
/>
<div>{{ $t('settings.filtering_explanation') }}</div>
<h3>{{ $t('settings.filter.mute_filter') }}</h3>
<div class="muteFilterContainer">
<div
class="mute-filter"
:style="{ order: filter[1].order }"
v-for="filter in muteFilters"
key="filter[0]"
>
<div class="filter-name">
<label
:for="'filterName' + filter[0]"
>
{{ $t('settings.filter.name') }}
</label>
{{ ' ' }}
<input
:id="'filterName' + filter[0]"
class="input"
:value="filter[1].name"
@input="updateFilter(filter[0], 'name', $event.target.value)"
>
</div>
<div class="filter-enabled">
<Checkbox
:id="'filterHide' + filter[0]"
:model-value="filter[1].hide"
:name="'filterHide' + filter[0]"
@update:model-value="updateFilter(filter[0], 'hide', $event.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>
<h3>{{ $t('settings.attachments') }}</h3>
<li>
@ -130,3 +265,4 @@
</div>
</template>
<script src="./filtering_tab.js"></script>
<style src="./filtering_tab.scss"></style>

View file

@ -409,6 +409,23 @@
"visual_tweaks": "Minor visual tweaks",
"theme_debug": "Show what background theme engine assumes when dealing with transparancy (DEBUG)",
"scale_and_layout": "Interface scale and layout",
"enabled": "Enabled",
"filter": {
"mute_filter": "Mute Filters",
"type": "Filter type",
"regexp": "RegExp",
"plain": "Simple",
"hide": "Hide completely",
"name": "Name",
"value": "Value",
"expires": "Expires",
"expired": "Expired",
"copy": "Copy filter",
"delete": "Remove filter",
"new": "Create filter",
"regexp_error": "Invalid Regular Expression",
"never_expires": "Never"
},
"mfa": {
"otp": "OTP",
"setup_otp": "Setup OTP",

View file

@ -623,35 +623,47 @@ const users = {
// Do server-side storage migrations
// Debug snippet to clean up storage and reset migrations
/*
// Reset wordfilter
Object.keys(
useServerSideStorageStore().prefsStorage.simple.muteFilters
).forEach(key => {
useServerSideStorageStore().unsetPreference({ path: 'simple.muteFilters.' + key, value: null })
})
// Reset flag to 0 to re-run migrations
useServerSideStorageStore().setFlag({ flag: 'configMigration', value: 0 })
/**/
const { configMigration } = useServerSideStorageStore().flagStorage
// Wordfilter migration
if (configMigration < 1) {
// 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
store.rootState.config.muteWords.forEach(word => {
store.rootState.config.muteWords.forEach((word, order) => {
const uniqueId = uuidv4()
useServerSideStorageStore().setPreference({
path: 'simple.muteFilters.' + uniqueId,
value: {
type: 'word',
value: word
value: word,
name: word,
enabled: true,
expires: null,
hide: false,
order
}
})
})
}
// Update the flag
useServerSideStorageStore().setflag({ flag: 'configMigration', value: CONFIG_MIGRATION })
useServerSideStorageStore().pushServerSideStorage()
if (configMigration < CONFIG_MIGRATION) {
// Update the flag
useServerSideStorageStore().setFlag({ flag: 'configMigration', value: CONFIG_MIGRATION })
useServerSideStorageStore().pushServerSideStorage()
}
if (user.token) {
dispatch('setWsToken', user.token)

View file

@ -17,6 +17,7 @@ import {
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
export const VERSION = 1
export const CONFIG_MIGRATION = 1
export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
export const COMMAND_TRIM_FLAGS = 1000
@ -28,6 +29,7 @@ export const defaultState = {
// storage of flags - stuff that can only be set and incremented
flagStorage: {
updateCounter: 0, // Counter for most recent update notification seen
configMigration: 0, // Counter for config -> server-side migrations
reset: 0 // special flag that can be used to force-reset all flags, debug purposes only
// special reset codes:
// 1000: trim keys to those known by currently running FE