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: {
|
||||
draft: {
|
||||
// TODO allow passing shared draft object?
|
||||
get () {
|
||||
if (this.realSource === 'admin' || this.path == null) {
|
||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||
|
|
|
@ -1,15 +1,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
|
||||
|
|
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>
|
||||
<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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue