Merge remote-tracking branch 'origin/develop' into from/develop/tusooa/sw-cache-assets

This commit is contained in:
Henry Jameson 2025-06-25 15:40:56 +03:00
commit a1f43234cd
235 changed files with 6354 additions and 4065 deletions

View file

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

View file

@ -26,8 +26,7 @@
.palettes-container {
height: 15em;
overflow-y: auto;
overflow-x: hidden;
overflow: hidden auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);
@ -112,8 +111,7 @@
flex-wrap: wrap;
margin: -0.5em 0;
height: 25em;
overflow-x: hidden;
overflow-y: auto;
overflow: hidden auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);

View file

@ -336,6 +336,15 @@
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li>
<UnitSetting
path="themeEditorMinWidth"
:units="['px', 'rem']"
expert="1"
>
{{ $t('settings.theme_editor_min_width') }}
</UnitSetting>
</li>
</ul>
</div>
<div class="setting-item">

View file

@ -2,6 +2,7 @@ import Importer from 'src/components/importer/importer.vue'
import Exporter from 'src/components/exporter/exporter.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { mapState } from 'vuex'
import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
const DataImportExportTab = {
data () {
@ -15,7 +16,7 @@ const DataImportExportTab = {
}
},
created () {
this.$store.dispatch('fetchTokens')
useOAuthTokensStore().fetchTokens()
this.fetchBackups()
},
components: {

View file

@ -1,48 +1,209 @@
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),
'-',
fmt.format(date.getDate()),
'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 () {

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

View file

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

View file

@ -11,6 +11,7 @@ import ProgressButton from 'src/components/progress_button/progress_button.vue'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
const BlockList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
@ -39,7 +40,7 @@ const MutesAndBlocks = {
}
},
created () {
this.$store.dispatch('fetchTokens')
useOAuthTokensStore().fetchTokens()
this.$store.dispatch('getKnownDomains')
},
components: {

View file

@ -216,7 +216,7 @@ const ProfileTab = {
this.submitBackground('')
}
},
submitAvatar (cropper, file) {
submitAvatar (canvas, file) {
const that = this
return new Promise((resolve, reject) => {
function updateAvatar (avatar, avatarName) {
@ -232,8 +232,8 @@ const ProfileTab = {
})
}
if (cropper) {
cropper.getCroppedCanvas().toBlob((data) => updateAvatar(data, file.name), file.type)
if (canvas) {
canvas.toBlob((data) => updateAvatar(data, file.name), file.type)
} else {
updateAvatar(file, file.name)
}

View file

@ -2,6 +2,7 @@ import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Mfa from './mfa.vue'
import localeService from 'src/services/locale/locale.service.js'
import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
const SecurityTab = {
data () {
@ -28,7 +29,7 @@ const SecurityTab = {
}
},
created () {
this.$store.dispatch('fetchTokens')
useOAuthTokensStore().fetchTokens()
this.fetchAliases()
},
components: {
@ -40,11 +41,11 @@ const SecurityTab = {
user () {
return this.$store.state.users.currentUser
},
pleromaBackend () {
return this.$store.state.instance.pleromaBackend
pleromaExtensionsAvailable () {
return this.$store.state.instance.pleromaExtensionsAvailable
},
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return useOAuthTokensStore().tokens.map(oauthToken => {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
@ -151,7 +152,7 @@ const SecurityTab = {
},
revokeToken (id) {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
useOAuthTokensStore().revokeToken(id)
}
}
}

View file

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

View file

@ -1,4 +1,6 @@
.StyleTab {
min-width: var(--themeEditorMinWidth, fit-content);
.style-control {
display: flex;
flex-wrap: wrap;
@ -185,7 +187,7 @@
.state-selector,
.variant-selector {
display: grid;
grid-template-columns: 1fr minmax(1fr, 10em);
grid-template-columns: 1fr minmax(10em, 1fr);
grid-template-rows: auto;
grid-auto-flow: column;
grid-gap: 0.5em;

View file

@ -241,10 +241,7 @@ export default {
.underlay-preview {
position: absolute;
top: 0;
bottom: 0;
left: 10px;
right: 10px;
inset: 0 10px;
}
}
</style>

View file

@ -1,4 +1,6 @@
.theme-tab {
min-width: var(--themeEditorMinWidth, fit-content);
.deprecation-warning {
padding: 0.5em;
margin: 2em;