Page MenuHomePhorge

No OneTemporary

Size
50 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js
index d1f3390cc7..d7c5aa225f 100644
--- a/src/components/font_control/font_control.js
+++ b/src/components/font_control/font_control.js
@@ -1,64 +1,57 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Popover from 'src/components/popover/popover.vue'
import LocalSettingIndicator from 'src/components/settings_modal/helpers/local_setting_indicator.vue'
import Select from '../select/select.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faExclamationTriangle,
faFont,
faKeyboard,
} from '@fortawesome/free-solid-svg-icons'
library.add(faExclamationTriangle, faKeyboard, faFont)
export default {
components: {
Select,
Checkbox,
Popover,
LocalSettingIndicator,
},
- props: [
- 'name',
- 'label',
- 'modelValue',
- 'fallback',
- 'options',
- 'no-inherit',
- ],
+ props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
mounted() {
useInterfaceStore().queryLocalFonts()
},
emits: ['update:modelValue'],
data() {
return {
manualEntry: false,
availableOptions: [
this.noInherit ? '' : 'inherit',
'serif',
'sans-serif',
'monospace',
...(this.options || []),
].filter((_) => _),
}
},
methods: {
toggleManualEntry() {
this.manualEntry = !this.manualEntry
},
},
computed: {
present() {
return typeof this.modelValue !== 'undefined'
},
localFontsList() {
return useInterfaceStore().localFonts
},
localFontsSize() {
return useInterfaceStore().localFonts?.length
},
},
}
diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js
index bc2465e70c..a391e9b218 100644
--- a/src/modules/default_config_state.js
+++ b/src/modules/default_config_state.js
@@ -1,234 +1,226 @@
const browserLocale = (navigator.language || 'en').split('-')[0]
/// 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 = {
name: 'PleromaFE',
theme: 'pleroma-dark',
palette: null,
style: null,
themeChecksum: undefined,
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: null,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
// Html stuff
instanceSpecificPanelContent: '',
tos: '',
}
/// 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: false,
hideMutedThreads: true,
hideWordFilteredPosts: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
collapseMessageWithSubject: false,
padEmoji: true,
hideAttachmentsInConv: false,
hideScrobbles: false,
hideScrobblesAfter: '2d',
maxThumbnails: 16,
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,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: true,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
notificationVisibility: {
follows: true,
mentions: true,
statuses: true,
likes: true,
repeats: true,
moves: true,
emojiReactions: true,
followRequest: true,
reports: true,
chatMention: true,
polls: true,
},
notificationNative: {
follows: true,
mentions: true,
statuses: true,
likes: false,
repeats: false,
moves: false,
emojiReactions: false,
followRequest: true,
reports: true,
chatMention: true,
polls: true,
},
webPushNotifications: false,
webPushAlwaysShowNotifications: false,
interfaceLanguage: browserLocale,
hideScopeNotice: false,
scopeCopy: true,
subjectLineBehavior: 'email',
alwaysShowSubjectInput: true,
minimalScopesMode: false,
// This hides statuses filtered via a word filter
hideFilteredStatuses: false,
// Confirmations
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: false,
playVideosInModal: false,
useContainFit: true,
disableStickyHeaders: false,
showScrollbars: false,
userPopoverAvatarAction: 'open',
userPopoverOverlay: false,
userCardLeftJustify: false,
userCardHidePersonalMarks: false,
forcedRoundness: -1,
greentext: false,
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',
}
export const defaultConfigLocal = {
hideAttachments: false,
hideAttachmentsInConv: false,
hideNsfw: true,
useOneClickNsfw: false,
preloadImage: true,
postContentType: 'text/plain',
sidebarRight: false,
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
themeEditorMinWidth: '0rem',
emojiReactionsScale: 0.5,
textSize: '1rem',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
navbarColumnStretch: false,
mentionLinkDisplay: 'short',
alwaysUseJpeg: false,
imageCompression: true,
useStreamingApi: 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),
+ ...makeUndefined(defaultConfigLocal),
+ // If there are any configurations that does not make sense to
+ // have instance-wide default, put it here and explain why.
- // Special processing
- // Theme stuff
+ // # 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.
}
diff --git a/src/modules/old_default_config_state.js b/src/modules/old_default_config_state.js
index 6e0e730de7..dc15e1c24c 100644
--- a/src/modules/old_default_config_state.js
+++ b/src/modules/old_default_config_state.js
@@ -1,187 +1,185 @@
// this is a snapshot of config keys used prior to sync config.
// used to migrate from old config.
// commented entries are unsynced stuff
export const defaultConfigSync = {
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: false,
hideMutedThreads: true,
hideWordFilteredPosts: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
collapseMessageWithSubject: false,
padEmoji: true,
hideScrobbles: false,
hideScrobblesAfter: '2d',
maxThumbnails: 16,
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,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: true,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
notificationVisibility: {
follows: true,
mentions: true,
statuses: true,
likes: true,
repeats: true,
moves: true,
emojiReactions: true,
followRequest: true,
reports: true,
chatMention: true,
polls: true,
},
notificationNative: {
follows: true,
mentions: true,
statuses: true,
likes: false,
repeats: false,
moves: false,
emojiReactions: false,
followRequest: true,
reports: true,
chatMention: true,
polls: true,
},
webPushNotifications: false,
webPushAlwaysShowNotifications: false,
//interfaceLanguage: '',
hideScopeNotice: false,
useStreamingApi: false,
sidebarRight: false,
scopeCopy: true,
subjectLineBehavior: 'email',
alwaysShowSubjectInput: true,
postContentType: 'text/plain',
minimalScopesMode: false,
// This hides statuses filtered via a word filter
hideFilteredStatuses: false,
// Confirmations
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: false,
playVideosInModal: false,
useContainFit: true,
disableStickyHeaders: false,
showScrollbars: false,
userPopoverAvatarAction: 'open',
userPopoverOverlay: false,
userCardLeftJustify: false,
userCardHidePersonalMarks: false,
forcedRoundness: -1,
greentext: false,
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',
theme3hacks: {
// Hacks, user overrides that are independent of theme used
underlay: 'none',
fonts: {
interface: undefined,
input: undefined,
post: undefined,
monospace: undefined,
},
},
// 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
// Those are handled outside config now
// muteWords: [],
// highlight: {},
}
export const defaultConfigLocal = {
hideAttachments: false,
hideAttachmentsInConv: false,
hideNsfw: true,
useOneClickNsfw: false,
preloadImage: true,
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
themeEditorMinWidth: '0rem',
emojiReactionsScale: 0.5,
textSize: '1rem',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
navbarColumnStretch: false,
mentionLinkDisplay: 'short',
imageCompression: true,
alwaysUseJpeg: false,
- imageCompression: true,
- alwaysUseJpeg: false,
}
diff --git a/src/stores/local_config.js b/src/stores/local_config.js
index 117be393d1..73ea1d7d4c 100644
--- a/src/stores/local_config.js
+++ b/src/stores/local_config.js
@@ -1,50 +1,50 @@
import { cloneDeep, set } from 'lodash'
import { defineStore } from 'pinia'
import { toRaw } from 'vue'
import { useInstanceStore } from 'src/stores/instance'
import { defaultState as configDefaultState } from 'src/modules/default_config_state'
export const defaultState = {
prefsStorage: {
...configDefaultState,
},
tempStorage: {
...configDefaultState,
},
}
export const useLocalConfigStore = defineStore('local_config', {
state() {
return cloneDeep(defaultState)
},
actions: {
set({ path, value }) {
set(this.prefsStorage, path, value)
},
setTemporarily({ path, value }) {
set(this.tempStorage, path, value)
},
unsetTemporarily({ path, value }) {
set(this.tempStorage, path, undefined)
},
unset({ path, value }) {
set(this.prefsStorage, path, undefined)
},
- clearSyncConfig() {
+ clearLocalConfig() {
const blankState = { ...cloneDeep(defaultState) }
Object.keys(this).forEach((k) => {
this.prefsStorage[k] = blankState[k]
})
},
},
persist: {
afterLoad(state) {
return {
prefsStorage: state.prefsStorage ?? { ...configDefaultState },
tempStorage: { ...configDefaultState },
}
},
},
})
diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js
index 51327c3893..b740e1bec3 100644
--- a/src/stores/sync_config.js
+++ b/src/stores/sync_config.js
@@ -1,736 +1,748 @@
import {
merge as _merge,
clamp,
cloneDeep,
findLastIndex,
flatten,
get,
groupBy,
isEqual,
set,
takeRight,
uniqWith,
unset,
} from 'lodash'
import { defineStore } from 'pinia'
import { toRaw } from 'vue'
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useLocalConfigStore } from 'src/stores/local_config.js'
import { storage } from 'src/lib/storage.js'
-import { defaultState as configDefaultState, makeUndefined, defaultConfigLocal } from 'src/modules/default_config_state.js'
+import {
+ defaultState as configDefaultState,
+ defaultConfigLocal,
+ instanceDefaultConfig,
+} from 'src/modules/default_config_state.js'
import { defaultConfigSync } from 'src/modules/old_default_config_state.js'
export const VERSION = 2
export const NEW_USER_DATE = new Date('2026-03-16') // date of writing this, basically
export const COMMAND_TRIM_FLAGS = 1000
export const COMMAND_TRIM_FLAGS_AND_RESET = 1001
export const COMMAND_WIPE_JOURNAL = 1010
export const COMMAND_WIPE_JOURNAL_AND_STORAGE = 1011
const LOCAL_ONLY_KEYS = new Set(Object.keys(defaultConfigLocal))
export const defaultState = {
// do we need to update data on server?
dirty: false,
// storage of flags - stuff that can only be set and incremented
flagStorage: {
updateCounter: 0, // Counter for most recent update notification seen
reset: 0, // special flag that can be used to force-reset all data, debug purposes only
// special reset codes:
// 1000: trim keys to those known by currently running FE
// 1001: same as above + reset everything to 0
},
prefsStorage: {
_journal: [],
simple: {
dontShowUpdateNotifs: false,
collapseNav: false,
muteFilters: {},
- ...makeUndefined(configDefaultState),
+
+ ...configDefaultState,
},
collections: {
pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
pinnedNavItems: ['home', 'dms', 'chats'],
},
},
// raw data
raw: null,
// local cache
cache: null,
}
export const newUserFlags = {
...defaultState.flagStorage,
updateCounter: CURRENT_UPDATE_COUNTER, // new users don't need to see update notification
}
export const _moveItemInArray = (array, value, movement) => {
const oldIndex = array.indexOf(value)
const newIndex = oldIndex + movement
const newArray = [...array]
// remove old
newArray.splice(oldIndex, 1)
// add new
newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value)
return newArray
}
const _wrapData = (data, userName) => {
return {
...data,
_user: userName,
_timestamp: Date.now(),
_version: VERSION,
}
}
const _checkValidity = (data) => data._timestamp > 0 && data._version > 0
const _verifyPrefs = (state) => {
state.prefsStorage = state.prefsStorage || {
simple: {},
collections: {},
}
// Simple
Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => {
if (typeof v === 'undefined') return
if (typeof v === 'number' || typeof v === 'boolean') return
if (typeof v === 'object') return
console.warn(
`Preference simple.${k} as invalid type ${typeof v}, reinitializing`,
)
set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k])
})
// Collections
Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => {
if (Array.isArray(v)) return
console.warn(
`Preference collections.${k} as invalid type ${typeof v}, reinitializing`,
)
set(
state.prefsStorage.collections,
k,
defaultState.prefsStorage.collections[k],
)
})
}
export const _getRecentData = (cache, live, isTest) => {
const result = { recent: null, stale: null, needUpload: false }
const cacheValid = _checkValidity(cache || {})
const liveValid = _checkValidity(live || {})
if (!liveValid && cacheValid) {
result.needUpload = true
console.debug(
'Nothing valid stored on server, assuming cache to be source of truth',
)
result.recent = cache
result.stale = live
} else if (!cacheValid && liveValid) {
console.debug(
'Valid storage on server found, no local cache found, using live as source of truth',
)
result.recent = live
result.stale = cache
} else if (cacheValid && liveValid) {
console.debug('Both sources have valid data, figuring things out...')
if (
live._timestamp === cache._timestamp &&
live._version === cache._version
) {
console.debug(
'Same version/timestamp on both sources, source of truth irrelevant',
)
result.recent = cache
result.stale = live
} else {
console.debug(
'Different timestamp or version, figuring out which one is more recent',
)
if (live._timestamp < cache._timestamp) {
result.recent = cache
result.stale = live
} else {
result.recent = live
result.stale = cache
}
}
} else {
console.debug('Both sources are invalid, start from scratch')
result.needUpload = true
}
const merge = (a, b) => {
return {
_user: a._user ?? b._user,
_version: a._version ?? b._version,
_timestamp: a._timestamp ?? b._timestamp,
needUpload: b.needUpload ?? a.needUpload,
prefsStorage: _merge(cloneDeep(a.prefsStorage), b.prefsStorage),
flagStorage: _merge(a.flagStorage, b.flagStorage),
}
}
result.recent = isTest
? result.recent
: result.recent && merge(defaultState, result.recent)
result.stale = isTest
? result.stale
: result.stale && merge(defaultState, result.stale)
return result
}
export const _getAllFlags = (recent, stale) => {
return Array.from(
new Set([
...Object.keys(toRaw((recent || {}).flagStorage || {})),
...Object.keys(toRaw((stale || {}).flagStorage || {})),
]),
)
}
export const _mergeFlags = (recent, stale, allFlagKeys) => {
if (!stale.flagStorage) return recent.flagStorage
if (!recent.flagStorage) return stale.flagStorage
return Object.fromEntries(
allFlagKeys.map((flag) => {
const recentFlag = recent.flagStorage[flag]
const staleFlag = stale.flagStorage[flag]
// use flag that is of higher value
return [
flag,
Number((recentFlag > staleFlag ? recentFlag : staleFlag) || 0),
]
}),
)
}
const _mergeJournal = (...journals) => {
// Ignore invalid journal entries
const allJournals = flatten(
journals.map((j) => (Array.isArray(j) ? j : [])),
).filter(
(entry) =>
Object.hasOwn(entry, 'path') &&
Object.hasOwn(entry, 'operation') &&
Object.hasOwn(entry, 'args') &&
Object.hasOwn(entry, 'timestamp'),
)
const grouped = groupBy(allJournals, 'path')
const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => {
// side effect
journal.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1))
if (path.startsWith('collections')) {
const lastRemoveIndex = findLastIndex(
journal,
({ operation }) => operation === 'removeFromCollection',
)
// everything before last remove is unimportant
let remainder
if (lastRemoveIndex > 0) {
remainder = journal.slice(lastRemoveIndex)
} else {
// everything else doesn't need trimming
remainder = journal
}
return uniqWith(remainder, (a, b) => {
if (a.path !== b.path) {
return false
}
if (a.operation !== b.operation) {
return false
}
if (a.operation === 'addToCollection') {
return a.args[0] === b.args[0]
}
return false
})
} else if (path.startsWith('simple')) {
// Only the last record is important
return takeRight(journal)
} else {
return journal
}
})
return flatten(trimmedGrouped).sort((a, b) =>
a.timestamp > b.timestamp ? 1 : -1,
)
}
export const _mergePrefs = (recent, stale) => {
if (!stale) return recent
if (!recent) return stale
const { _journal: recentJournal, ...recentData } = recent
const { _journal: staleJournal } = stale
/** Journal entry format:
* path: path to entry in prefsStorage
* timestamp: timestamp of the change
* operation: operation type
* arguments: array of arguments, depends on operation type
*
* currently only supported operation type is "set" which just sets the value
* to requested one. Intended only to be used with simple preferences (boolean, number)
* shouldn't be used with collections!
*/
const resultOutput = { ...recentData }
const totalJournal = _mergeJournal(staleJournal, recentJournal)
totalJournal.forEach(({ path, operation, args }) => {
if (path.startsWith('_')) {
throw new Error(
`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`,
)
}
switch (operation) {
case 'set':
if (path.startsWith('collections')) {
return console.error('Illegal operation "set" on a collection')
}
if (path.split(/\./g).length <= 1) {
return console.error(
`Calling set on depth <= 1 (path: ${path}) is not allowed`,
)
}
set(resultOutput, path, args[0])
break
case 'unset':
if (path.startsWith('collections')) {
return console.error('Illegal operation "unset" on a collection')
}
if (path.split(/\./g).length <= 2) {
return console.error(
`Calling unset on depth <= 2 (path: ${path}) is not allowed`,
)
}
unset(resultOutput, path)
break
case 'addToCollection':
if (!path.startsWith('collections')) {
return console.error(
'Illegal operation "addToCollection" on a non-collection',
)
}
set(
resultOutput,
path,
Array.from(new Set(get(resultOutput, path)).add(args[0])),
)
break
case 'removeFromCollection': {
if (!path.startsWith('collections')) {
return console.error(
'Illegal operation "removeFromCollection" on a non-collection',
)
}
const newSet = new Set(get(resultOutput, path))
newSet.delete(args[0])
set(resultOutput, path, Array.from(newSet))
break
}
case 'reorderCollection': {
const [value, movement] = args
set(
resultOutput,
path,
_moveItemInArray(get(resultOutput, path), value, movement),
)
break
}
default:
return console.error(
`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`,
)
}
})
return { ...resultOutput, _journal: totalJournal }
}
export const _resetFlags = (
totalFlags,
knownKeys = defaultState.flagStorage,
) => {
let result = { ...totalFlags }
const allFlagKeys = Object.keys(totalFlags)
// flag reset functionality
if (
totalFlags.reset >= COMMAND_TRIM_FLAGS &&
totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET
) {
console.debug('Received command to trim the flags')
const knownKeysSet = new Set(Object.keys(knownKeys))
// Trim
result = {}
allFlagKeys.forEach((flag) => {
if (knownKeysSet.has(flag)) {
result[flag] = totalFlags[flag]
}
})
// Reset
if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) {
// 1001 - and reset everything to 0
console.debug('Received command to reset the flags')
Object.keys(knownKeys).forEach((flag) => {
result[flag] = 0
})
}
}
result.reset = 0
return result
}
export const _resetPrefs = (
totalPrefs,
totalFlags,
knownKeys = defaultState.flagStorage,
) => {
// prefs reset functionality
if (
totalFlags.reset >= COMMAND_WIPE_JOURNAL &&
totalFlags.reset <= COMMAND_WIPE_JOURNAL_AND_STORAGE
) {
console.debug('Received command to reset journals')
this.flagStorage.reset = COMMAND_WIPE_JOURNAL
this.prefsStorage._journal = []
this.cache.prefsStorage._journal = []
this.raw.prefsStorage._journal = []
this.pushSyncConfig()
if (totalFlags.reset === COMMAND_WIPE_JOURNAL_AND_STORAGE) {
console.debug('Received command to reset storage')
return cloneDeep(defaultState)
}
}
return totalPrefs
}
export const _doMigrations = async (data, setPreference) => {
if (data._version < VERSION) {
console.debug(
'Data has older version, seeing if there any migrations that can be applied',
)
}
if (data._version > VERSION) {
console.debug(
'Data has newer version, seeing if there any reverse migrations that can be applied',
)
// no reverse migrations right now but we leave a possibility of loading a hotpatch if need be
if (window._PLEROMA_HOTPATCH) {
if (window._PLEROMA_HOTPATCH.reverseMigrations) {
console.debug('Found hotpatch migration, applying')
return window._PLEROMA_HOTPATCH.reverseMigrations.call(
{},
'syncConfigStore',
{ from: data._version, to: VERSION },
data,
)
}
}
}
return data
}
export const useSyncConfigStore = defineStore('sync_config', {
state() {
return cloneDeep(defaultState)
},
actions: {
setFlag({ flag, value }) {
this.flagStorage[flag] = value
this.dirty = true
},
setSimplePrefAndSave({ path, value }) {
this.setPreference({ path: `simple.${path}`, value })
this.pushSyncConfig()
},
unsetSimplePrefAndSave({ path }) {
this.unsetPreference({ path: `simple.${path}` })
this.pushSyncConfig()
},
setPreference({ path, value }) {
if (path.startsWith('_')) {
throw new Error(
`Tried to edit internal (starts with _) field '${path}', ignoring.`,
)
}
if (path.startsWith('collections')) {
throw new Error(
`Invalid operation 'set' for collection field '${path}', ignoring.`,
)
}
if (path.split(/\./g).length <= 1) {
throw new Error(
`Calling set on depth <= 1 (path: ${path}) is not allowed`,
)
}
if (path.split(/\./g).length > 3) {
throw new Error(
`Calling set on depth > 3 (path: ${path}) is not allowed`,
)
}
set(this.prefsStorage, path, value)
this.prefsStorage._journal = [
...this.prefsStorage._journal,
{ operation: 'set', path, args: [value], timestamp: Date.now() },
]
this.dirty = true
},
unsetPreference({ path, value }) {
if (path.startsWith('_')) {
throw new Error(
`Tried to edit internal (starts with _) field '${path}', ignoring.`,
)
}
if (path.startsWith('collections')) {
throw new Error(
`Invalid operation 'unset' for collection field '${path}', ignoring.`,
)
}
if (path.split(/\./g).length <= 2) {
throw new Error(
`Calling unset on depth <= 2 (path: ${path}) is not allowed`,
)
}
if (path.split(/\./g).length > 3) {
throw new Error(
`Calling unset on depth > 3 (path: ${path}) is not allowed`,
)
}
unset(this.prefsStorage, path)
this.prefsStorage._journal = [
...this.prefsStorage._journal,
{ operation: 'unset', path, args: [], timestamp: Date.now() },
]
this.dirty = true
},
addCollectionPreference({ path, value }) {
if (path.startsWith('_')) {
throw new Error(
`tried to edit internal (starts with _) field '${path}'`,
)
}
if (path.startsWith('collections')) {
const collection = new Set(get(this.prefsStorage, path))
collection.add(value)
set(this.prefsStorage, path, [...collection])
}
this.prefsStorage._journal = [
...this.prefsStorage._journal,
{
operation: 'addToCollection',
path,
args: [value],
timestamp: Date.now(),
},
]
this.dirty = true
},
removeCollectionPreference({ path, value }) {
if (path.startsWith('_')) {
throw new Error(
`tried to edit internal (starts with _) field '${path}', ignoring.`,
)
}
const { _key } = value
if (path.startsWith('collection')) {
const collection = new Set(get(this.prefsStorage, path))
collection.delete(value)
set(this.prefsStorage, path, [...collection])
this.prefsStorage._journal = [
...this.prefsStorage._journal,
{
operation: 'removeFromCollection',
path,
args: [value],
timestamp: Date.now(),
},
]
this.dirty = true
}
},
reorderCollectionPreference({ path, value, movement }) {
if (path.startsWith('_')) {
throw new Error(
`tried to edit internal (starts with _) field '${path}', ignoring.`,
)
}
const collection = get(this.prefsStorage, path)
const newCollection = _moveItemInArray(collection, value, movement)
set(this.prefsStorage, path, newCollection)
this.prefsStorage._journal = [
...this.prefsStorage._journal,
{
operation: 'arrangeCollection',
path,
args: [value],
timestamp: Date.now(),
},
]
this.dirty = true
},
updateCache({ username }) {
this.prefsStorage._journal = _mergeJournal(this.prefsStorage._journal)
this.cache = _wrapData(
{
flagStorage: toRaw(this.flagStorage),
prefsStorage: toRaw(this.prefsStorage),
},
username,
)
},
clearSyncConfig() {
const blankState = { ...cloneDeep(defaultState) }
Object.keys(this).forEach((k) => {
this[k] = blankState[k]
})
this.flagStorage.reset = COMMAND_WIPE_JOURNAL_AND_STORAGE
},
async initSyncConfig(userData) {
const live = userData.storage
this.raw = live
let cache = this.cache
if (cache?._user !== userData.fqn) {
console.warn(
'Cache belongs to another user! reinitializing local cache!',
)
cache = null
}
let { recent, stale, needUpload } = _getRecentData(cache, live)
const userNew = userData.created_at > NEW_USER_DATE
const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage
let dirty = false
console.debug('Migrating from old config')
- const vuexState = await storage.getItem('vuex-lz') ?? {}
+ const vuexState = (await storage.getItem('vuex-lz')) ?? {}
vuexState.config = vuexState.config ?? {}
const migratedEntries = new Set(vuexState.config._syncMigration ?? [])
console.debug(`Already migrated Values: ${[...migratedEntries].join()}`)
Object.entries(defaultConfigSync).forEach(([key, value]) => {
const oldValue = vuexState.config[key]
const defaultValue = value
const present = oldValue !== undefined
const migrated = migratedEntries.has(key)
const different = !isEqual(oldValue, defaultValue)
if (present && !migrated && different) {
- console.debug(`Migrating config ${key}: ${oldValue}`,)
+ console.debug(`Migrating config ${key}: ${oldValue}`)
this.setPreference({ path: `simple.${key}`, oldValue })
migratedEntries.add(key)
needUpload = true
}
})
vuexState.config._syncMigration = [...migratedEntries]
storage.setItem('vuex-lz', vuexState)
if (recent === null) {
console.debug(
`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`,
)
recent = _wrapData({
flagStorage: { ...flagsTemplate },
prefsStorage: { ...defaultState.prefsStorage },
})
}
recent = recent && (await _doMigrations(recent, this.setPreference))
stale = stale && (await _doMigrations(stale, this.setPreference))
if (!needUpload && recent && stale) {
console.debug('Checking if data needs merging...')
// discarding timestamps and versions
const { _timestamp: _0, _version: _1, ...recentData } = recent
const { _timestamp: _2, _version: _3, ...staleData } = stale
dirty = !isEqual(
// Something wrong happens if we compare both objects directly
// or with cloneDeep()
JSON.parse(JSON.stringify(recentData)),
JSON.parse(JSON.stringify(staleData)),
)
console.debug(`Data ${dirty ? 'needs' : "doesn't need"} merging`)
}
const allFlagKeys = _getAllFlags(recent, stale)
let totalFlags
let totalPrefs
if (dirty) {
// Merge the flags
console.debug('Merging the data...')
totalFlags = _mergeFlags(recent, stale, allFlagKeys)
_verifyPrefs(recent)
_verifyPrefs(stale)
totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage)
} else {
totalFlags = recent.flagStorage
totalPrefs = recent.prefsStorage
}
totalPrefs = _resetPrefs(totalPrefs, totalFlags)
totalFlags = _resetFlags(totalFlags)
recent.flagStorage = { ...flagsTemplate, ...totalFlags }
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
this.dirty = dirty || needUpload
this.cache = recent
// set local timestamp to smaller one if we don't have any changes
if (stale && recent && !this.dirty) {
this.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
}
this.flagStorage = this.cache.flagStorage
this.prefsStorage = this.cache.prefsStorage
this.pushSyncConfig()
},
pushSyncConfig({ force = false } = {}) {
const needPush = this.dirty || force
if (!needPush) return
this.updateCache({ username: window.vuex.state.users.currentUser.fqn })
const params = { pleroma_settings_store: { 'pleroma-fe': this.cache } }
window.vuex.state.api.backendInteractor
.updateProfileJSON({ params })
.then((user) => {
this.initSyncConfig(user)
this.dirty = false
})
},
},
getters: {
mergedConfig: (state) => {
const instancePrefs = useInstanceStore().prefsStorage
const localPrefs = useLocalConfigStore().prefsStorage
const tempPrefs = useLocalConfigStore().tempStorage
const result = Object.fromEntries(
Object.entries(state.prefsStorage.simple).map(([k, value]) => [
k,
LOCAL_ONLY_KEYS.has(k)
- ? tempPrefs[k] ?? localPrefs[k] ?? instancePrefs[k]
- : tempPrefs[k] ?? localPrefs[k] ?? value ?? instancePrefs[k],
+ ? (tempPrefs[k] ??
+ localPrefs[k] ??
+ instancePrefs[k] ??
+ defaultConfigLocal[k])
+ : (tempPrefs[k] ??
+ localPrefs[k] ??
+ value ??
+ instancePrefs[k] ??
+ instanceDefaultConfig[k]),
]),
)
return result
},
},
persist: {
afterLoad(state) {
return state
},
},
})
diff --git a/src/stores/user_highlight.js b/src/stores/user_highlight.js
index 6a3badd01b..ae2bd4d4ca 100644
--- a/src/stores/user_highlight.js
+++ b/src/stores/user_highlight.js
@@ -1,359 +1,359 @@
import {
merge as _merge,
clamp,
clone,
cloneDeep,
findLastIndex,
flatten,
get,
groupBy,
isEqual,
takeRight,
uniqWith,
} from 'lodash'
import { defineStore } from 'pinia'
import { toRaw } from 'vue'
import { storage } from 'src/lib/storage.js'
import { defaultState as configDefaultState } from 'src/modules/default_config_state'
export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
export const defaultState = {
// do we need to update data on server?
dirty: false,
highlight: {
_journal: [],
},
// raw data
raw: null,
// local cache
cache: null,
}
export const _moveItemInArray = (array, value, movement) => {
const oldIndex = array.indexOf(value)
const newIndex = oldIndex + movement
const newArray = [...array]
// remove old
newArray.splice(oldIndex, 1)
// add new
newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value)
return newArray
}
const _wrapData = (data, userName) => {
return {
...data,
_user: userName,
_timestamp: Date.now(),
}
}
const _checkValidity = (data) => data._timestamp > 0
const _verifyHighlights = (state) => {
state.highlight = state.highlight || {}
// Simple
Object.entries(defaultState.highlight).forEach(([k, v]) => {
if (typeof v === 'undefined') return
if (typeof v === 'object') return
console.warn(`User highlight ${k} is invalid type ${typeof v}, unsetting`)
delete state.highlight[k]
})
}
export const _getRecentData = (cache, live, isTest) => {
const result = { recent: null, stale: null, needUpload: false }
const cacheValid = _checkValidity(cache || {})
const liveValid = _checkValidity(live || {})
if (!liveValid && cacheValid) {
result.needUpload = true
console.debug(
'[HIGHLIGHT] Nothing valid stored on server, assuming cache to be source of truth',
)
result.recent = cache
result.stale = live
} else if (!cacheValid && liveValid) {
console.debug(
'[HIGHLIGHT] Valid storage on server found, no local cache found, using live as source of truth',
)
result.recent = live
result.stale = cache
} else if (cacheValid && liveValid) {
console.debug(
'[HIGHLIGHT] Both sources have valid data, figuring things out...',
)
if (live._timestamp === cache._timestamp) {
console.debug(
'[HIGHLIGHT] Same timestamp on both sources, source of truth irrelevant',
)
result.recent = cache
result.stale = live
} else {
console.debug(
'[HIGHLIGHT] Different timestamp, figuring out which one is more recent',
)
if (live._timestamp < cache._timestamp) {
result.recent = cache
result.stale = live
} else {
result.recent = live
result.stale = cache
}
}
} else {
console.debug('[HIGHLIGHT] Both sources are invalid, start from scratch')
result.needUpload = true
}
const merge = (a, b) => {
return {
_user: a._user ?? b._user,
_timestamp: a._timestamp ?? b._timestamp,
needUpload: b.needUpload ?? a.needUpload,
highlight: _merge(cloneDeep(a.highlight), b.highlight),
}
}
result.recent = isTest
? result.recent
: result.recent && merge(defaultState, result.recent)
result.stale = isTest
? result.stale
: result.stale && merge(defaultState, result.stale)
return result
}
const _mergeJournal = (...journals) => {
// Ignore invalid journal entries
const allJournals = flatten(
journals.map((j) => (Array.isArray(j) ? j : [])),
).filter(
(entry) =>
Object.hasOwn(entry, 'user') &&
Object.hasOwn(entry, 'operation') &&
Object.hasOwn(entry, 'args') &&
Object.hasOwn(entry, 'timestamp'),
)
const grouped = groupBy(allJournals, 'user')
const trimmedGrouped = Object.entries(grouped).map(([user, journal]) => {
// side effect
journal.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1))
return takeRight(journal)
})
return flatten(trimmedGrouped).sort((a, b) =>
a.timestamp > b.timestamp ? 1 : -1,
)
}
export const _mergeHighlights = (recent, stale) => {
if (!stale) return recent
if (!recent) return stale
const { _journal: recentJournal, ...recentData } = recent
const { _journal: staleJournal } = stale
/** Journal entry format:
* user: user to entry in highlight storage
* timestamp: timestamp of the change
* operation: operation type
* arguments: array of arguments, depends on operation type
*
* currently only supported operation type is "set" which just sets the value
* to requested one. Intended only to be used with simple preferences (boolean, number)
*/
const resultOutput = { ...recentData }
const Journal = _mergeJournal(staleJournal, recentJournal)
Journal.forEach(({ user, operation, args }) => {
if (user.startsWith('_')) {
throw new Error(
`journal contains entry to edit internal (starts with _) field '${user}', something is incorrect here, ignoring.`,
)
}
switch (operation) {
case 'set':
resultOutput[user] = args[0]
break
case 'unset':
delete resultOutput[user]
break
default:
return console.error(`Unknown journal operation: '${operation}'`)
}
})
return { ...resultOutput, _journal: Journal }
}
export const useUserHighlightStore = defineStore('user_highlight', {
state() {
return cloneDeep(defaultState)
},
actions: {
setAndSave({ user, value }) {
this.set({ user, value })
this.pushHighlight()
},
unsetAndSave({ user }) {
this.unset({ user })
this.pushHighlight()
},
get(user) {
const present = this.highlight[user] || {}
return {
user,
type: 'disabled',
color: '#FFFFFF',
...present,
}
},
set({ user, value }) {
if (user.startsWith('_')) {
throw new Error(
`Tried to edit internal (starts with _) field '${user}', ignoring.`,
)
}
const oldValue = this.highlight[user]
const newValue = {
user,
...oldValue,
...value,
}
this.highlight[user] = newValue
this.highlight._journal = [
...this.highlight._journal,
{ operation: 'set', user, args: [newValue], timestamp: Date.now() },
]
this.dirty = true
},
unset({ user, value }) {
if (user.startsWith('_')) {
throw new Error(
`Tried to edit internal (starts with _) field '${user}', ignoring.`,
)
}
delete this.highlight[user]
this.highlight._journal = [
...this.highlight._journal,
{ operation: 'unset', user, args: [], timestamp: Date.now() },
]
this.dirty = true
},
updateCache({ username }) {
this.highlight._journal = _mergeJournal(this.highlight._journal)
this.cache = _wrapData(
{
highlight: toRaw(this.highlight),
},
username,
)
},
clearSyncConfig() {
const blankState = { ...cloneDeep(defaultState) }
Object.keys(this).forEach((k) => {
this[k] = blankState[k]
})
},
clearJournals() {
this.highlight._journal = []
this.cache.highlight._journal = []
this.raw.highlight._journal = []
this.pushSyncConfig()
},
async initUserHighlight(userData) {
const live = userData.user_highlight
this.raw = live
let cache = this.cache
if (cache?._user !== userData.fqn) {
console.warn(
'Cache belongs to another user! reinitializing local cache!',
)
cache = null
}
let { recent, stale, needUpload } = _getRecentData(cache, live)
const userNew = userData.created_at > NEW_USER_DATE
let dirty = false
- const vuexState = await storage.getItem('vuex-lz') ?? {}
+ const vuexState = (await storage.getItem('vuex-lz')) ?? {}
vuexState.config = vuexState.config ?? {}
const highlight = vuexState.config.highlight ?? {}
Object.entries(highlight).forEach(([user, value]) => {
if ((highlight[user]._migrated || 0) < 1) {
dirty = true
needUpload = true
this.set({ user, value: clone(value) })
vuexState.config.highlight[user]._migrated = 1
console.debug(
`[HIGHLIGHT] Migrating user ${user}: ${JSON.stringify(value)}`,
)
}
})
storage.setItem('vuex-lz', vuexState)
if (recent === null) {
console.debug(
`[HIGHLIGHT] Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`,
)
recent = _wrapData({
highlight: { ...defaultState.highlight },
})
}
if (!needUpload && recent && stale) {
console.debug('[HIGHLIGHT] Checking if data needs merging...')
// discarding timestamps
const { _timestamp: _0, ...recentData } = recent
const { _timestamp: _2, ...staleData } = stale
dirty = !isEqual(recentData, staleData)
console.debug(
`[HIGHLIGHT] Data ${dirty ? 'needs' : "doesn't need"} merging`,
)
}
let highlights
if (dirty) {
console.debug('[HIGHLIGHT] Merging the data...')
_verifyHighlights(recent)
_verifyHighlights(stale)
highlights = _mergeHighlights(recent.highlight, stale.highlight)
} else {
highlights = recent.highlight
}
recent.highlight = { ...defaultState.highlight, ...highlights }
this.dirty = dirty || needUpload
this.cache = recent
// set local timestamp to smaller one if we don't have any changes
if (stale && recent && !this.dirty) {
this.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
}
this.highlight = this.cache.highlight
this.pushHighlight()
},
pushHighlight({ force = false } = {}) {
const needPush = this.dirty || force
if (!needPush) return
this.updateCache({ username: window.vuex.state.users.currentUser.fqn })
const params = {
pleroma_settings_store: { user_highlight: this.cache },
}
window.vuex.state.api.backendInteractor
.updateProfileJSON({ params })
.then((user) => {
this.initUserHighlight(user)
this.dirty = false
})
},
},
persist: {
afterLoad(state) {
return state
},
},
})

File Metadata

Mime Type
text/x-diff
Expires
Mon, Oct 12, 2:36 AM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786022
Default Alt Text
(50 KB)

Event Timeline