757 lines
19 KiB
JavaScript
757 lines
19 KiB
JavaScript
import { get, set } from 'lodash'
|
|
|
|
const browserLocale = (navigator.language || 'en').split('-')[0]
|
|
|
|
export const convertDefinitions = (definitions) =>
|
|
Object.fromEntries(
|
|
Object.entries(definitions).map(([k, v]) => {
|
|
const defaultValue = v.default ?? null
|
|
return [k, defaultValue]
|
|
}),
|
|
)
|
|
|
|
/// 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 INSTANCE_IDENTITY_DEFAULT_DEFINITIONS = {
|
|
style: {
|
|
description: 'Instance default style name',
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
palette: {
|
|
description: 'Instance default palette name',
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
theme: {
|
|
description: 'Instance default theme name',
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
defaultAvatar: {
|
|
description: "Default avatar image to use when user doesn't have one set",
|
|
type: 'string',
|
|
default: '/images/avi.png',
|
|
},
|
|
defaultBanner: {
|
|
description: "Default banner image to use when user doesn't have one set",
|
|
type: 'string',
|
|
default: '/images/banner.png',
|
|
},
|
|
background: {
|
|
description: 'Instance background/wallpaper',
|
|
type: 'string',
|
|
default: '/static/aurora_borealis.jpg',
|
|
},
|
|
embeddedToS: {
|
|
description: 'Whether to show Terms of Service title bar',
|
|
type: 'boolean',
|
|
default: true,
|
|
},
|
|
logo: {
|
|
description: 'Instance logo',
|
|
type: 'string',
|
|
default: '/static/logo.svg',
|
|
},
|
|
logoMargin: {
|
|
description: 'Margin for logo (spacing above/below)',
|
|
type: 'string',
|
|
default: '.2em',
|
|
},
|
|
logoMask: {
|
|
description:
|
|
'Use logo as a mask (works well for monochrome/transparent logos)',
|
|
type: 'boolean',
|
|
default: true,
|
|
},
|
|
logoLeft: {
|
|
description: 'Show logo on the left side of navbar',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
redirectRootLogin: {
|
|
description: 'Where to redirect user after login',
|
|
type: 'string',
|
|
default: '/main/friends',
|
|
},
|
|
redirectRootNoLogin: {
|
|
description: 'Where to redirect anonymous visitors',
|
|
type: 'string',
|
|
default: '/main/all',
|
|
},
|
|
hideSitename: {
|
|
description: 'Hide the instance name in navbar',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
nsfwCensorImage: {
|
|
description: 'Default NSFW censor image',
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
showFeaturesPanel: {
|
|
description: 'Show features panel to anonymous visitors',
|
|
type: 'boolean',
|
|
default: true,
|
|
},
|
|
showInstanceSpecificPanel: {
|
|
description: 'Show instance-specific panel',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
|
|
// Html stuff
|
|
instanceSpecificPanelContent: {
|
|
description: 'HTML of Instance-specific panel',
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
tos: {
|
|
description: 'HTML of Terms of Service panel',
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
}
|
|
export const INSTANCE_IDENTITY_DEFAULT = convertDefinitions(
|
|
INSTANCE_IDENTITY_DEFAULT_DEFINITIONS,
|
|
)
|
|
|
|
/// 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 INSTANCE_DEFAULT_CONFIG_DEFINITIONS = {
|
|
expertLevel: {
|
|
description:
|
|
'Used to track which settings to show and hide in settings modal',
|
|
type: 'number', // not a boolean so we could potentially make multiple levels of expert-ness
|
|
default: 0,
|
|
},
|
|
hideISP: {
|
|
description: 'Hide Instance-specific panel',
|
|
default: false,
|
|
},
|
|
hideInstanceWallpaper: {
|
|
description: 'Hide Instance default background',
|
|
default: false,
|
|
},
|
|
hideShoutbox: {
|
|
description: 'Hide shoutbox if present',
|
|
default: false,
|
|
},
|
|
hideMutedPosts: {
|
|
// bad name
|
|
description: 'Hide posts of muted users entirely',
|
|
default: false,
|
|
},
|
|
hideMutedThreads: {
|
|
description: 'Hide muted threads entirely',
|
|
default: true,
|
|
},
|
|
hideWordFilteredPosts: {
|
|
description: 'Hide wordfiltered posts entirely',
|
|
default: false,
|
|
},
|
|
muteBotStatuses: {
|
|
description: 'Mute posts made by bots',
|
|
default: false,
|
|
},
|
|
muteSensitiveStatuses: {
|
|
description: 'Mute posts marked as NSFW',
|
|
default: false,
|
|
},
|
|
collapseMessageWithSubject: {
|
|
description: 'Collapse posts with subject',
|
|
default: false,
|
|
},
|
|
padEmoji: {
|
|
description: 'Pad emoji with spaces when using emoji picker',
|
|
default: true,
|
|
},
|
|
hideAttachmentsInConv: {
|
|
description: 'Hide attachments',
|
|
default: false,
|
|
},
|
|
hideScrobbles: {
|
|
description: 'Hide scrobbles',
|
|
default: false,
|
|
},
|
|
hideScrobblesAfter: {
|
|
description: 'Hide scrobbles older than',
|
|
default: '2d',
|
|
},
|
|
maxThumbnails: {
|
|
description: 'Maximum attachments to show',
|
|
default: 16,
|
|
},
|
|
loopVideo: {
|
|
description: 'Loop videos',
|
|
default: true,
|
|
},
|
|
loopVideoSilentOnly: {
|
|
description: 'Loop only videos without sound',
|
|
default: 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: {
|
|
description: 'Automatically show new posts',
|
|
default: false,
|
|
},
|
|
pauseOnUnfocused: {
|
|
description: 'Pause showing new posts when tab is unfocused',
|
|
default: true,
|
|
},
|
|
emojiReactionsOnTimeline: {
|
|
description: 'Show emoji reactions on timeline',
|
|
default: true,
|
|
},
|
|
alwaysShowNewPostButton: {
|
|
description: 'Always show mobile "new post" button, even in desktop mode',
|
|
default: false,
|
|
},
|
|
autohideFloatingPostButton: {
|
|
description:
|
|
'Automatically hide mobile "new post" button when scrolling down',
|
|
default: false,
|
|
},
|
|
stopGifs: {
|
|
description: 'Play animated gifs on hover only',
|
|
default: true,
|
|
},
|
|
replyVisibility: {
|
|
description: 'Type of replies to show',
|
|
default: 'all',
|
|
},
|
|
thirdColumnMode: {
|
|
description: 'What to display in third column',
|
|
default: 'notifications',
|
|
},
|
|
notificationVisibility: {
|
|
description: 'What types of notifications to show',
|
|
default: {
|
|
follows: true,
|
|
mentions: true,
|
|
statuses: true,
|
|
likes: true,
|
|
repeats: true,
|
|
moves: true,
|
|
emojiReactions: true,
|
|
followRequest: true,
|
|
reports: true,
|
|
chatMention: true,
|
|
polls: true,
|
|
},
|
|
},
|
|
notificationNative: {
|
|
description: 'What type of notifications to show desktop notification for',
|
|
default: {
|
|
follows: true,
|
|
mentions: true,
|
|
statuses: true,
|
|
likes: false,
|
|
repeats: false,
|
|
moves: false,
|
|
emojiReactions: false,
|
|
followRequest: true,
|
|
reports: true,
|
|
chatMention: true,
|
|
polls: true,
|
|
},
|
|
},
|
|
webPushNotifications: {
|
|
description: 'Use WebPush',
|
|
default: false,
|
|
},
|
|
webPushAlwaysShowNotifications: {
|
|
description: 'Ignore filter when using WebPush',
|
|
default: false,
|
|
},
|
|
interfaceLanguage: {
|
|
description: 'UI language',
|
|
default: browserLocale,
|
|
},
|
|
hideScopeNotice: {
|
|
description: 'Hide scope notification',
|
|
default: false,
|
|
},
|
|
scopeCopy: {
|
|
description: 'Copy scope like mastodon does',
|
|
default: true,
|
|
},
|
|
subjectLineBehavior: {
|
|
description: 'How to treat subject line',
|
|
default: 'email',
|
|
},
|
|
alwaysShowSubjectInput: {
|
|
description: 'Always show subject line field',
|
|
default: true,
|
|
},
|
|
minimalScopesMode: {
|
|
description: 'Minimize amount of options shown in scope selector',
|
|
default: false,
|
|
},
|
|
|
|
// This hides statuses filtered via a word filter
|
|
hideFilteredStatuses: {
|
|
description: 'Hide wordfiltered entirely',
|
|
default: false,
|
|
},
|
|
|
|
// Confirmations
|
|
modalOnRepeat: {
|
|
description: 'Show confirmation modal for repeat',
|
|
default: false,
|
|
},
|
|
modalOnUnfollow: {
|
|
description: 'Show confirmation modal for unfollow',
|
|
default: false,
|
|
},
|
|
modalOnBlock: {
|
|
description: 'Show confirmation modal for block',
|
|
default: true,
|
|
},
|
|
modalOnMute: {
|
|
description: 'Show confirmation modal for mute',
|
|
default: false,
|
|
},
|
|
modalOnMuteConversation: {
|
|
description: 'Show confirmation modal for mute conversation',
|
|
default: false,
|
|
},
|
|
modalOnMuteDomain: {
|
|
description: 'Show confirmation modal for mute domain',
|
|
default: true,
|
|
},
|
|
modalOnDelete: {
|
|
description: 'Show confirmation modal for delete',
|
|
default: true,
|
|
},
|
|
modalOnLogout: {
|
|
description: 'Show confirmation modal for logout',
|
|
default: true,
|
|
},
|
|
modalOnApproveFollow: {
|
|
description: 'Show confirmation modal for approve follow',
|
|
default: false,
|
|
},
|
|
modalOnDenyFollow: {
|
|
description: 'Show confirmation modal for deny follow',
|
|
default: false,
|
|
},
|
|
modalOnRemoveUserFromFollowers: {
|
|
description: 'Show confirmation modal for follower removal',
|
|
default: false,
|
|
},
|
|
|
|
// Expiry confirmations/default actions
|
|
onMuteDefaultAction: {
|
|
description: 'Default action when muting user',
|
|
default: 'ask',
|
|
},
|
|
onBlockDefaultAction: {
|
|
description: 'Default action when blocking user',
|
|
default: 'ask',
|
|
},
|
|
|
|
modalMobileCenter: {
|
|
description: 'Center mobile dialogs vertically',
|
|
default: false,
|
|
},
|
|
playVideosInModal: {
|
|
description: 'Play videos in gallery view',
|
|
default: false,
|
|
},
|
|
useContainFit: {
|
|
description: 'Use object-fit: contain for attachments',
|
|
default: true,
|
|
},
|
|
disableStickyHeaders: {
|
|
description: 'Disable sticky headers',
|
|
default: false,
|
|
},
|
|
showScrollbars: {
|
|
description: 'Always show scrollbars',
|
|
default: false,
|
|
},
|
|
userPopoverAvatarAction: {
|
|
description: 'What to do when clicking popover avatar',
|
|
default: 'open',
|
|
},
|
|
userPopoverOverlay: {
|
|
description: 'Overlay user popover with centering on avatar',
|
|
default: false,
|
|
},
|
|
userCardLeftJustify: {
|
|
description: 'Justify user bio to the left',
|
|
default: false,
|
|
},
|
|
userCardHidePersonalMarks: {
|
|
description: 'Hide highlight/personal note in user view',
|
|
default: false,
|
|
},
|
|
forcedRoundness: {
|
|
description: 'Force roundness of the theme',
|
|
default: -1,
|
|
},
|
|
greentext: {
|
|
description: 'Highlight plaintext >quotes',
|
|
default: false,
|
|
},
|
|
mentionLinkShowTooltip: {
|
|
description: 'Show tooltips for mention links',
|
|
default: true,
|
|
},
|
|
mentionLinkShowAvatar: {
|
|
description: 'Show avatar next to mention link',
|
|
default: false,
|
|
},
|
|
mentionLinkFadeDomain: {
|
|
description:
|
|
'Mute (fade) domain name in mention links if configured to show it',
|
|
default: true,
|
|
},
|
|
mentionLinkShowYous: {
|
|
description: 'Show (you)s when you are mentioned',
|
|
default: false,
|
|
},
|
|
mentionLinkBoldenYou: {
|
|
description: 'Boldern mentionlink of you',
|
|
default: true,
|
|
},
|
|
hidePostStats: {
|
|
description: 'Hide post stats (rt, favs)',
|
|
default: false,
|
|
},
|
|
hideBotIndication: {
|
|
description: 'Hide bot indicator',
|
|
default: false,
|
|
},
|
|
hideUserStats: {
|
|
description: 'Hide user stats (followers etc)',
|
|
default: false,
|
|
},
|
|
virtualScrolling: {
|
|
description: 'Timeline virtual scrolling',
|
|
default: true,
|
|
},
|
|
sensitiveByDefault: {
|
|
description: 'Assume attachments are NSFW by default',
|
|
default: false,
|
|
},
|
|
conversationDisplay: {
|
|
description: 'Style of conversation display',
|
|
default: 'linear',
|
|
},
|
|
conversationTreeAdvanced: {
|
|
description: 'Advanced features of tree view conversation',
|
|
default: false,
|
|
},
|
|
conversationOtherRepliesButton: {
|
|
description: 'Where to show "other replies" in tree conversation view',
|
|
default: 'below',
|
|
},
|
|
conversationTreeFadeAncestors: {
|
|
description: 'Fade ancestors in tree conversation view',
|
|
default: false,
|
|
},
|
|
showExtraNotifications: {
|
|
description:
|
|
'Show extra notifications (chats, announcements etc) in notification panel',
|
|
default: true,
|
|
},
|
|
showExtraNotificationsTip: {
|
|
description: 'Show tip for extra notifications (that user can remove them)',
|
|
default: true,
|
|
},
|
|
showChatsInExtraNotifications: {
|
|
description: 'Show chat messages in notifications',
|
|
default: true,
|
|
},
|
|
showAnnouncementsInExtraNotifications: {
|
|
description: 'Show announcements in notifications',
|
|
default: true,
|
|
},
|
|
showFollowRequestsInExtraNotifications: {
|
|
description: 'Show follow requests in notifications',
|
|
default: true,
|
|
},
|
|
maxDepthInThread: {
|
|
description: 'Maximum depth in tree conversation view',
|
|
default: 6,
|
|
},
|
|
autocompleteSelect: {
|
|
description: '',
|
|
default: false,
|
|
},
|
|
closingDrawerMarksAsSeen: {
|
|
description: 'Closing mobile notification pane marks everything as seen',
|
|
default: true,
|
|
},
|
|
unseenAtTop: {
|
|
description: 'Show unseen notifications above others',
|
|
default: false,
|
|
},
|
|
ignoreInactionableSeen: {
|
|
description: 'Treat inactionable (fav, rt etc) notifications as "seen"',
|
|
default: false,
|
|
},
|
|
unsavedPostAction: {
|
|
description: 'What to do if post is aborted',
|
|
default: 'confirm',
|
|
},
|
|
autoSaveDraft: {
|
|
description: 'Save drafts automatically',
|
|
default: false,
|
|
},
|
|
useAbsoluteTimeFormat: {
|
|
description: 'Use absolute time format',
|
|
default: false,
|
|
},
|
|
absoluteTimeFormatMinAge: {
|
|
description: 'Show absolute time format only after this post age',
|
|
default: '0d',
|
|
},
|
|
absoluteTime12h: {
|
|
description: 'Use 24h time format',
|
|
default: '24h',
|
|
},
|
|
themeChecksum: {
|
|
description: 'Checksum of theme used',
|
|
type: 'string',
|
|
required: false,
|
|
},
|
|
}
|
|
export const INSTANCE_DEFAULT_CONFIG = convertDefinitions(
|
|
INSTANCE_DEFAULT_CONFIG_DEFINITIONS,
|
|
)
|
|
|
|
export const LOCAL_DEFAULT_CONFIG_DEFINITIONS = {
|
|
// TODO these two used to be separate but since separation feature got broken it doesn't matter
|
|
hideAttachments: {
|
|
description: 'Hide attachments in timeline',
|
|
default: false,
|
|
},
|
|
hideAttachmentsInConv: {
|
|
description: 'Hide attachments in coversation',
|
|
default: false,
|
|
},
|
|
hideNsfw: {
|
|
description: 'Hide nsfw posts',
|
|
default: true,
|
|
},
|
|
useOneClickNsfw: {
|
|
description: 'Open NSFW images directly in media modal',
|
|
default: false,
|
|
},
|
|
preloadImage: {
|
|
description: 'Preload images for NSFW',
|
|
default: true,
|
|
},
|
|
postContentType: {
|
|
description: 'Default post content type',
|
|
default: 'text/plain',
|
|
},
|
|
sidebarRight: {
|
|
description: 'Reverse order of columns',
|
|
default: false,
|
|
},
|
|
sidebarColumnWidth: {
|
|
description: 'Sidebar column width',
|
|
default: '25rem',
|
|
},
|
|
contentColumnWidth: {
|
|
description: 'Middle column width',
|
|
default: '45rem',
|
|
},
|
|
notifsColumnWidth: {
|
|
description: 'Notifications column width',
|
|
default: '25rem',
|
|
},
|
|
themeEditorMinWidth: {
|
|
description: 'Hack for theme editor on mobile',
|
|
default: '0rem',
|
|
},
|
|
emojiReactionsScale: {
|
|
description: 'Emoji reactions scale factor',
|
|
default: 0.5,
|
|
},
|
|
textSize: {
|
|
description: 'Font size',
|
|
default: '1rem',
|
|
},
|
|
emojiSize: {
|
|
description: 'Emoji size',
|
|
default: '2.2rem',
|
|
},
|
|
navbarSize: {
|
|
description: 'Navbar size',
|
|
default: '3.5rem',
|
|
},
|
|
panelHeaderSize: {
|
|
description: 'Panel header size',
|
|
default: '3.2rem',
|
|
},
|
|
navbarColumnStretch: {
|
|
description: 'Stretch navbar to match columns width',
|
|
default: false,
|
|
},
|
|
mentionLinkDisplay: {
|
|
description: 'How to display mention links',
|
|
default: 'short',
|
|
},
|
|
imageCompression: {
|
|
description: 'Image compression (WebP/JPEG)',
|
|
default: true,
|
|
},
|
|
alwaysUseJpeg: {
|
|
description: 'Compress images using JPEG only',
|
|
default: false,
|
|
},
|
|
useStreamingApi: {
|
|
description: 'Streaming API (WebSocket)',
|
|
default: false,
|
|
},
|
|
underlay: {
|
|
description: 'Underlay override',
|
|
required: true,
|
|
default: 'none',
|
|
},
|
|
fontInterface: {
|
|
description: 'Interface font override',
|
|
type: 'string',
|
|
default: null,
|
|
},
|
|
fontInput: {
|
|
description: 'Input font override',
|
|
type: 'string',
|
|
default: null,
|
|
},
|
|
fontPosts: {
|
|
description: 'Post font override',
|
|
type: 'string',
|
|
default: null,
|
|
},
|
|
fontMonospace: {
|
|
description: 'Monospace font override',
|
|
type: 'string',
|
|
default: null,
|
|
},
|
|
themeDebug: {
|
|
description:
|
|
'Debug mode that uses computed backgrounds instead of real ones to debug contrast functions',
|
|
default: false,
|
|
},
|
|
forceThemeRecompilation: {
|
|
description: 'Flag that forces recompilation on boot even if cache exists',
|
|
default: false,
|
|
},
|
|
}
|
|
export const LOCAL_DEFAULT_CONFIG = convertDefinitions(
|
|
LOCAL_DEFAULT_CONFIG_DEFINITIONS,
|
|
)
|
|
|
|
export const LOCAL_ONLY_KEYS = new Set(Object.keys(LOCAL_DEFAULT_CONFIG))
|
|
|
|
export const THEME_CONFIG_DEFINITIONS = {
|
|
theme: {
|
|
description: 'Very old theme store, stores preset name, still in use',
|
|
default: null,
|
|
},
|
|
colors: {
|
|
description: 'VERY old theme store, just colors of V1, probably not even used anymore',
|
|
default: {},
|
|
},
|
|
// V2
|
|
customTheme: {
|
|
description: '"Snapshot", previously was used as actual theme store for V2 so it\'s still used in case of PleromaFE downgrade event.',
|
|
default: null,
|
|
},
|
|
customThemeSource: {
|
|
description: '"Source", stores original theme data',
|
|
default: null,
|
|
},
|
|
// V3
|
|
style: {
|
|
description: 'Style name for builtins',
|
|
default: null,
|
|
},
|
|
styleCustomData: {
|
|
description: 'Custom style data (i.e. not builtin)',
|
|
default: null,
|
|
},
|
|
palette: {
|
|
description: 'Palette name for builtins',
|
|
default: null,
|
|
},
|
|
paletteCustomData: {
|
|
description: 'Custom palette data (i.e. not builtin)',
|
|
default: null,
|
|
},
|
|
}
|
|
export const THEME_CONFIG = convertDefinitions(
|
|
THEME_CONFIG_DEFINITIONS,
|
|
)
|
|
|
|
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(INSTANCE_DEFAULT_CONFIG),
|
|
...makeUndefined(LOCAL_DEFAULT_CONFIG),
|
|
...makeUndefined(THEME_CONFIG),
|
|
}
|
|
|
|
export const validateSetting = ({
|
|
value,
|
|
path,
|
|
definition,
|
|
throwError,
|
|
defaultState,
|
|
}) => {
|
|
if (value === undefined) return // only null is allowed as missing value
|
|
if (get(defaultState, path) === undefined) {
|
|
const string = `Unknown instance option ${path}, value: ${value}`
|
|
|
|
if (throwError) {
|
|
throw new Error(string)
|
|
} else {
|
|
console.error(string)
|
|
return value
|
|
}
|
|
}
|
|
|
|
let { required, type, default: defaultValue } = definition
|
|
|
|
if (type == null && defaultValue != null) {
|
|
type = typeof defaultValue
|
|
}
|
|
|
|
if (required && value == null) {
|
|
const string = `Value required for setting ${path} but was provided nullish; defaulting`
|
|
|
|
if (throwError) {
|
|
throw new Error(string)
|
|
} else {
|
|
console.error(string)
|
|
return defaultValue
|
|
}
|
|
}
|
|
|
|
if (value !== null && type != null && typeof value !== type) {
|
|
const string = `Invalid type for setting ${path}: expected type ${type}, got ${typeof value}, value ${value}; defaulting`
|
|
|
|
if (throwError) {
|
|
throw new Error(string)
|
|
} else {
|
|
console.error(string)
|
|
return defaultValue
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|