Merge branch 'settings-shuffle' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2025-11-24 20:54:21 +02:00
commit 1abbba698d
61 changed files with 2948 additions and 1940 deletions

View file

@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.9.3
### Fixed
- Being unable to update profile
## 2.9.2
### Changed
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible

2
changelog.d/broken.fix Normal file
View file

@ -0,0 +1,2 @@
Fix display of the broken/deleted/banned users

View file

@ -0,0 +1 @@
Make every configuration option default-overridable by instance admins

View file

@ -1,6 +1,6 @@
{
"name": "pleroma_fe",
"version": "2.9.2",
"version": "2.9.3",
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
"private": false,
@ -17,12 +17,12 @@
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
},
"dependencies": {
"@babel/runtime": "7.28.3",
"@babel/runtime": "7.28.4",
"@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/vue-fontawesome": "3.1.1",
"@fortawesome/fontawesome-svg-core": "7.1.0",
"@fortawesome/free-regular-svg-icons": "7.1.0",
"@fortawesome/free-solid-svg-icons": "7.1.0",
"@fortawesome/vue-fontawesome": "3.1.2",
"@kazvmoe-infra/pinch-zoom-element": "1.3.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
@ -39,7 +39,7 @@
"js-cookie": "3.0.5",
"localforage": "1.10.0",
"parse-link-header": "2.0.0",
"phoenix": "1.8.0",
"phoenix": "1.8.1",
"pinia": "^3.0.0",
"punycode.js": "2.3.1",
"qrcode": "1.5.4",
@ -47,15 +47,15 @@
"url": "0.11.4",
"utf8": "3.0.0",
"uuid": "11.1.0",
"vue": "3.5.19",
"vue": "3.5.22",
"vue-i18n": "11",
"vue-router": "4.5.1",
"vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0"
},
"devDependencies": {
"@babel/core": "7.28.3",
"@babel/eslint-parser": "7.28.0",
"@babel/core": "7.28.4",
"@babel/eslint-parser": "7.28.4",
"@babel/plugin-transform-runtime": "7.28.3",
"@babel/preset-env": "7.28.3",
"@babel/register": "7.28.3",
@ -66,24 +66,24 @@
"@vitest/ui": "^3.0.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.5.0",
"@vue/compiler-sfc": "3.5.19",
"@vue/compiler-sfc": "3.5.22",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.21",
"babel-plugin-lodash": "3.3.4",
"chai": "5.3.2",
"chalk": "5.6.0",
"chai": "5.3.3",
"chalk": "5.6.2",
"chromedriver": "135.0.4",
"connect-history-api-fallback": "2.0.0",
"cross-spawn": "7.0.6",
"custom-event-polyfill": "1.0.7",
"eslint": "9.33.0",
"eslint": "9.37.0",
"vue-eslint-parser": "10.2.0",
"eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.21.3",
"eslint-plugin-n": "17.23.1",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "10.4.0",
"eslint-plugin-vue": "10.5.0",
"eventsource-polyfill": "0.9.6",
"express": "5.1.0",
"function-bind": "1.1.2",
@ -96,14 +96,14 @@
"postcss": "8.5.6",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
"sass": "1.89.2",
"sass": "1.93.2",
"selenium-server": "3.141.59",
"semver": "7.7.2",
"semver": "7.7.3",
"serve-static": "2.2.0",
"shelljs": "0.10.0",
"sinon": "20.0.0",
"sinon-chai": "4.0.0",
"stylelint": "16.19.1",
"sinon-chai": "4.0.1",
"stylelint": "16.25.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^16.0.0",
"stylelint-config-recommended-scss": "^14.0.0",

View file

@ -24,6 +24,7 @@ import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface'
import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow'
import { staticOrApiConfigDefault, instanceDefaultConfig } from 'src/modules/default_config_state.js'
let staticInitialResults = null
@ -130,50 +131,15 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
}
const copyInstanceOption = (name) => {
store.dispatch('setInstanceOption', { name, value: config[name] })
if (typeof config[name] !== 'undefined') {
store.dispatch('setInstanceOption', { name, value: config[name] })
}
}
copyInstanceOption('theme')
copyInstanceOption('style')
copyInstanceOption('palette')
copyInstanceOption('embeddedToS')
copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
copyInstanceOption('hideBotIndication')
copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo')
Object.keys(staticOrApiConfigDefault).forEach(copyInstanceOption)
Object.keys(instanceDefaultConfig).forEach(copyInstanceOption)
store.dispatch('setInstanceOption', {
name: 'logoMask',
value: typeof config.logoMask === 'undefined'
? true
: config.logoMask
})
store.dispatch('setInstanceOption', {
name: 'logoMargin',
value: typeof config.logoMargin === 'undefined'
? 0
: config.logoMargin
})
copyInstanceOption('logoLeft')
useAuthFlowStore().setInitialStrategy(config.loginMethod)
copyInstanceOption('redirectRootNoLogin')
copyInstanceOption('redirectRootLogin')
copyInstanceOption('showInstanceSpecificPanel')
copyInstanceOption('minimalScopesMode')
copyInstanceOption('hideMutedPosts')
copyInstanceOption('collapseMessageWithSubject')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior')
copyInstanceOption('postContentType')
copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename')
copyInstanceOption('sidebarRight')
}
const getTOS = async ({ store }) => {

View file

@ -23,6 +23,9 @@
<style lang="scss">
.exporter {
display: flex;
flex-direction: column;
&-processing {
margin: 0.25em;
}

View file

@ -1,13 +1,5 @@
<template>
<div class="font-control">
<label
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ $t('settings.style.themes3.font.label', { label }) }}
</label>
{{ ' ' }}
<Checkbox
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
@ -15,8 +7,15 @@
:model-value="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
{{ $t('settings.style.themes3.define') }}
<i18n-t
scope="global"
keypath="settings.style.fonts.override"
tag="span"
>
{{ label }}
</i18n-t>
</Checkbox>
{{ ' ' }}
<div
v-if="modelValue?.family"
class="font-input"
@ -143,10 +142,6 @@
margin-left: 2em;
margin-top: 0.5em;
}
.font-checkbox {
margin-left: 1em;
}
}
.invalid-tooltip {

View file

@ -1,5 +1,5 @@
<template>
<div class="importer">
<div class="importer btn-group">
<form>
<input
ref="input"
@ -19,7 +19,7 @@
class="btn button-default"
@click="submit"
>
{{ submitButtonLabel || $t('importer.submit') }}
{{ submitButtonLabel || $t('importer.import') }}
</button>
<div v-if="success">
<button

View file

@ -299,8 +299,8 @@ const Popover = {
if (this.trigger === 'click') {
document.removeEventListener('click', this.onClickOutside)
}
this.scrollable.removeEventListener('scroll', this.onScroll)
this.scrollable.removeEventListener('resize', this.onResize)
this.scrollable?.removeEventListener('scroll', this.onScroll)
this.scrollable?.removeEventListener('resize', this.onResize)
},
resizePopover () {
setTimeout(() => {

View file

@ -104,8 +104,8 @@
.visibility-tray {
display: flex;
justify-content: space-between;
padding-top: 0.5em;
align-items: baseline;
margin-left: -0.5em;
}
.visibility-notice {

View file

@ -14,13 +14,33 @@ library.add(
)
const ScopeSelector = {
props: [
'showAll',
'userDefault',
'originalScope',
'initialScope',
'onScopeChange'
],
props: {
showAll: {
required: true,
type: Boolean
},
userDefault: {
required: true,
type: String
},
originalScope: {
required: false,
type: String
},
initialScope: {
required: false,
type: String
},
onScopeChange: {
required: true,
type: Function
},
unstyled: {
required: false,
type: Boolean,
default: true
}
},
data () {
return {
currentScope: this.initialScope
@ -43,11 +63,12 @@ const ScopeSelector = {
return this.shouldShow('direct')
},
css () {
const style = this.unstyled ? 'button-unstyled' : 'button-default'
return {
public: { toggled: this.currentScope === 'public' },
unlisted: { toggled: this.currentScope === 'unlisted' },
private: { toggled: this.currentScope === 'private' },
direct: { toggled: this.currentScope === 'direct' }
public: [style, { toggled: this.currentScope === 'public' }],
unlisted: [style, { toggled: this.currentScope === 'unlisted' }],
private: [style, { toggled: this.currentScope === 'private' }],
direct: [style, { toggled: this.currentScope === 'direct' }]
}
}
},

View file

@ -1,11 +1,11 @@
<template>
<div
v-if="!showNothing"
class="ScopeSelector"
class="ScopeSelector btn-group"
>
<button
v-if="showDirect"
class="button-unstyled scope"
class="scope"
:class="css.direct"
:title="$t('post_status.scope.direct')"
type="button"
@ -19,7 +19,7 @@
{{ ' ' }}
<button
v-if="showPrivate"
class="button-unstyled scope"
class="scope"
:class="css.private"
:title="$t('post_status.scope.private')"
type="button"
@ -33,7 +33,7 @@
{{ ' ' }}
<button
v-if="showUnlisted"
class="button-unstyled scope"
class="scope"
:class="css.unlisted"
:title="$t('post_status.scope.unlisted')"
type="button"
@ -47,7 +47,7 @@
{{ ' ' }}
<button
v-if="showPublic"
class="button-unstyled scope"
class="scope"
:class="css.public"
:title="$t('post_status.scope.public')"
type="button"
@ -65,12 +65,14 @@
<style lang="scss">
.ScopeSelector {
display: inline-block;
.scope {
display: inline-block;
cursor: pointer;
min-width: 1.3em;
min-height: 1.3em;
text-align: center;
padding: 0.5em 0.25em
}
}
</style>

View file

@ -1,21 +1,48 @@
.settings-modal {
overflow: hidden;
h4 {
h2 {
font-size: 1.3rem;
font-weight: 500;
margin-top: 1em;
margin-bottom: 1em;
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-top: 1em;
margin-bottom: 0.5em;
border-bottom: 1px solid var(--border);
padding-bottom: 0.25em;
box-sizing: border-box;
padding-left: 0.5em;
}
h4 {
font-size: 1.1rem;
margin-top: 1em;
margin-bottom: 0.5em;
}
h5 {
font-size: 1rem;
margin-bottom: 0.5em;
margin-top: 0;
}
.setting-list,
.option-list {
list-style-type: none;
padding-left: 2em;
margin: 0;
.btn:not(.dropdown-button) {
padding: 0 2em;
}
li {
margin-bottom: 0.5em;
margin: 1em 0;
}
.suboptions {
@ -42,7 +69,7 @@
transition: transform;
transition-timing-function: ease-in-out;
transition-duration: 300ms;
width: 1000px;
width: 70em;
max-width: 90vw;
height: 90vh;
@ -77,6 +104,12 @@
}
&.-mobile {
.tabs {
.menu-item {
font-size: 1.2em
}
}
.setting-list,
.option-list {
padding-left: 0.25em;

View file

@ -1,4 +1,4 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'
@ -31,7 +31,7 @@ library.add(
const SettingsModalAdminContent = {
components: {
TabSwitcher,
VerticalTabSwitcher,
InstanceTab,
LimitsTab,

View file

@ -1,5 +1,5 @@
<template>
<tab-switcher
<vertical-tab-switcher
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
ref="tabSwitcher"
class="settings_tab-switcher"
@ -71,7 +71,7 @@
>
<EmojiTab />
</div>
</tab-switcher>
</vertical-tab-switcher>
</template>
<script src="./settings_modal_admin_content.js"></script>

View file

@ -1,4 +1,4 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
import DataImportExportTab from './tabs/data_import_export_tab.vue'
import MutesAndBlocksTab from './tabs/mutes_and_blocks_tab.vue'
@ -7,15 +7,20 @@ import FilteringTab from './tabs/filtering_tab.vue'
import SecurityTab from './tabs/security_tab/security_tab.vue'
import ProfileTab from './tabs/profile_tab.vue'
import GeneralTab from './tabs/general_tab.vue'
import PostsTab from './tabs/posts_tab.vue'
import ComposingTab from './tabs/composing_tab.vue'
import ClutterTab from './tabs/clutter_tab.vue'
import LayoutTab from './tabs/layout_tab.vue'
import AppearanceTab from './tabs/appearance_tab.vue'
import VersionTab from './tabs/version_tab.vue'
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
import DeveloperTab from './tabs/developer_tab.vue'
import OldThemeTab from './tabs/old_theme_tab/old_theme_tab.vue'
import StyleTab from './tabs/style_tab/style_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faWrench,
faUser,
faMessage,
faFilter,
faPaintBrush,
faPalette,
@ -23,13 +28,16 @@ import {
faDownload,
faEyeSlash,
faInfo,
faWindowRestore
faWindowRestore,
faCode,
faBroom
} from '@fortawesome/free-solid-svg-icons'
import { useInterfaceStore } from 'src/stores/interface'
library.add(
faWrench,
faUser,
faMessage,
faFilter,
faPaintBrush,
faPalette,
@ -37,12 +45,14 @@ library.add(
faDownload,
faEyeSlash,
faInfo,
faWindowRestore
faWindowRestore,
faBroom,
faCode
)
const SettingsModalContent = {
components: {
TabSwitcher,
VerticalTabSwitcher,
DataImportExportTab,
MutesAndBlocksTab,
@ -51,10 +61,14 @@ const SettingsModalContent = {
SecurityTab,
ProfileTab,
GeneralTab,
PostsTab,
ComposingTab,
ClutterTab,
LayoutTab,
AppearanceTab,
StyleTab,
VersionTab,
ThemeTab
DeveloperTab,
OldThemeTab
},
computed: {
isLoggedIn () {
@ -68,9 +82,12 @@ const SettingsModalContent = {
},
expertLevel () {
return this.$store.state.config.expertLevel
},
isMobileLayout () {
return useInterfaceStore().layoutType === 'mobile'
}
},
data () {
return {
navCollapsed: false,
navHideHeader: false
}
},
methods: {

View file

@ -1,21 +1,6 @@
.settings_tab-switcher {
height: 100%;
h1 {
margin-bottom: 0.5em;
margin-top: 0.5em;
}
h4 {
margin-bottom: 0;
margin-top: 0.25em;
}
h5 {
margin-bottom: 0;
margin-top: 0.25em;
}
.setting-item {
border-bottom: 2px solid var(--border);
margin: 1em 1em 1.4em;

View file

@ -1,10 +1,11 @@
<template>
<tab-switcher
<vertical-tab-switcher
ref="tabSwitcher"
class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:child-collapsed="childCollapsed"
:body-scroll-lock="bodyLock"
:hide-header="navHideHeader"
>
<div
:label="$t('settings.general')"
@ -14,6 +15,32 @@
<GeneralTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.profile_tab')"
icon="user"
data-tab-name="profile"
:full-width="true"
>
<ProfileTab />
</div>
<div
:label="$t('settings.composing')"
icon="pen-alt"
data-tab-name="composing"
:delay-render="true"
>
<ComposingTab />
</div>
<div
:label="$t('settings.posts')"
icon="message"
data-tab-name="posts"
:delay-render="true"
>
<PostsTab />
</div>
<div
:full-width="true"
:label="$t('settings.appearance')"
icon="window-restore"
data-tab-name="appearance"
@ -22,49 +49,26 @@
<AppearanceTab />
</div>
<div
v-if="expertLevel > 0"
:label="$t('settings.style.themes3.editor.title')"
icon="palette"
data-tab-name="style"
:full-width="true"
:label="$t('settings.layout')"
icon="table-columns"
data-tab-name="layout"
:delay-render="true"
>
<StyleTab />
</div>
<div
v-if="expertLevel > 0"
:label="$t('settings.theme_old')"
icon="paint-brush"
data-tab-name="theme"
:delay-render="true"
>
<ThemeTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.profile_tab')"
icon="user"
data-tab-name="profile"
>
<ProfileTab />
<LayoutTab />
</div>
<div
v-if="isLoggedIn"
:full-width="true"
:label="$t('settings.notifications')"
icon="bell"
data-tab-name="notifications"
>
<NotificationsTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.security_tab')"
icon="lock"
data-tab-name="security"
>
<SecurityTab />
</div>
<div
:label="$t('settings.filtering')"
:full-width="true"
icon="filter"
data-tab-name="filtering"
>
@ -73,12 +77,28 @@
<div
v-if="isLoggedIn"
:label="$t('settings.mutes_and_blocks')"
:fullHeight="true"
icon="eye-slash"
data-tab-name="mutesAndBlocks"
:full-width="true"
>
<MutesAndBlocksTab />
</div>
<div
:label="$t('settings.clutter')"
icon="broom"
data-tab-name="clutter"
>
<ClutterTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.security_tab')"
icon="lock"
data-tab-name="security"
:full-width="true"
>
<SecurityTab />
</div>
<div
v-if="isLoggedIn"
:label="$t('settings.data_import_export_tab')"
@ -88,13 +108,34 @@
<DataImportExportTab />
</div>
<div
:label="$t('settings.version.title')"
icon="info"
data-tab-name="version"
v-if="expertLevel > 0"
:label="$t('settings.style.themes3.editor.title')"
icon="palette"
data-tab-name="style"
:delay-render="true"
:full-width="true"
>
<VersionTab />
<StyleTab />
</div>
</tab-switcher>
<div
v-if="expertLevel > 0"
:label="$t('settings.theme_old')"
icon="paint-brush"
data-tab-name="theme"
:delay-render="true"
:full-width="true"
>
<OldThemeTab />
</div>
<div
v-if="expertLevel > 0"
:label="$t('settings.developer')"
icon="code"
data-tab-name="developer"
>
<DeveloperTab />
</div>
</vertical-tab-switcher>
</template>
<script src="./settings_modal_user_content.js"></script>

View file

@ -1,12 +1,12 @@
import VerticalTabSwitcher from 'src/components/tab_switcher/vertical_tab_switcher.jsx'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
import Preview from './theme_tab/theme_preview.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import Preview from './old_theme_tab/theme_preview.vue'
import { newImporter } from 'src/services/export_import/export_import.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
@ -26,11 +26,17 @@ import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
faGlobe,
faDashboard,
faPaintRoller,
faTableColumns
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
faGlobe,
faPaintRoller,
faDashboard,
faTableColumns
)
const AppearanceTab = {
@ -59,11 +65,6 @@ const AppearanceTab = {
],
userPalette: {},
intersectionObserver: null,
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
})),
forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
key: mode,
value: i - 1,
@ -86,9 +87,9 @@ const AppearanceTab = {
FloatSetting,
UnitSetting,
ProfileSettingIndicator,
FontControl,
Preview,
PaletteEditor
PaletteEditor,
VerticalTabSwitcher
},
mounted () {
useInterfaceStore().getThemeData()
@ -253,33 +254,10 @@ const AppearanceTab = {
noIntersectionObserver () {
return !window.IntersectionObserver
},
horizontalUnits () {
return defaultHorizontalUnits
},
fontsOverride () {
return this.$store.getters.mergedConfig.fontsOverride
},
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
instanceWallpaperUsed () {
return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
},
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
customThemeVersion () {
const { themeVersion } = useInterfaceStore()
return themeVersion
@ -295,18 +273,6 @@ const AppearanceTab = {
...SharedComputedObject()
},
methods: {
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
importFile () {
this.fileImporter.importData()
},

View file

@ -1,28 +1,19 @@
.appearance-tab {
h3 {
border: none
}
.palette,
.theme-notice {
padding: 0.5em;
margin: 1em;
}
.setting-item {
padding-bottom: 0;
&.heading {
display: grid;
align-items: baseline;
grid-template-columns: 1fr auto auto auto;
grid-gap: 0.5em;
h2 {
flex: 1 0 auto;
}
}
.theme-name {
font-weight: 900;
padding-bottom: 0.5em;
}
h4 {
margin: 0.5em 0;
}
input[type="file"] {
padding: 0.25em;
@ -71,6 +62,7 @@
border-radius: var(--roundness);
border: 1px solid var(--border);
margin: -0.5em;
margin-top: 0;
}
.palettes {
@ -80,9 +72,9 @@
padding: 0.5em;
width: 100%;
h4 {
margin: 0;
h5 {
grid-column: 1 / span 2;
margin-bottom: 0;
}
}
@ -160,7 +152,7 @@
.theme-preview {
font-size: 1rem; // fix for firefox
width: 19rem;
width: 14rem;
display: flex;
flex-direction: column;
align-items: center;

View file

@ -1,10 +1,15 @@
<template>
<div
class="appearance-tab"
:label="$t('settings.general')"
:label="$t('settings.interface')"
icon="table-columns"
>
<div class="setting-item">
<h2>{{ $t('settings.theme') }}</h2>
<div
class="setting-item"
:label="$t('settings.theme')"
icon="paintbrush"
>
<h3>{{ $t('settings.style.style_section') }}</h3>
<ul
ref="themeList"
class="theme-list"
@ -17,10 +22,10 @@
@click="resetTheming"
>
<preview id="theme-preview-stock" />
<h4 class="theme-name">
<span class="theme-name">
{{ $t('settings.style.stock_theme_used') }}
<span class="alert neutral version">v3</span>
</h4>
</span>
</button>
<button
v-if="isCustomThemeUsed"
@ -28,10 +33,10 @@
class="button-default theme-preview toggled"
>
<preview />
<h4 class="theme-name">
<span class="theme-name">
{{ $t('settings.style.custom_theme_used') }}
<span class="alert neutral version">v2</span>
</h4>
</span>
</button>
<button
v-if="isCustomStyleUsed"
@ -39,10 +44,10 @@
class="button-default theme-preview toggled"
>
<preview />
<h4 class="theme-name">
<span class="theme-name">
{{ $t('settings.style.custom_style_used') }}
<span class="alert neutral version">v3</span>
</h4>
</span>
</button>
<button
v-for="style in availableStyles"
@ -54,10 +59,10 @@
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
>
<preview :id="'theme-preview-' + style.key" />
<h4 class="theme-name">
<span class="theme-name">
{{ style.name }}
<span class="alert neutral version">{{ style.version }}</span>
</h4>
</span>
</button>
</ul>
<div class="import-file-container">
@ -72,14 +77,14 @@
</button>
</div>
<div class="setting-item">
<h2>{{ $t('settings.style.themes3.palette.label') }}</h2>
<h4>{{ $t('settings.style.themes3.palette.label') }}</h4>
<div
v-if="customThemeVersion === 'v3'"
class="palettes-container"
>
<h4 v-if="stylePalettes?.length > 0">
<h5 v-if="stylePalettes?.length > 0">
{{ $t('settings.style.themes3.palette.style') }}
</h4>
</h5>
<div class="palettes">
<button
v-for="p in stylePalettes || []"
@ -103,7 +108,7 @@
/>
</div>
</button>
<h4>{{ $t('settings.style.themes3.palette.bundled') }}</h4>
<h5>{{ $t('settings.style.themes3.palette.bundled') }}</h5>
<button
v-for="p in bundledPalettes"
:key="p.name"
@ -130,9 +135,9 @@
</div>
<div>
<template v-if="customThemeVersion === 'v3'">
<h4 v-if="expertLevel > 0">
<h5 v-if="expertLevel > 0">
{{ $t('settings.style.themes3.palette.user') }}
</h4>
</h5>
<PaletteEditor
v-if="expertLevel > 0"
v-model="userPalette"
@ -150,9 +155,7 @@
</template>
</div>
</div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.background') }}</h2>
<h3>{{ $t('settings.background') }}</h3>
<div class="banner-background-preview">
<img :src="user.background_image">
<button
@ -193,193 +196,11 @@
>
{{ $t('settings.save') }}
</button>
</div>
<div class="setting-item">
<h2>{{ $t('settings.scale_and_layout') }}</h2>
<h3>{{ $t('settings.visual_tweaks') }}</h3>
<div class="alert neutral theme-notice">
{{ $t("settings.style.appearance_tab_note") }}
{{ $t("settings.style.visual_tweaks_section_note") }}
</div>
<ul class="setting-list">
<li>
<UnitSetting
path="textSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 14, 'rem': 1 }"
timed-apply-mode
>
{{ $t('settings.text_size') }}
</UnitSetting>
<div>
<small>
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</small>
</div>
</li>
<li>
<UnitSetting
path="emojiSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 32, 'rem': 2.2 }"
>
{{ $t('settings.emoji_size') }}
</UnitSetting>
<ul
class="setting-list suboptions"
>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
expert="1"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
</ul>
</li>
<li>
<UnitSetting
path="navbarSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
{{ $t('settings.navbar_size') }}
</UnitSetting>
</li>
<h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.interface"
name="ui"
:label="$t('settings.style.fonts.components.interface')"
:fallback="{ family: 'sans-serif' }"
no-inherit="1"
@update:model-value="v => updateFont('interface', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.input"
name="input"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.input')"
@update:model-value="v => updateFont('input', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.post"
name="post"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.post')"
@update:model-value="v => updateFont('post', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.monospace"
name="postCode"
:fallback="{ family: 'monospace' }"
:label="$t('settings.style.fonts.components.monospace')"
@update:model-value="v => updateFont('monospace', v)"
/>
</li>
<h3>{{ $t('settings.columns') }}</h3>
<li>
<UnitSetting
path="panelHeaderSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 52, 'rem': 3.2 }"
timed-apply-mode
>
{{ $t('settings.panel_header_size') }}
</UnitSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="navbarColumnStretch">
{{ $t('settings.navbar_column_stretch') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li v-if="expertLevel > 0">
{{ $t('settings.column_sizes') }}
<div class="column-settings">
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</UnitSetting>
</div>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $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">
<h2>{{ $t('settings.visual_tweaks') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path="modalMobileCenter">
{{ $t('settings.mobile_center_dialog') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="forcedRoundness"
@ -403,22 +224,6 @@
{{ $t('settings.hide_wallpaper') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="forceThemeRecompilation"
:expert="1"
>
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="themeDebug"
:expert="1"
>
{{ $t('settings.theme_debug') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>

View file

@ -0,0 +1,194 @@
import { mapState, mapActions } from 'pinia'
import { mapState as mapVuexState } from 'vuex'
import { v4 as uuidv4 } from 'uuid';
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
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 ClutterTab = {
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
IntegerSetting,
Checkbox,
Select,
HelpIndicator
},
computed: {
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(),
...mapState(
useServerSideStorageStore,
{
muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters),
muteFiltersObject: store => store.prefsStorage.simple.muteFilters
}
),
...mapVuexState({
blockExpirationSupported: state => state.instance.blockExpiration
}),
onMuteDefaultActionLv1: {
get () {
const value = this.$store.state.config.onMuteDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onMuteDefaultAction', value: realValue })
}
},
onBlockDefaultActionLv1: {
get () {
const value = this.$store.state.config.onBlockDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
set (value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
this.$store.dispatch('setOption', { name: 'onBlockDefaultAction', value: realValue })
}
},
muteFiltersDraft () {
return Object.entries(this.muteFiltersDraftObject)
},
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)
const 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 () {
this.$store.dispatch('queueFlushAll')
}
}
}
export default ClutterTab

View file

@ -0,0 +1,104 @@
<template>
<div class="clutter-tab">
<div class="setting-item">
<h3>{{ $t('settings.interface') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
path="alwaysShowSubjectInput"
expert="1"
>
{{ $t('settings.subject_input_always_show') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="minimalScopesMode"
expert="1"
>
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</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>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<ul class="setting-list">
<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="userCardHidePersonalMarks"
expert="1"
>
{{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting>
</li>
<li v-if="instanceShoutboxPresent">
<BooleanSetting
path="hideShoutbox"
expert="1"
>
{{ $t('settings.hide_shoutbox') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./clutter_tab.js"></script>

View file

@ -0,0 +1,178 @@
import { mapState } from 'vuex'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import Select from 'src/components/select/select.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe,
faMessage,
faPenAlt,
faDatabase,
faSliders
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe,
faMessage,
faPenAlt,
faDatabase,
faSliders
)
const ComposingTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
})),
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
absoluteTime12hOptions: ['24h', '12h'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.absolute_time_format_12h_${mode}`)
})),
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
emailLanguage: this.$store.state.users.currentUser.language || ['']
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting,
InterfaceLanguageSwitcher,
ProfileSettingIndicator,
ScopeSelector,
Select,
FontControl
},
computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
postContentOptions () {
return this.postFormats.map(format => ({
key: format,
value: format,
label: this.$t(`post_status.content_type["${format}"]`)
}))
},
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
...SharedComputedObject(),
...mapState({
blockExpirationSupported: state => state.instance.blockExpiration,
})
},
methods: {
changeDefaultScope (value) {
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
},
clearCache (key) {
clearCache(key)
.then(() => {
this.$store.dispatch('settingsSaved', { success: true })
})
.catch(error => {
this.$store.dispatch('settingsSaved', { error })
})
},
tooSmall () {
this.$emit('tooSmall')
},
tooBig () {
this.$emit('tooBig')
},
getNavMode () {
return this.$refs.tabSwitcher.getNavMode()
},
clearAssetCache () {
this.clearCache(cacheKey)
},
clearEmojiCache () {
this.clearCache(emojiCacheKey)
},
updateProfile () {
const params = {
language: localeService.internalToBackendLocaleMulti(this.emailLanguage)
}
this.$store.state.api.backendInteractor
.updateProfile({ params })
.then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
})
},
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
}
}
export default ComposingTab

View file

@ -0,0 +1,111 @@
<template>
<div :label="$t('settings.posts')">
<div class="setting-item">
<h3>{{ $t('settings.general') }}</h3>
<ul class="setting-list">
<li>
<label for="default-vis">
{{ $t('settings.default_vis') }}
{{ ' ' }}
<ScopeSelector
class="scope-selector"
:show-all="true"
:user-default="$store.state.profileConfig.defaultScope"
:initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope"
:unstyled="false"uns
/>
<ProfileSettingIndicator :is-profile="true" />
</label>
</li>
<li>
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
</li>
<li v-if="postFormats.length > 0">
<ChoiceSetting
id="postContentType"
path="postContentType"
:options="postContentOptions"
>
{{ $t('settings.default_post_status_content_type') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting path="padEmoji">
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autocompleteSelect"
expert="1"
>
{{ $t('settings.autocomplete_select_first') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autoSaveDraft"
>
{{ $t('settings.auto_save_draft') }}
</BooleanSetting>
</li>
<li v-if="!mergedConfig.autoSaveDraft">
<ChoiceSetting
id="unsavedPostAction"
path="unsavedPostAction"
:options="unsavedPostActionOptions"
>
{{ $t('settings.unsaved_post_action') }}
</ChoiceSetting>
</li>
</ul>
<h3>{{ $t('settings.replies') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
path="scopeCopy"
>
{{ $t('settings.scope_copy') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="subjectLineBehavior"
path="subjectLineBehavior"
:options="subjectLineOptions"
>
{{ $t('settings.subject_line_behavior') }}
</ChoiceSetting>
</li>
</ul>
<h3 v-if="expertLevel > 0">{{ $t('settings.attachments') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting
path="imageCompression"
expert="1"
>
{{ $t('settings.image_compression') }}
</BooleanSetting>
</li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="alwaysUseJpeg"
expert="1"
parent-path="imageCompression"
>
{{ $t('settings.always_use_jpeg') }}
</BooleanSetting>
</li>
</ul>
</ul>
</div>
</div>
</template>
<script src="./composing_tab.js"></script>

View file

@ -0,0 +1,11 @@
.data-import-export-tab {
h3 {
margin-right: -2em;
}
.importer-exporter {
display: inline-flex;
flex-direction: column;
gap: 0.5em;
}
}

View file

@ -1,60 +1,61 @@
<template>
<div
class="data-import-export-tab"
:label="$t('settings.data_import_export_tab')"
>
<div class="setting-item">
<h2>{{ $t('settings.follow_import') }}</h2>
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
<Importer
:submit-handler="importFollows"
:success-message="$t('settings.follows_imported')"
:error-message="$t('settings.follow_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.follow_export') }}</h2>
<Exporter
:get-content="getFollowsContent"
filename="friends.csv"
:export-button-label="$t('settings.follow_export_button')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.block_import') }}</h2>
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
<Importer
:submit-handler="importBlocks"
:success-message="$t('settings.blocks_imported')"
:error-message="$t('settings.block_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.block_export') }}</h2>
<Exporter
:get-content="getBlocksContent"
filename="blocks.csv"
:export-button-label="$t('settings.block_export_button')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.mute_import') }}</h2>
<p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
<Importer
:submit-handler="importMutes"
:success-message="$t('settings.mutes_imported')"
:error-message="$t('settings.mute_import_error')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.mute_export') }}</h2>
<Exporter
:get-content="getMutesContent"
filename="mutes.csv"
:export-button-label="$t('settings.mute_export_button')"
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.account_backup') }}</h2>
<h3>{{ $t('settings.import_export.title') }}</h3>
<ul class="setting-list">
<li>
<h4>{{ $t('settings.import_export.follows') }}</h4>
<p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
<div class="importer-exporter">
<Importer
:submit-handler="importFollows"
:success-message="$t('settings.follows_imported')"
:error-message="$t('settings.follow_import_error')"
/>
<Exporter
:get-content="getFollowsContent"
filename="friends.csv"
:export-button-label="$t('settings.follow_export_button')"
/>
</div>
</li>
<li>
<h4>{{ $t('settings.import_export.mutes') }}</h4>
<p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p>
<div class="importer-exporter">
<Importer
:submit-handler="importMutes"
:success-message="$t('settings.mutes_imported')"
:error-message="$t('settings.mute_import_error')"
/>
<Exporter
:get-content="getMutesContent"
filename="friends.csv"
:export-button-label="$t('settings.mute_export_button')"
/>
</div>
</li>
<li>
<h4>{{ $t('settings.import_export.blocks') }}</h4>
<p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
<div class="importer-exporter">
<Importer
:submit-handler="importBlocks"
:success-message="$t('settings.blocks_imported')"
:error-message="$t('settings.block_import_error')"
/>
<Exporter
:get-content="getBlocksContent"
filename="friends.csv"
:export-button-label="$t('settings.block_export_button')"
/>
</div>
</li>
</ul>
<h3>{{ $t('settings.account_backup') }}</h3>
<p>{{ $t('settings.account_backup_description') }}</p>
<table>
<thead>
@ -128,4 +129,4 @@
</template>
<script src="./data_import_export_tab.js"></script>
<!-- <style lang="scss" src="./profile.scss"></style> -->
<style lang="scss" src="./data_import_export_tab.scss"></style>

View file

@ -1,3 +1,9 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const VersionTab = {
@ -9,10 +15,14 @@ const VersionTab = {
frontendVersion: instance.frontendVersion
}
},
components: {
BooleanSetting
},
computed: {
frontendVersionLink () {
return pleromaFeCommitUrl + this.frontendVersion
}
},
...SharedComputedObject(),
}
}

View file

@ -0,0 +1,10 @@
.developer-tab {
dt {
font-weight: 900;
}
dd {
margin-top: 0.5em;
margin-bottom: 1em;
}
}

View file

@ -0,0 +1,72 @@
<template>
<div
:label="$t('settings.developer')"
class="developer-tab"
>
<div class="setting-item">
<h3>{{ $t('settings.version.title')}}</h3>
<dl class="setting-list">
<dt>{{ $t('settings.version.backend_version') }}</dt>
<dd>
<a
:href="backendRepository"
target="_blank"
>
{{ backendVersion }}
</a>
</dd>
<dt>{{ $t('settings.version.frontend_version') }}</dt>
<dd>
<a
:href="frontendVersionLink"
target="_blank"
>
{{ frontendVersion }}
</a>
</dd>
</dl>
<h3>{{ $t('settings.debug')}}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="virtualScrolling">
{{ $t('settings.virtual_scrolling') }}
</BooleanSetting>
</li>
<li>
<button
class="btn button-default"
@click="clearAssetCache"
>
{{ $t('settings.clear_asset_cache') }}
</button>
</li>
<li>
<button
class="btn button-default"
@click="clearEmojiCache"
>
{{ $t('settings.clear_emoji_cache') }}
</button>
</li>
<li>
<BooleanSetting
path="themeDebug"
:expert="1"
>
{{ $t('settings.theme_debug') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="forceThemeRecompilation"
:expert="1"
>
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./developer_tab.js" />
<style lang="scss" src="./developer_tab.scss"></style>

View file

@ -91,6 +91,7 @@ const FilteringTab = {
HelpIndicator
},
computed: {
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(),
...mapState(
useServerSideStorageStore,

View file

@ -1,10 +1,7 @@
<template>
<div
:label="$t('settings.filtering')"
class="filtering-tab"
>
<div class="filtering-tab">
<div class="setting-item">
<h2>{{ $t('settings.filter.clutter') }}</h2>
<h3>{{ $t('settings.filter.mute_filter') }}</h3>
<ul class="setting-list">
<li>
<ChoiceSetting
@ -16,70 +13,6 @@
{{ $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>
{{ $t('user_card.default_mute_expiration') }}
<Select

View file

@ -2,129 +2,57 @@ import { mapState } from 'vuex'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import Select from 'src/components/select/select.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
import { clearCache, cacheKey, emojiCacheKey } from 'src/services/sw/sw.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const GeneralTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
})),
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
absoluteTime12hOptions: ['24h', '12h'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.absolute_time_format_12h_${mode}`)
})),
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
emailLanguage: this.$store.state.users.currentUser.language || ['']
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
UnitSetting,
FloatSetting,
FontControl,
InterfaceLanguageSwitcher,
ProfileSettingIndicator,
ScopeSelector,
Select
ProfileSettingIndicator
},
computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
postContentOptions () {
return this.postFormats.map(format => ({
key: format,
value: format,
label: this.$t(`post_status.content_type["${format}"]`)
}))
},
language: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(),
...mapState({
blockExpirationSupported: state => state.instance.blockExpiration,
})
},
methods: {
changeDefaultScope (value) {
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
},
clearCache (key) {
clearCache(key)
.then(() => {
this.$store.dispatch('settingsSaved', { success: true })
})
.catch(error => {
this.$store.dispatch('settingsSaved', { error })
})
},
clearAssetCache () {
this.clearCache(cacheKey)
},
clearEmojiCache () {
this.clearCache(emojiCacheKey)
},
updateProfile () {
const params = {
language: localeService.internalToBackendLocaleMulti(this.emailLanguage)
@ -137,6 +65,18 @@ const GeneralTab = {
this.$store.commit('setCurrentUser', user)
})
},
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
}
}

View file

@ -1,7 +1,7 @@
<template>
<div :label="$t('settings.general')">
<div>
<div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2>
<h3>{{ $t('settings.format_and_language') }}</h3>
<ul class="setting-list">
<li>
<interface-language-switcher
@ -20,16 +20,98 @@
{{ $t('settings.email_language') }}
</interface-language-switcher>
</li>
<li v-if="instanceSpecificPanelPresent">
<BooleanSetting path="hideISP">
{{ $t('settings.hide_isp') }}
<li>
<BooleanSetting path="useAbsoluteTimeFormat">
{{ $t('settings.absolute_time_format') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
</BooleanSetting>
<ChoiceSetting
id="absoluteTime12h"
path="absoluteTime12h"
:options="absoluteTime12hOptions"
>
{{ $t('settings.absolute_time_format_12h') }}
</ChoiceSetting>
</li>
</ul>
<h3>{{ $t('settings.scale_and_font') }}</h3>
<ul class="setting-list">
<li>
<UnitSetting
path="textSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 14, 'rem': 1 }"
timed-apply-mode
>
{{ $t('settings.text_size') }}
</UnitSetting>
<div>
<small>
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</small>
</div>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.interface"
name="ui"
:label="$t('settings.style.fonts.components_inline.interface')"
:fallback="{ family: 'sans-serif' }"
no-inherit="1"
@update:model-value="v => updateFont('interface', v)"
/>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.input"
name="input"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components_inline.input')"
@update:model-value="v => updateFont('input', v)"
/>
</li>
<li>
<UnitSetting
path="emojiSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 32, 'rem': 2.2 }"
>
{{ $t('settings.emoji_size') }}
</UnitSetting>
<ul
class="setting-list suboptions"
>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
</ul>
</li>
</ul>
<h3>{{ $t('settings.timelines') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="streaming">
{{ $t('settings.streaming') }}
@ -53,72 +135,9 @@
{{ $t('settings.useStreamingApi') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="virtualScrolling"
expert="1"
>
{{ $t('settings.virtual_scrolling') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="userPopoverAvatarAction"
path="userPopoverAvatarAction"
:options="userPopoverAvatarActionOptions"
expert="1"
>
{{ $t('settings.user_popover_avatar_action') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="userPopoverOverlay"
expert="1"
>
{{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="userCardLeftJustify"
expert="1"
>
{{ $t('settings.user_card_left_justify') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="userCardHidePersonalMarks"
expert="1"
>
{{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"
expert="1"
>
{{ $t('settings.always_show_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autohideFloatingPostButton"
expert="1"
>
{{ $t('settings.autohide_floating_post_button') }}
</BooleanSetting>
</li>
<li v-if="instanceShoutboxPresent">
<BooleanSetting
path="hideShoutbox"
expert="1"
>
{{ $t('settings.hide_shoutbox') }}
</BooleanSetting>
</li>
</ul>
<h3 v-if="expertLevel > 0">{{ $t('settings.confirmations') }}</h3>
<ul v-if="expertLevel > 0" class="setting-list">
<li class="select-multiple">
<span class="label">{{ $t('settings.confirm_dialogs') }}</span>
<ul class="option-list">
@ -179,383 +198,6 @@
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.post_look_feel') }}</h2>
<ul class="setting-list">
<li>
<ChoiceSetting
id="conversationDisplay"
path="conversationDisplay"
:options="conversationDisplayOptions"
>
{{ $t('settings.conversation_display') }}
</ChoiceSetting>
</li>
<ul
v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
<BooleanSetting path="conversationTreeAdvanced">
{{ $t('settings.tree_advanced') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="conversationTreeFadeAncestors"
:expert="1"
>
{{ $t('settings.tree_fade_ancestors') }}
</BooleanSetting>
</li>
<li>
<IntegerSetting
path="maxDepthInThread"
:min="3"
:expert="1"
>
{{ $t('settings.max_depth_in_thread') }}
</IntegerSetting>
</li>
<li>
<ChoiceSetting
id="conversationOtherRepliesButton"
path="conversationOtherRepliesButton"
:options="conversationOtherRepliesButtonOptions"
:expert="1"
>
{{ $t('settings.conversation_other_replies_button') }}
</ChoiceSetting>
</li>
</ul>
<li>
<BooleanSetting path="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="emojiReactionsOnTimeline"
expert="1"
>
{{ $t('settings.emoji_reactions_on_timeline') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
v-if="user"
source="profile"
path="stripRichContent"
expert="1"
>
{{ $t('settings.no_rich_text_description') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useAbsoluteTimeFormat"
expert="1"
>
{{ $t('settings.absolute_time_format') }}
</BooleanSetting>
</li>
<ul
v-if="mergedConfig.useAbsoluteTimeFormat"
class="setting-list suboptions"
>
<li>
<UnitSetting
path="absoluteTimeFormatMinAge"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('settings.absolute_time_format_min_age') }}
</UnitSetting>
</li>
<li>
<ChoiceSetting
id="absoluteTime12h"
path="absoluteTime12h"
:options="absoluteTime12hOptions"
:expert="1"
>
{{ $t('settings.absolute_time_format_12h') }}
</ChoiceSetting>
</li>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<li>
<BooleanSetting
path="imageCompression"
expert="1"
>
{{ $t('settings.image_compression') }}
</BooleanSetting>
</li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="alwaysUseJpeg"
expert="1"
parent-path="imageCompression"
>
{{ $t('settings.always_use_jpeg') }}
</BooleanSetting>
</li>
</ul>
<li>
<BooleanSetting
path="useContainFit"
expert="1"
>
{{ $t('settings.use_contain_fit') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideNsfw">
{{ $t('settings.nsfw_clickthrough') }}
</BooleanSetting>
</li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="preloadImage"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.preload_images') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useOneClickNsfw"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting>
</li>
</ul>
<li>
<BooleanSetting
path="loopVideo"
expert="1"
>
{{ $t('settings.loop_video') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="loopVideoSilentOnly"
expert="1"
parent-path="loopVideo"
:disabled="!loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</BooleanSetting>
<div
v-if="!loopSilentAvailable"
class="unavailable"
>
<FAIcon icon="globe" />! {{ $t('settings.limited_availability') }}
</div>
</li>
</ul>
</li>
<li>
<BooleanSetting
path="playVideosInModal"
expert="1"
>
{{ $t('settings.play_videos_in_modal') }}
</BooleanSetting>
</li>
<h3>{{ $t('settings.mention_links') }}</h3>
<li>
<ChoiceSetting
id="mentionLinkDisplay"
path="mentionLinkDisplay"
:options="mentionLinkDisplayOptions"
>
{{ $t('settings.mention_link_display') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="mentionLinkShowTooltip"
expert="1"
>
{{ $t('settings.mention_link_use_tooltip') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="mentionLinkShowAvatar">
{{ $t('settings.mention_link_show_avatar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="mentionLinkFadeDomain"
expert="1"
>
{{ $t('settings.mention_link_fade_domain') }}
</BooleanSetting>
</li>
<li v-if="user">
<BooleanSetting
path="mentionLinkBoldenYou"
expert="1"
>
{{ $t('settings.mention_link_bolden_you') }}
</BooleanSetting>
</li>
<h3 v-if="expertLevel > 0">
{{ $t('settings.fun') }}
</h3>
<li>
<BooleanSetting
path="greentext"
expert="1"
>
{{ $t('settings.greentext') }}
</BooleanSetting>
</li>
<li v-if="user">
<BooleanSetting
path="mentionLinkShowYous"
expert="1"
>
{{ $t('settings.show_yous') }}
</BooleanSetting>
</li>
</ul>
</div>
<div
v-if="user"
class="setting-item"
>
<h2>{{ $t('settings.composing') }}</h2>
<ul class="setting-list">
<li>
<label for="default-vis">
{{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
<ScopeSelector
class="scope-selector"
:show-all="true"
:user-default="$store.state.profileConfig.defaultScope"
:initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope"
/>
</label>
</li>
<li>
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="scopeCopy"
expert="1"
>
{{ $t('settings.scope_copy') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowSubjectInput"
expert="1"
>
{{ $t('settings.subject_input_always_show') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="subjectLineBehavior"
path="subjectLineBehavior"
:options="subjectLineOptions"
expert="1"
>
{{ $t('settings.subject_line_behavior') }}
</ChoiceSetting>
</li>
<li v-if="postFormats.length > 0">
<ChoiceSetting
id="postContentType"
path="postContentType"
:options="postContentOptions"
>
{{ $t('settings.post_status_content_type') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="minimalScopesMode"
expert="1"
>
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="padEmoji"
expert="1"
>
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autocompleteSelect"
expert="1"
>
{{ $t('settings.autocomplete_select_first') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autoSaveDraft"
>
{{ $t('settings.auto_save_draft') }}
</BooleanSetting>
</li>
<li v-if="!mergedConfig.autoSaveDraft">
<ChoiceSetting
id="unsavedPostAction"
path="unsavedPostAction"
:options="unsavedPostActionOptions"
>
{{ $t('settings.unsaved_post_action') }}
</ChoiceSetting>
</li>
</ul>
</div>
<div
class="setting-item"
>
<h2>{{ $t('settings.cache') }}</h2>
<ul class="setting-list">
<li>
<button
class="btn button-default"
@click="clearAssetCache"
>
{{ $t('settings.clear_asset_cache') }}
</button>
</li>
<li>
<button
class="btn button-default"
@click="clearEmojiCache"
>
{{ $t('settings.clear_emoji_cache') }}
</button>
</li>
</ul>
</div>
</div>
</template>

View file

@ -0,0 +1,49 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const GeneralTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
}))
}
},
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
ProfileSettingIndicator
},
computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
...SharedComputedObject(),
}
}
export default GeneralTab

View file

@ -0,0 +1,130 @@
<template>
<div :label="$t('settings.layout')">
<div class="setting-item">
<h3>{{ $t('settings.general') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="modalMobileCenter">
{{ $t('settings.mobile_center_dialog') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"
expert="1"
>
{{ $t('settings.always_show_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autohideFloatingPostButton"
expert="1"
>
{{ $t('settings.autohide_floating_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="userPopoverOverlay"
expert="1"
>
{{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="userCardLeftJustify"
expert="1"
>
{{ $t('settings.user_card_left_justify') }}
</BooleanSetting>
</li>
<li>
<UnitSetting
path="themeEditorMinWidth"
:units="['px', 'rem']"
expert="1"
>
{{ $t('settings.theme_editor_min_width') }}
</UnitSetting>
</li>
</ul>
<h3>{{ $t('settings.columns') }}</h3>
<ul class="setting-list">
<li>
<UnitSetting
path="navbarSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
{{ $t('settings.navbar_size') }}
</UnitSetting>
</li>
<li v-if="instanceSpecificPanelPresent">
<BooleanSetting path="hideISP">
{{ $t('settings.hide_isp') }}
</BooleanSetting>
</li>
<li>
<UnitSetting
path="panelHeaderSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 52, 'rem': 3.2 }"
timed-apply-mode
>
{{ $t('settings.panel_header_size') }}
</UnitSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="navbarColumnStretch">
{{ $t('settings.navbar_column_stretch') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li v-if="expertLevel > 0">
{{ $t('settings.column_sizes') }}
<div class="column-settings">
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</UnitSetting>
</div>
</li>
</ul>
</div>
</div>
</template>
<script src="./layout_tab.js"></script>

View file

@ -1,4 +1,4 @@
.theme-tab {
.old-theme-tab {
min-width: var(--themeEditorMinWidth, fit-content);
.deprecation-warning {

View file

@ -1,5 +1,5 @@
<template>
<div class="theme-tab">
<div class="old-theme-tab">
<div class="alert warning deprecation-warning">
{{ $t("settings.style.themes2_outdated") }}
</div>
@ -1020,6 +1020,6 @@
</div>
</template>
<script src="./theme_tab.js"></script>
<script src="./old_theme_tab.js"></script>
<style src="./theme_tab.scss" lang="scss"></style>
<style src="./old_theme_tab.scss" lang="scss"></style>

View file

@ -0,0 +1,76 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const GeneralTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.mention_link_display_${mode}`)
})),
userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
}
},
components: {
BooleanSetting,
ChoiceSetting,
FontControl,
ProfileSettingIndicator
},
computed: {
...SharedComputedObject(),
},
methods: {
updateFont (key, value) {
this.$store.dispatch('setOption', {
name: 'theme3hacks',
value: {
...this.mergedConfig.theme3hacks,
fonts: {
...this.mergedConfig.theme3hacks.fonts,
[key]: value
}
}
})
},
}
}
export default GeneralTab

View file

@ -0,0 +1,5 @@
.posts-tab {
.greentext {
color: var(--funtextGreentext);
}
}

View file

@ -0,0 +1,255 @@
<template>
<div class="posts-tab">
<div class="setting-item">
<h3>{{ $t('settings.posts_appearance') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="conversationDisplay"
path="conversationDisplay"
:options="conversationDisplayOptions"
>
{{ $t('settings.conversation_display') }}
</ChoiceSetting>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.post"
name="post"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.post')"
@update:model-value="v => updateFont('post', v)"
/>
</li>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.monospace"
name="postCode"
:fallback="{ family: 'monospace' }"
:label="$t('settings.style.fonts.components.monospace')"
@update:model-value="v => updateFont('monospace', v)"
/>
</li>
<ul
v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
<BooleanSetting path="conversationTreeAdvanced">
{{ $t('settings.tree_advanced') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="conversationTreeFadeAncestors"
:expert="1"
>
{{ $t('settings.tree_fade_ancestors') }}
</BooleanSetting>
</li>
<li>
<IntegerSetting
path="maxDepthInThread"
:min="3"
:expert="1"
>
{{ $t('settings.max_depth_in_thread') }}
</IntegerSetting>
</li>
<li>
<ChoiceSetting
id="conversationOtherRepliesButton"
path="conversationOtherRepliesButton"
:options="conversationOtherRepliesButtonOptions"
:expert="1"
>
{{ $t('settings.conversation_other_replies_button') }}
</ChoiceSetting>
</li>
</ul>
<li>
<BooleanSetting path="greentext">
<i18n-t
keypath="settings.plaintext_quotes"
tag="span"
>
<span class="greentext">
{{ $t('settings.greentext_quotes') }}
</span>
</i18n-t>
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="emojiReactionsOnTimeline"
expert="1"
>
{{ $t('settings.emoji_reactions_on_timeline') }}
</BooleanSetting>
</li>
</ul>
<h3>{{ $t('settings.mention_links') }}</h3>
<ul class="setting-list">
<li>
<ChoiceSetting
id="mentionLinkDisplay"
path="mentionLinkDisplay"
:options="mentionLinkDisplayOptions"
>
{{ $t('settings.mention_link_display') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="mentionLinkShowTooltip"
expert="1"
>
{{ $t('settings.mention_link_use_tooltip') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="mentionLinkShowAvatar">
{{ $t('settings.mention_link_show_avatar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
v-if="mergedConfig.mentionLinkDisplay !== 'short'"
path="mentionLinkFadeDomain"
>
{{ $t('settings.mention_link_fade_domain') }}
</BooleanSetting>
</li>
<li v-if="user">
<BooleanSetting
path="mentionLinkBoldenYou"
expert="1"
>
{{ $t('settings.mention_link_bolden_you') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
v-if="user"
source="profile"
path="stripRichContent"
expert="1"
>
{{ $t('settings.no_rich_text_description') }}
</BooleanSetting>
</li>
<ul
v-if="mergedConfig.useAbsoluteTimeFormat"
class="setting-list suboptions"
>
<li>
<UnitSetting
path="absoluteTimeFormatMinAge"
unit-set="time"
:units="['s', 'm', 'h', 'd']"
:min="0"
>
{{ $t('settings.absolute_time_format_min_age') }}
</UnitSetting>
</li>
</ul>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideNsfw">
{{ $t('settings.nsfw_clickthrough') }}
</BooleanSetting>
</li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="preloadImage"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.preload_images') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useOneClickNsfw"
expert="1"
parent-path="hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting>
</li>
</ul>
<li>
<BooleanSetting
path="loopVideo"
expert="1"
>
{{ $t('settings.loop_video') }}
</BooleanSetting>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="loopVideoSilentOnly"
expert="1"
parent-path="loopVideo"
:disabled="!loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</BooleanSetting>
<div
v-if="!loopSilentAvailable"
class="unavailable"
>
<FAIcon icon="globe" />! {{ $t('settings.limited_availability') }}
</div>
</li>
</ul>
</li>
<li>
<BooleanSetting
path="playVideosInModal"
expert="1"
>
{{ $t('settings.play_videos_in_modal') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useContainFit"
expert="1"
>
{{ $t('settings.use_contain_fit') }}
</BooleanSetting>
</li>
</ul>
<h3 v-if="expertLevel > 0">
{{ $t('settings.fun') }}
</h3>
<ul class="setting-list">
<li v-if="user">
<BooleanSetting
path="mentionLinkShowYous"
expert="1"
>
{{ $t('settings.show_yous') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./posts_tab.js"></script>
<style src="./posts_tab.scss"></style>

View file

@ -1,7 +1,6 @@
<template>
<div class="profile-tab">
<div class="setting-item profile-edit">
<h2>{{ $t('settings.account_profile_edit') }}</h2>
<UserCard
:user-id="user.id"
:editable="true"
@ -9,7 +8,7 @@
/>
</div>
<div class="setting-item">
<h2>{{ $t('settings.account_privacy') }}</h2>
<h3>{{ $t('settings.account_privacy') }}</h3>
<ul class="setting-list">
<li>
<Checkbox v-model="locked">

View file

@ -15,7 +15,7 @@ import RoundnessInput from 'src/components/roundness_input/roundness_input.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Tooltip from 'src/components/tooltip/tooltip.vue'
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import Preview from '../theme_tab/theme_preview.vue'
import Preview from '../old_theme_tab/theme_preview.vue'
import VirtualDirectivesTab from './virtual_directives_tab.vue'

View file

@ -1,31 +0,0 @@
<template>
<div :label="$t('settings.version.title')">
<div class="setting-item">
<ul class="setting-list">
<li>
<p>{{ $t('settings.version.backend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="backendRepository"
target="_blank"
>{{ backendVersion }}</a>
</li>
</ul>
</li>
<li>
<p>{{ $t('settings.version.frontend_version') }}</p>
<ul class="option-list">
<li>
<a
:href="frontendVersionLink"
target="_blank"
>{{ frontendVersion }}</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./version_tab.js" />

View file

@ -452,7 +452,7 @@ const Status = {
},
scrobblePresent () {
if (this.mergedConfig.hideScrobbles) return false
if (!this.status.user.latestScrobble) return false
if (!this.status.user?.latestScrobble) return false
const value = this.mergedConfig.hideScrobblesAfter.match(/\d+/gs)[0]
const unit = this.mergedConfig.hideScrobblesAfter.match(/\D+/gs)[0]
let multiplier = 60 * 1000 // minutes is smallest unit
@ -474,7 +474,7 @@ const Status = {
return this.status.user.latestScrobble.artist
},
scrobble () {
return this.status.user.latestScrobble
return this.status.user?.latestScrobble
}
},
methods: {

View file

@ -108,6 +108,10 @@
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 0;
&.unknown {
min-width: 8em;
}
}
.heading-left {

View file

@ -109,6 +109,7 @@
class="left-side"
>
<a
v-if="status.user?.name"
:href="$router.resolve(userProfileLink).href"
@click.prevent
>
@ -120,10 +121,17 @@
class="post-avatar"
:show-actor-type-indicator="showActorTypeIndicator"
:compact="compact"
:user="status.user"
:user="status?.user"
/>
</UserPopover>
</a>
<UserAvatar
v-else
:user="status?.user"
class="post-avatar"
:compact="compact"
:title="$t('status.unknown_user_info')"
/>
</div>
<div class="right-side">
<div
@ -133,29 +141,30 @@
<div class="heading-name-row">
<div class="heading-left">
<h4
v-if="status.user.name_html"
class="status-username"
:title="status.user.name"
:title="status.user?.name ?? $t('status.unknown_user_info')"
>
<RichContent
:html="status.user.name"
:emoji="status.user.emoji"
:is-local="status.user.is_local"
/>
<user-link
v-if="status.user?.name"
class="account-name"
:title="status.user?.screen_name_ui"
:user="status?.user"
:at="false"
>
<RichContent
:html="status.user.name"
:emoji="status.user.emoji"
:is-local="status.user.is_local"
/>
<span>{{ status.user.name }}</span>
</user-link>
<span
v-else
class="account-name unknown"
>
{{ $t('status.unknown_user') }}
</span>
</h4>
<h4
v-else
class="status-username"
:title="status.user.name"
>
{{ status.user.name }}
</h4>
<user-link
class="account-name"
:title="status.user.screen_name_ui"
:user="status.user"
:at="false"
/>
<img
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"

View file

@ -31,11 +31,6 @@ export default {
type: Boolean,
default: false
},
sideTabBar: {
required: false,
type: Boolean,
default: false
},
bodyScrollLock: {
required: false,
type: Boolean,
@ -157,29 +152,28 @@ export default {
return (
<div class={classes}>
{
this.sideTabBar
? <h1 class="mobile-label">{props.label}</h1>
: ''
}
{renderSlot}
</div>
)
})
return (
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
<div
class="tab-switcher top-tabs"
ref="root"
>
<div
class="tabs"
role="tablist"
ref="nav"
>
{tabs}
</div>
<div
ref="contents"
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
ref="content"
>
{contents}
</div>

View file

@ -52,104 +52,6 @@
}
}
&.side-tabs {
flex-direction: row;
@media all and (width <= 800px) {
overflow-x: auto;
}
> .contents {
flex: 1 1 auto;
}
> .tabs {
flex: 0 0 auto;
overflow: hidden auto;
flex-direction: column;
&::after,
&::before {
flex-shrink: 0;
flex-basis: 0.5em;
content: "";
border-right: 1px solid;
border-right-color: var(--border);
}
&::after {
flex-grow: 1;
}
&::before {
flex-grow: 0;
}
.tab-wrapper {
min-width: 10em;
display: flex;
flex-direction: column;
@media all and (width <= 800px) {
min-width: 4em;
}
&:not(.active)::after {
top: 0;
right: 0;
bottom: 0;
border-right: 1px solid;
border-right-color: var(--border);
}
&::before {
flex: 0 0 6px;
content: "";
border-right: 1px solid;
border-right-color: var(--border);
}
&:last-child .tab {
margin-bottom: 0;
}
}
.tab {
flex: 1;
box-sizing: content-box;
max-width: 9em;
min-width: 1px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding-left: 1em;
padding-right: calc(1em + 200px);
margin-right: -200px;
margin-left: 1em;
&:not(.active) {
margin-top: 0;
margin-left: 1.5em;
}
@media all and (width <= 800px) {
padding-left: 0.25em;
padding-right: calc(0.25em + 200px);
margin-right: calc(0.25em - 200px);
margin-left: 0.25em;
&:not(.active) {
margin-top: 0;
margin-left: 0.5em;
}
.text {
display: none;
}
}
}
}
}
.contents {
flex: 1 0 auto;
min-height: 0;

View file

@ -0,0 +1,207 @@
// eslint-disable-next-line no-unused
import { h, Fragment } from 'vue'
import { mapState } from 'pinia'
import { throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './vertical_tab_switcher.scss'
import { useInterfaceStore } from 'src/stores/interface'
const findFirstUsable = (slots) => slots.findIndex(_ => _.props)
export default {
name: 'VerticalTabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false
},
onSwitch: {
required: false,
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String,
default: undefined
},
bodyScrollLock: {
required: false,
type: Boolean,
default: false
},
parentCollapsed: {
required: false,
type: Boolean,
default: null
}
},
data () {
return {
active: findFirstUsable(this.slots()),
resizeHandler: null,
navSide: 'tabs'
}
},
computed: {
activeIndex () {
// In case of controlled component
if (this.activeTab) {
return this.slots().findIndex(slot => slot && slot.props && this.activeTab === slot.props.key)
} else {
return this.active
}
},
isActive () {
return tabName => {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
...mapPiniaState(useInterfaceStore, {
mobileLayout: store => store.layoutType === 'mobile'
}),
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
if (!currentSlot.props) {
this.active = findFirstUsable(this.slots())
}
},
methods: {
clickTab (index) {
return (e) => {
e.preventDefault()
this.setTab(index)
}
},
setTab (index) {
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
this.changeNavSide('content')
},
changeNavSide (side) {
if (this.navSide !== side) {
this.navSide = side
}
},
// DO NOT put it to computed, it doesn't work (caching?)
slots () {
if (this.$slots.default()[0].type === Fragment) {
return this.$slots.default()[0].children
}
return this.$slots.default()
}
},
render () {
const tabs = this.slots()
.map((slot, index) => {
const props = slot.props
if (!props) return
const classesTab = ['vertical-tab', 'menu-item']
if (this.activeIndex === index && useInterfaceStore().layoutType !== 'mobile') {
classesTab.push('-active')
}
return (
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
title={props.label}
>
{!props.icon ? '' : (<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon}/>)}
<span class="text">
{props.label}
</span>
</button>
)
})
const contents = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const active = this.activeIndex === index
const classes = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
if (props.fullHeight) {
classes.push('-full-height')
}
let delayRender = slot.props['delay-render']
if (delayRender && active) {
slot.props['delay-render'] = false
delayRender = false
}
const renderSlot = (!delayRender && (!this.renderOnlyFocused || active))
? slot
: ''
const headerClasses = ['tab-content-label']
const header = (
<h2 class={headerClasses}>
<button
type="button"
onClick={() => this.changeNavSide('tabs')}
class="button-unstyled"
>
<FAIcon
size="lg"
class="back-button-icon"
icon="chevron-left"
/>
</button>
{props.label}
</h2>
)
return (
<div class={classes} >
<div class="tab-mobile-header">
{header}
</div>
<div class="tab-slot-wrapper">
<div class={ ['tab-content', props['full-width'] ? '-full-width' : null].join(' ') } >
{renderSlot}
</div>
</div>
</div>
)
})
const rootClasses = ['vertical-tab-switcher']
if (useInterfaceStore().layoutType === 'mobile') {
rootClasses.push('-mobile')
}
if (this.navSide === 'tabs') {
rootClasses.push('-nav-tabs')
} else {
rootClasses.push('-nav-contents')
}
return (
<div ref="root" class={ rootClasses.join(' ') }>
<div
class="tabs"
role="tablist"
ref="nav"
>
{tabs}
</div>
<div
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
ref="contents"
>
{contents}
</div>
</div>
)
}
}

View file

@ -0,0 +1,176 @@
.vertical-tab-switcher {
display: flex;
flex-direction: row;
container-type: inline-size;
> .tabs {
flex: 0 0 15em;
flex-direction: column;
overflow: hidden auto;
white-space: nowrap;
text-overflow: ellipsis;
width: 15em;
min-width: 15em;
border-right: 1px solid;
border-right-color: var(--border);
box-sizing: border-box;
> .menu-item {
padding: 0.5em 1em;
.tab-icon {
vertical-align: middle;
margin-right: 0.75em;
}
}
}
> .contents {
flex: 1 0 35em;
.tab-content {
align-self: center;
height: 100%;
&:not(.-full-width) {
max-width: 40em;
}
&.-full-width {
align-self: stretch;
}
}
.tab-content-label {
box-sizing: border-box;
margin: 0;
border-bottom: 1px solid var(--border);
display: none;
button {
box-sizing: border-box;
padding: 0.5em;
}
}
.tab-slot-wrapper {
flex: 1 1 auto;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.tab-content-wrapper {
flex: 1 1 auto;
height: 100%;
&.-hidden {
display: none;
}
}
}
&.-mobile {
> .contents {
.tab-content-label {
display: block
}
}
&.-nav-contents {
> .contents {
display: block;
flex-grow: 1;
flex-shrink: 1;
}
> .tabs {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
&.-nav-tabs {
> .tabs {
display: block;
flex-grow: 1;
}
> .contents {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
}
@supports (container-type: inline-size) {
&,
&.-mobile {
&.-nav-contents,
&.-nav-tabs {
/* I THINK it's a false positive and eslint doesn't understand the @-rule */
/* stylelint-disable no-descending-specificity */
> .contents {
display: block;
flex-grow: 1;
}
> .tabs {
display: block;
flex-grow: 0;
}
/* stylelint-enable no-descending-specificity */
}
}
@container (width < 50em) {
> .contents {
.tab-content-label {
display: block
}
}
&.-mobile {
> .contents {
.tab-content-label {
display: block
}
}
}
&,
&.-mobile {
&.-nav-contents {
> .contents {
display: block;
flex-grow: 1;
}
> .tabs {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
}
&.-nav-tabs {
/* stylelint-disable no-descending-specificity */
> .tabs {
display: block;
flex-grow: 1;
}
> .contents {
display: none;
flex-grow: 0;
flex-shrink: 1;
}
/* stylelint-enable no-descending-specificity */
}
}
}
}
}

View file

@ -132,6 +132,7 @@
},
"importer": {
"submit": "Submit",
"import": "Import",
"success": "Imported successfully.",
"error": "An error occured while importing this file."
},
@ -403,6 +404,9 @@
"setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
"post_look_feel": "Posts Look & Feel",
"posts": "Posts",
"developer": "Developer",
"debug": "Debug",
"mention_links": "Mention links",
"appearance": "Appearance",
"confirm_new_setting": "Confirm new setting?",
@ -416,9 +420,14 @@
"navbar_size": "Top bar size",
"panel_header_size": "Panel header size",
"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",
"scale_and_layout": "Interface scale and layout",
"timelines": "Timelines",
"format_and_language": "Format and Language",
"confirmations": "Confirmations",
"layout": "Layout",
"enabled": "Enabled",
"clutter": "Clutter",
"filter": {
"clutter": "Remove clutter",
"mute_filter": "Mute Filters",
@ -533,6 +542,7 @@
"chatMessageRadius": "Chat message",
"collapse_subject": "Collapse posts with subjects",
"composing": "Composing",
"replies": "Replying",
"confirm_new_password": "Confirm new password",
"current_password": "Current password",
"confirm_dialogs": "Ask for confirmation when",
@ -594,6 +604,12 @@
"follow_export_button": "Export your follows to a csv file",
"follow_import": "Follow import",
"follow_import_error": "Error importing followers",
"import_export": {
"title": "Import / Export",
"follows": "List of users you follow",
"blocks": "List of users you block",
"mutes": "List of users you mute"
},
"follows_imported": "Follows imported! Processing them will take a while.",
"accent": "Accent",
"foreground": "Foreground",
@ -745,7 +761,7 @@
"subject_line_email": "Like email: \"re: subject\"",
"subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy",
"force_theme_recompilation_debug": "Disable theme cahe, force recompile on each boot (DEBUG)",
"force_theme_recompilation_debug": "Disable theme cahe, force recompile on each boot",
"conversation_display": "Conversation display style",
"conversation_display_tree": "Tree-style",
"conversation_display_tree_quick": "Tree view",
@ -760,6 +776,8 @@
"column_sizes_sidebar": "Sidebar",
"column_sizes_content": "Content",
"column_sizes_notifs": "Notifications",
"layout": "Layout",
"scale_and_font": "Scale and Font",
"theme_editor_min_width": "Minimum width of theme editor (0 for \"fit-content\")",
"tree_advanced": "Allow more flexible navigation in tree view",
"tree_fade_ancestors": "Display ancestors of the current status in faint text",
@ -770,6 +788,7 @@
"conversation_other_replies_button_inside": "Inside statuses",
"max_depth_in_thread": "Maximum number of levels in thread to display by default",
"post_status_content_type": "Post status content type",
"default_post_status_content_type": "Default post status content type",
"sensitive_by_default": "Mark posts as sensitive by default",
"stop_gifs": "Pause animated images until you hover on them",
"streaming": "Automatically show new posts when scrolled to the top",
@ -811,8 +830,11 @@
"user_popover_avatar_overlay": "Show user popover over user avatar",
"user_card_left_justify": "Justify user bio to the left",
"user_card_hide_personal_marks": "Hide personal marks (highlight/note) in user profiles",
"posts_appearance": "Posts appearance",
"fun": "Fun",
"greentext": "Meme arrows",
"plaintext_quotes": "Highlight plaintext {0}",
"greentext_quotes": ">quotes",
"show_yous": "Show (You)s",
"notifications": "Notifications",
"notification_setting_annoyance": "Annoyance",
@ -832,11 +854,13 @@
"enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.",
"more_settings": "More settings",
"style": {
"style_section": "Style",
"custom_theme_used": "(Custom theme)",
"custom_style_used": "(Custom style)",
"stock_theme_used": "(Stock theme)",
"themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.",
"appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI",
"visual_tweaks_section_note": "Changes in this section do not affect the theme used, exported theme will be different from what seen in the UI",
"update_preview": "Update preview",
"themes3": {
"define": "Override",
@ -1075,6 +1099,13 @@
"post": "Post text",
"monospace": "Monospaced text"
},
"components_inline": {
"interface": "interface",
"input": "input fields",
"post": "post text",
"monospace": "monospaced text"
},
"override": "Override {0} font",
"family": "Font name",
"size": "Size (in px)",
"weight": "Weight (boldness)",
@ -1346,6 +1377,8 @@
"show_content": "Show content",
"hide_content": "Hide content",
"status_deleted": "This post was deleted",
"unknown_user": "unknown user",
"unknown_user_info": "Unable to fetch information about this user",
"nsfw": "NSFW",
"expand": "Expand",
"you": "(You)",

View file

@ -6,7 +6,7 @@ import localeService from '../services/locale/locale.service.js'
import { useI18nStore } from 'src/stores/i18n.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { defaultState } from './default_config_state.js'
import { instanceDefaultConfig, defaultState } from './default_config_state.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
const APPEARANCE_SETTINGS_KEYS = new Set([
@ -38,9 +38,7 @@ export const multiChoiceProperties = [
]
// caching the instance default properties
export const instanceDefaultProperties = Object.entries(defaultState)
.filter(([, value]) => value === undefined)
.map(([key]) => key)
export const instanceDefaultProperties = Object.keys(instanceDefaultConfig)
const config = {
state: { ...defaultState },

View file

@ -1,45 +1,43 @@
const browserLocale = (window.navigator.language || 'en').split('-')[0]
export const defaultState = {
expertLevel: 0, // used to track which settings to show and hide
// Theme stuff
theme: undefined, // Very old theme store, stores preset name, still in use
// V1
colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
// V2
customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
customThemeSource: undefined, // "source", stores original theme data
// V3
style: null,
styleCustomData: null,
/// Instance config entries provided by static config or pleroma api
/// Put settings here only if it does not make sense for a normal user
/// to override it.
export const staticOrApiConfigDefault = {
theme: 'pleroma-dark',
palette: null,
paletteCustomData: null,
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
theme3hacks: { // Hacks, user overrides that are independent of theme used
underlay: 'none',
fonts: {
interface: undefined,
input: undefined,
post: undefined,
monospace: undefined
}
},
style: null,
defaultAvatar: '/images/avi.png',
defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg',
embeddedToS: true,
logo: '/static/logo.svg',
logoMargin: '.2em',
logoMask: true,
logoLeft: false,
redirectRootLogin: '/main/friends',
redirectRootNoLogin: '/main/all',
hideSitename: false,
nsfwCensorImage: undefined,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
}
/// This object contains setting entries that makes sense
/// at the user level. The defaults can also be overriden by
/// instance admins in the frontend_configuration endpoint or static config.
export const instanceDefaultConfig = {
expertLevel: 0, // used to track which settings to show and hide
hideISP: false,
hideInstanceWallpaper: false,
hideShoutbox: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default
hideMutedThreads: undefined, // instance default
hideWordFilteredPosts: undefined, // instance default
muteBotStatuses: undefined, // instance default
muteSensitiveStatuses: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
hideMutedPosts: false,
hideMutedThreads: true,
hideWordFilteredPosts: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
collapseMessageWithSubject: false,
padEmoji: true,
hideAttachments: false,
hideAttachmentsInConv: false,
@ -50,6 +48,9 @@ export const defaultState = {
preloadImage: true,
loopVideo: true,
loopVideoSilentOnly: true,
/// This is not the streaming API configuration, but rather an option
/// for automatically loading new posts into the timeline without
/// the user clicking the Show New button.
streaming: false,
emojiReactionsOnTimeline: true,
alwaysShowNewPostButton: false,
@ -86,39 +87,37 @@ export const defaultState = {
},
webPushNotifications: false,
webPushAlwaysShowNotifications: false,
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale,
hideScopeNotice: false,
useStreamingApi: false,
sidebarRight: undefined, // instance default
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
postContentType: undefined, // instance default
minimalScopesMode: undefined, // instance default
sidebarRight: false,
scopeCopy: true,
subjectLineBehavior: 'email',
alwaysShowSubjectInput: true,
postContentType: 'text/plain',
minimalScopesMode: false,
// This hides statuses filtered via a word filter
hideFilteredStatuses: undefined, // instance default
hideFilteredStatuses: false,
// Confirmations
modalOnRepeat: undefined, // instance default
modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default
modalOnMuteConversation: undefined, // instance default
modalOnMuteDomain: undefined, // instance default
modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default
modalOnDenyFollow: undefined, // instance default
modalOnRemoveUserFromFollowers: undefined, // instance default
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: false,
// Expiry confirmations/default actions
onMuteDefaultAction: 'ask',
onBlockDefaultAction: 'ask',
modalMobileCenter: undefined,
modalMobileCenter: false,
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: true,
@ -131,45 +130,93 @@ export const defaultState = {
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
themeEditorMinWidth: undefined, // instance default
emojiReactionsScale: undefined,
textSize: undefined, // instance default
emojiSize: undefined, // instance default
navbarSize: undefined, // instance default
panelHeaderSize: undefined, // instance default
forcedRoundness: undefined, // instance default
themeEditorMinWidth: '0rem',
emojiReactionsScale: 0.5,
textSize: '1rem',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
forcedRoundness: -1,
navbarColumnStretch: false,
greentext: undefined, // instance default
mentionLinkDisplay: undefined, // instance default
mentionLinkShowTooltip: undefined, // instance default
mentionLinkShowAvatar: undefined, // instance default
mentionLinkFadeDomain: undefined, // instance default
mentionLinkShowYous: undefined, // instance default
mentionLinkBoldenYou: undefined, // instance default
hidePostStats: undefined, // instance default
hideBotIndication: undefined, // instance default
hideUserStats: undefined, // instance default
virtualScrolling: undefined, // instance default
sensitiveByDefault: undefined, // instance default
conversationDisplay: undefined, // instance default
conversationTreeAdvanced: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default
showExtraNotifications: undefined, // instance default
showExtraNotificationsTip: undefined, // instance default
showChatsInExtraNotifications: undefined, // instance default
showAnnouncementsInExtraNotifications: undefined, // instance default
showFollowRequestsInExtraNotifications: undefined, // instance default
maxDepthInThread: undefined, // instance default
autocompleteSelect: undefined, // instance default
closingDrawerMarksAsSeen: undefined, // instance default
unseenAtTop: undefined, // instance default
ignoreInactionableSeen: undefined, // instance default
unsavedPostAction: undefined, // instance default
autoSaveDraft: undefined, // instance default
useAbsoluteTimeFormat: undefined, // instance default
absoluteTimeFormatMinAge: undefined, // instance default
absoluteTime12h: undefined, // instance default
greentext: false,
mentionLinkDisplay: 'short',
mentionLinkShowTooltip: true,
mentionLinkShowAvatar: false,
mentionLinkFadeDomain: true,
mentionLinkShowYous: false,
mentionLinkBoldenYou: true,
hidePostStats: false,
hideBotIndication: false,
hideUserStats: false,
virtualScrolling: true,
sensitiveByDefault: false,
conversationDisplay: 'linear',
conversationTreeAdvanced: false,
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
showExtraNotifications: true,
showExtraNotificationsTip: true,
showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
unsavedPostAction: 'confirm',
autoSaveDraft: false,
useAbsoluteTimeFormat: false,
absoluteTimeFormatMinAge: '0d',
absoluteTime12h: '24h',
imageCompression: true,
alwaysUseJpeg: false
}
export const makeUndefined = c => Object.fromEntries(Object.keys(c).map(key => [key, undefined]))
/// For properties with special processing or properties that does not
/// make sense to be overriden on a instance-wide level.
export const defaultState = {
// Set these to undefined so it does not interfere with default settings check
...makeUndefined(instanceDefaultConfig),
// Special processing
// Theme stuff
theme: undefined, // Very old theme store, stores preset name, still in use
// V1
colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore
// V2
customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event.
customThemeSource: undefined, // "source", stores original theme data
// V3
style: null,
styleCustomData: null,
palette: null,
paletteCustomData: null,
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
theme3hacks: { // Hacks, user overrides that are independent of theme used
underlay: 'none',
fonts: {
interface: undefined,
input: undefined,
post: undefined,
monospace: undefined
}
},
// Special handling: These fields are not of a primitive type, and
// might cause problems with current code because it specifically checks
// them in state.config (not getters.mergedConfig).
// Specifically, muteWords is now deprecated in favour of a server-side configuration.
muteWords: [],
highlight: {},
// If there are any configurations that does not make sense to
// have instance-wide default, put it here and explain why.
}

View file

@ -4,6 +4,7 @@ import { ensureFinalFallback } from '../i18n/languages.js'
import { useInterfaceStore } from 'src/stores/interface.js'
// See build/emojis_plugin for more details
import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations'
import { staticOrApiConfigDefault, instanceDefaultConfig } from './default_config_state.js';
const SORTED_EMOJI_GROUP_IDS = [
'smileys-and-emotion',
@ -52,90 +53,15 @@ const defaultState = {
vapidPublicKey: undefined,
// Stuff from static/config.json
alwaysShowSubjectInput: true,
defaultAvatar: '/images/avi.png',
defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg',
embeddedToS: true,
collapseMessageWithSubject: false,
greentext: false,
mentionLinkDisplay: 'short',
mentionLinkShowTooltip: true,
mentionLinkShowAvatar: false,
mentionLinkFadeDomain: true,
mentionLinkShowYous: false,
mentionLinkBoldenYou: true,
hideFilteredStatuses: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: false,
hideMutedThreads: true,
hideWordFilteredPosts: false,
hidePostStats: false,
hideBotIndication: false,
hideSitename: false,
hideUserStats: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: false,
modalMobileCenter: false,
loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
logoMask: true,
logoLeft: false,
disableUpdateNotification: false,
minimalScopesMode: false,
nsfwCensorImage: undefined,
postContentType: 'text/plain',
redirectRootLogin: '/main/friends',
redirectRootNoLogin: '/main/all',
scopeCopy: true,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
palette: null,
style: null,
emojiReactionsScale: 0.5,
textSize: '1rem',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
themeEditorMinWidth: '0rem',
forcedRoundness: -1,
fontsOverride: {},
virtualScrolling: true,
sensitiveByDefault: false,
conversationDisplay: 'linear',
conversationTreeAdvanced: false,
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
showExtraNotifications: true,
showExtraNotificationsTip: true,
showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
unsavedPostAction: 'confirm',
autoSaveDraft: false,
useAbsoluteTimeFormat: false,
absoluteTimeFormatMinAge: '0d',
absoluteTime12h: '24h',
// Instance-wide configurations that should not be changed by individual users
...staticOrApiConfigDefault,
// Instance admins can override default settings for the whole instance
...instanceDefaultConfig,
// Nasty stuff
customEmoji: [],

View file

@ -111,7 +111,10 @@ const sortTimeline = (timeline) => {
const getLatestScrobble = (state, user) => {
const scrobblesSupport = state.pleromaScrobblesAvailable
if (!scrobblesSupport) return
if (!scrobblesSupport || !user.name || user.id === 'undefined') {
return
}
if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) {
return

View file

@ -2,7 +2,7 @@ export const muteFilterHits = (muteFilters, status) => {
const statusText = status.text.toLowerCase()
const statusSummary = status.summary.toLowerCase()
const replyToUser = status.in_reply_to_screen_name?.toLowerCase()
const poster = status.user.screen_name.toLowerCase()
const poster = status.user.screen_name?.toLowerCase()
const mentions = (status.attentions || []).map(att => att.screen_name.toLowerCase())

View file

@ -46,7 +46,7 @@ const highlightStyle = (prefs) => {
const highlightClass = (user) => {
return 'USER____' + user.screen_name
.replace(/\./g, '_')
?.replace(/\./g, '_')
.replace(/@/g, '_AT_')
}

1138
yarn.lock

File diff suppressed because it is too large Load diff