Page MenuHomePhorge

No OneTemporary

Size
56 KB
Referenced Files
None
Subscribers
None
diff --git a/package.json b/package.json
index 10c0ce613f..cb5378a864 100644
--- a/package.json
+++ b/package.json
@@ -1,121 +1,122 @@
{
"name": "pleroma_fe",
"version": "2.7.1",
"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,
"scripts": {
"dev": "node build/update-emoji.js && vite dev",
"build": "node build/update-emoji.js && vite build",
"unit": "node build/update-emoji.js && vitest --run",
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
"unit:watch": "node build/update-emoji.js && vitest",
"e2e": "node test/e2e/runner.js",
"test": "yarn run unit && yarn run e2e",
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
"lint": "eslint src test/unit/specs test/e2e/specs",
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
},
"dependencies": {
"@babel/runtime": "7.26.10",
"@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.0.8",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
"@vuelidate/core": "2.0.3",
"@vuelidate/validators": "2.0.4",
"@web3-storage/parse-link-header": "^3.1.0",
"body-scroll-lock": "3.1.5",
"chromatism": "3.0.0",
"click-outside-vue3": "4.0.1",
"cropperjs": "1.6.2",
"escape-html": "1.0.3",
"globals": "^16.0.0",
"hash-sum": "^2.0.0",
"js-cookie": "3.0.5",
"localforage": "1.10.0",
"parse-link-header": "2.0.0",
"phoenix": "1.7.20",
"pinia": "^2.0.33",
"punycode.js": "2.3.1",
"qrcode": "1.5.4",
"querystring-es3": "0.2.1",
"url": "0.11.4",
"utf8": "3.0.0",
+ "uuid": "8.3.2",
"vue": "3.5.13",
"vue-i18n": "10",
"vue-router": "4.5.0",
"vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0"
},
"devDependencies": {
"@babel/core": "7.26.10",
"@babel/eslint-parser": "7.26.10",
"@babel/plugin-transform-runtime": "7.26.10",
"@babel/preset-env": "7.26.9",
"@babel/register": "7.25.9",
"@ungap/event-target": "0.2.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vitest/browser": "^3.0.7",
"@vitest/ui": "^3.0.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.4.0",
"@vue/compiler-sfc": "3.5.13",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.21",
"babel-plugin-lodash": "3.3.4",
"chai": "4.5.0",
"chalk": "5.4.1",
"chromedriver": "134.0.3",
"connect-history-api-fallback": "2.0.0",
"cross-spawn": "7.0.6",
"custom-event-polyfill": "1.0.7",
"eslint": "9.22.0",
"eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-n": "17.16.2",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "9.33.0",
"eventsource-polyfill": "0.9.6",
"express": "4.21.2",
"function-bind": "1.1.2",
"http-proxy-middleware": "3.0.3",
"iso-639-1": "3.1.5",
"lodash": "4.17.21",
"msw": "2.7.3",
"nightwatch": "3.12.0",
"ora": "0.4.1",
"playwright": "1.49.1",
"postcss": "8.5.3",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
"sass": "1.85.1",
"selenium-server": "3.141.59",
"semver": "7.7.1",
"serve-static": "1.16.2",
"shelljs": "0.9.1",
"sinon": "15.2.0",
"sinon-chai": "3.7.0",
"stylelint": "14.16.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended-scss": "^8.0.0",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "29.0.0",
"stylelint-rscss": "0.4.0",
"vite": "^6.1.0",
"vite-plugin-eslint2": "^5.0.3",
"vite-plugin-stylelint": "^6.0.0",
"vitest": "^3.0.7"
},
"type": "module",
"engines": {
"node": ">= 16.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index e6ed05f280..878d95aa8c 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -1,253 +1,252 @@
import merge from 'lodash.merge'
import { each, get, set, cloneDeep } from 'lodash'
import { useInterfaceStore } from 'src/stores/interface'
import { storage } from './storage.js'
let loaded = false
const defaultReducer = (state, paths) => (
paths.length === 0
? state
: paths.reduce((substate, path) => {
set(substate, path, get(state, path))
return substate
}, {})
)
const saveImmedeatelyActions = [
'markNotificationsAsSeen',
'clearCurrentUser',
'setCurrentUser',
- 'setServerSideStorage',
'setHighlight',
'setOption',
'setClientData',
'setToken',
'clearToken'
]
const defaultStorage = (() => {
return storage
})()
export default function createPersistedState ({
key = 'vuex-lz',
paths = [],
getState = (key, storage) => {
const value = storage.getItem(key)
return value
},
setState = (key, state, storage) => {
if (!loaded) {
console.info('waiting for old state to be loaded...')
return Promise.resolve()
} else {
return storage.setItem(key, state)
}
},
reducer = defaultReducer,
storage = defaultStorage,
subscriber = store => handler => store.subscribe(handler)
} = {}) {
return getState(key, storage).then((savedState) => {
return store => {
try {
if (savedState !== null && typeof savedState === 'object') {
// build user cache
const usersState = savedState.users || {}
usersState.usersObject = {}
const users = usersState.users || []
each(users, (user) => { usersState.usersObject[user.id] = user })
savedState.users = usersState
store.replaceState(
merge({}, store.state, savedState)
)
}
loaded = true
} catch (e) {
console.error("Couldn't load state")
console.error(e)
loaded = true
}
subscriber(store)((mutation, state) => {
try {
if (saveImmedeatelyActions.includes(mutation.type)) {
setState(key, reducer(cloneDeep(state), paths), storage)
.then(success => {
if (typeof success !== 'undefined') {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
useInterfaceStore().settingsSaved({ success })
}
}
}, error => {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
useInterfaceStore().settingsSaved({ error })
}
})
}
} catch (e) {
console.error("Couldn't persist state:")
console.error(e)
}
})
}
})
}
/**
* This persists state for pinia, which falls back to read from the vuex state
* if pinia persisted state does not exist.
*
* When you migrate a module from vuex to pinia, you have to keep the original name.
* If the module was called `xxx`, the name of the store has to be `xxx` too.
*
* This adds one property to the store, $persistLoaded, which is a promise
* that resolves when the initial state is loaded. If the plugin is not enabled,
* $persistLoaded is a promise that resolves immediately.
* If we are not able to get the stored state because storage.getItem() throws or
* rejects, $persistLoaded will be a rejected promise with the thrown error.
*
* Call signature:
*
* defineStore(name, {
* ...,
* // setting the `persist` property enables this plugin
* // IMPORTANT: by default it is disabled, you have to set `persist` to at least an empty object
* persist: {
* // set to list of individual paths, or undefined/unset to persist everything
* paths: [],
* // function to call after loading initial state
* // if afterLoad is a function, it must return a state object that will be sent to `store.$patch`, or a promise to the state object
* // by default afterLoad is undefined
* afterLoad: (originalState) => {
* // ...
* return modifiedState
* },
* // if it exists, only persist state after these actions
* // if it doesn't exist or is undefined, persist state after every mutation of the state
* saveImmediatelyActions: [],
* // what to do after successfully saving the state
* onSaveSuccess: () => {},
* // what to do after there is an error saving the state
* onSaveError: () => {}
* }
* })
*
*/
export const piniaPersistPlugin = ({
vuexKey = 'vuex-lz',
keyFunction = (id) => `pinia-local-${id}`,
storage = defaultStorage,
reducer = defaultReducer
} = {}) => ({ store, options }) => {
if (!options.persist) {
return {
$persistLoaded: Promise.resolve()
}
}
let resolveLoaded
let rejectLoaded
const loadedPromise = new Promise((resolve, reject) => {
resolveLoaded = resolve
rejectLoaded = reject
})
const {
afterLoad,
paths = [],
saveImmediatelyActions,
onSaveSuccess = () => {},
onSaveError = () => {}
} = options.persist || {}
const loadedGuard = { loaded: false }
const key = keyFunction(store.$id)
const getState = async () => {
const id = store.$id
const value = await storage.getItem(key)
if (value) {
return value
}
const fallbackValue = await storage.getItem(vuexKey)
if (fallbackValue && fallbackValue[id]) {
console.info(`Migrating ${id} store data from vuex to pinia`)
const res = fallbackValue[id]
await storage.setItem(key, res)
return res
}
return {}
}
const setState = (state) => {
if (!loadedGuard.loaded) {
console.info('waiting for old state to be loaded...')
return Promise.reject()
} else {
return storage.setItem(key, state)
}
}
const getMaybeAugmentedState = async () => {
const savedRawState = await getState()
if (typeof afterLoad === 'function') {
try {
return await afterLoad(savedRawState)
} catch (e) {
console.error('Error running afterLoad:', e)
return savedRawState
}
} else {
return savedRawState
}
}
const persistCurrentState = async (state) => {
const stateClone = cloneDeep(state)
const stateToPersist = reducer(stateClone, paths)
try {
const res = await setState(stateToPersist)
onSaveSuccess(res)
} catch (e) {
console.error('Cannot persist state:', e)
onSaveError(e)
}
}
getMaybeAugmentedState()
.then(savedState => {
if (savedState) {
store.$patch(savedState)
}
loadedGuard.loaded = true
resolveLoaded()
// only subscribe after we have done setting the initial state
if (!saveImmediatelyActions) {
store.$subscribe(async (_mutation, state) => {
await persistCurrentState(state)
})
} else {
store.$onAction(({
name,
store,
after,
}) => {
if (saveImmediatelyActions.includes(name)) {
after(() => persistCurrentState(store.$state))
}
})
}
}, error => {
console.error('Cannot load storage:', error)
rejectLoaded(error)
})
return {
$persistLoaded: loadedPromise
}
}
diff --git a/src/modules/users.js b/src/modules/users.js
index f6c05ca219..6a25ba832f 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,699 +1,732 @@
+import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
+import { v4 as uuidv4 } from 'uuid';
+
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import apiService from '../services/api/api.service.js'
import oauthApi from '../services/new_api/oauth.js'
-import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useOAuthStore } from 'src/stores/oauth.js'
-import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
+import { useServerSideStorageStore, CONFIG_MIGRATION } from 'src/stores/serverSideStorage'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
if (!item) { return false }
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
mergeWith(oldItem, item, mergeArrayLength)
return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
arr.push(item)
obj[item.id] = item
return { item, new: true }
}
}
const mergeArrayLength = (oldValue, newValue) => {
if (isArray(oldValue) && isArray(newValue)) {
oldValue.length = newValue.length
return mergeWith(oldValue, newValue, mergeArrayLength)
}
}
const getNotificationPermission = () => {
const Notification = window.Notification
if (!Notification) return Promise.resolve(null)
if (Notification.permission === 'default') return Notification.requestPermission()
return Promise.resolve(Notification.permission)
}
const blockUser = (store, id) => {
return store.rootState.api.backendInteractor.blockUser({ id })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
store.commit('removeStatus', { timeline: 'friends', userId: id })
store.commit('removeStatus', { timeline: 'public', userId: id })
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
})
}
const unblockUser = (store, id) => {
return store.rootState.api.backendInteractor.unblockUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const removeUserFromFollowers = (store, id) => {
return store.rootState.api.backendInteractor.removeUserFromFollowers({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const editUserNote = (store, { id, comment }) => {
return store.rootState.api.backendInteractor.editUserNote({ id, comment })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const muteUser = (store, args) => {
const id = typeof args === 'object' ? args.id : args
const expiresIn = typeof args === 'object' ? args.expiresIn : 0
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.muting = true
store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addMuteId', id)
return store.rootState.api.backendInteractor.muteUser({ id, expiresIn })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
})
}
const unmuteUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.muting = false
store.commit('updateUserRelationship', [predictedRelationship])
return store.rootState.api.backendInteractor.unmuteUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const hideReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
})
}
const showReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const muteDomain = (store, domain) => {
return store.rootState.api.backendInteractor.muteDomain({ domain })
.then(() => store.commit('addDomainMute', domain))
}
const unmuteDomain = (store, domain) => {
return store.rootState.api.backendInteractor.unmuteDomain({ domain })
.then(() => store.commit('removeDomainMute', domain))
}
export const mutations = {
tagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.concat([tag])
user.tags = newTags
},
untagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.filter(t => t !== tag)
user.tags = newTags
},
updateRight (state, { user: { id }, right, value }) {
const user = state.usersObject[id]
const newRights = user.rights
newRights[right] = value
user.rights = newRights
},
updateActivationStatus (state, { user: { id }, deactivated }) {
const user = state.usersObject[id]
user.deactivated = deactivated
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
},
clearCurrentUser (state) {
state.currentUser = false
state.lastLoginName = false
},
beginLogin (state) {
state.loggingIn = true
},
endLogin (state) {
state.loggingIn = false
},
saveFriendIds (state, { id, friendIds }) {
const user = state.usersObject[id]
user.friendIds = uniq(concat(user.friendIds || [], friendIds))
},
saveFollowerIds (state, { id, followerIds }) {
const user = state.usersObject[id]
user.followerIds = uniq(concat(user.followerIds || [], followerIds))
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
clearFriends (state, userId) {
const user = state.usersObject[userId]
if (user) {
user.friendIds = []
}
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (user) {
user.followerIds = []
}
},
addNewUsers (state, users) {
each(users, (user) => {
if (user.relationship) {
state.relationships[user.relationship.id] = user.relationship
}
const res = mergeOrAdd(state.users, state.usersObject, user)
const item = res.item
if (res.new && item.screen_name && !item.screen_name.includes('@')) {
state.usersByNameObject[item.screen_name.toLowerCase()] = item
}
})
},
updateUserRelationship (state, relationships) {
relationships.forEach((relationship) => {
state.relationships[relationship.id] = relationship
})
},
updateUserInLists (state, { id, inLists }) {
state.usersObject[id].inLists = inLists
},
saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds
},
addBlockId (state, blockId) {
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
state.currentUser.blockIds.push(blockId)
}
},
setBlockIdsMaxId (state, blockIdsMaxId) {
state.currentUser.blockIdsMaxId = blockIdsMaxId
},
saveMuteIds (state, muteIds) {
state.currentUser.muteIds = muteIds
},
setMuteIdsMaxId (state, muteIdsMaxId) {
state.currentUser.muteIdsMaxId = muteIdsMaxId
},
addMuteId (state, muteId) {
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
state.currentUser.muteIds.push(muteId)
}
},
saveDomainMutes (state, domainMutes) {
state.currentUser.domainMutes = domainMutes
},
addDomainMute (state, domain) {
if (state.currentUser.domainMutes.indexOf(domain) === -1) {
state.currentUser.domainMutes.push(domain)
}
},
removeDomainMute (state, domain) {
const index = state.currentUser.domainMutes.indexOf(domain)
if (index !== -1) {
state.currentUser.domainMutes.splice(index, 1)
}
},
setPinnedToUser (state, status) {
const user = state.usersObject[status.user.id]
user.pinnedStatusIds = user.pinnedStatusIds || []
const index = user.pinnedStatusIds.indexOf(status.id)
if (status.pinned && index === -1) {
user.pinnedStatusIds.push(status.id)
} else if (!status.pinned && index !== -1) {
user.pinnedStatusIds.splice(index, 1)
}
},
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
if (notification.type !== 'follow') {
notification.action.user = state.usersObject[notification.action.user.id]
}
notification.from_profile = state.usersObject[notification.from_profile.id]
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
user.highlight = highlighted
},
signUpPending (state) {
state.signUpPending = true
state.signUpErrors = []
state.signUpNotice = {}
},
signUpSuccess (state) {
state.signUpPending = false
},
signUpFailure (state, errors) {
state.signUpPending = false
state.signUpErrors = errors
state.signUpNotice = {}
},
signUpNotice (state, notice) {
state.signUpPending = false
state.signUpErrors = []
state.signUpNotice = notice
}
}
export const getters = {
findUser: state => query => {
return state.usersObject[query]
},
findUserByName: state => query => {
return state.usersByNameObject[query.toLowerCase()]
},
findUserByUrl: state => query => {
return state.users
.find(u => u.statusnet_profile_url &&
u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
},
relationship: state => id => {
const rel = id && state.relationships[id]
return rel || { id, loading: true }
}
}
export const defaultState = {
loggingIn: false,
lastLoginName: false,
currentUser: false,
users: [],
usersObject: {},
usersByNameObject: {},
signUpPending: false,
signUpErrors: [],
signUpNotice: {},
relationships: {}
}
const users = {
state: defaultState,
mutations,
getters,
actions: {
fetchUserIfMissing (store, id) {
if (!store.getters.findUser(id)) {
store.dispatch('fetchUser', id)
}
},
fetchUser (store, id) {
return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserByName (store, name) {
return store.rootState.api.backendInteractor.fetchUserByName({ name })
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserRelationship (store, id) {
if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserRelationship({ id })
.then((relationships) => store.commit('updateUserRelationship', relationships))
}
},
fetchUserInLists (store, id) {
if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserInLists({ id })
.then((inLists) => store.commit('updateUserInLists', { id, inLists }))
}
},
fetchBlocks (store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.blockIdsMaxId
return store.rootState.api.backendInteractor.fetchBlocks({ maxId })
.then((blocks) => {
if (reset) {
store.commit('saveBlockIds', map(blocks, 'id'))
} else {
map(blocks, 'id').map(id => store.commit('addBlockId', id))
}
if (blocks.length) {
store.commit('setBlockIdsMaxId', last(blocks).id)
}
store.commit('addNewUsers', blocks)
return blocks
})
},
blockUser (store, id) {
return blockUser(store, id)
},
unblockUser (store, id) {
return unblockUser(store, id)
},
removeUserFromFollowers (store, id) {
return removeUserFromFollowers(store, id)
},
blockUsers (store, ids = []) {
return Promise.all(ids.map(id => blockUser(store, id)))
},
unblockUsers (store, ids = []) {
return Promise.all(ids.map(id => unblockUser(store, id)))
},
editUserNote (store, args) {
return editUserNote(store, args)
},
fetchMutes (store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.muteIdsMaxId
return store.rootState.api.backendInteractor.fetchMutes({ maxId })
.then((mutes) => {
if (reset) {
store.commit('saveMuteIds', map(mutes, 'id'))
} else {
map(mutes, 'id').map(id => store.commit('addMuteId', id))
}
if (mutes.length) {
store.commit('setMuteIdsMaxId', last(mutes).id)
}
store.commit('addNewUsers', mutes)
return mutes
})
},
muteUser (store, id) {
return muteUser(store, id)
},
unmuteUser (store, id) {
return unmuteUser(store, id)
},
hideReblogs (store, id) {
return hideReblogs(store, id)
},
showReblogs (store, id) {
return showReblogs(store, id)
},
muteUsers (store, ids = []) {
return Promise.all(ids.map(id => muteUser(store, id)))
},
unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id)))
},
fetchDomainMutes (store) {
return store.rootState.api.backendInteractor.fetchDomainMutes()
.then((domainMutes) => {
store.commit('saveDomainMutes', domainMutes)
return domainMutes
})
},
muteDomain (store, domain) {
return muteDomain(store, domain)
},
unmuteDomain (store, domain) {
return unmuteDomain(store, domain)
},
muteDomains (store, domains = []) {
return Promise.all(domains.map(domain => muteDomain(store, domain)))
},
unmuteDomains (store, domain = []) {
return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
},
fetchFriends ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
return rootState.api.backendInteractor.fetchFriends({ id, maxId })
.then((friends) => {
commit('addNewUsers', friends)
commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
return friends
})
},
fetchFollowers ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.followerIds)
return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
.then((followers) => {
commit('addNewUsers', followers)
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers
})
},
clearFriends ({ commit }, userId) {
commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
},
subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.followUser({ id, notify: true })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
unsubscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.followUser({ id, notify: false })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
toggleActivationStatus ({ rootState, commit }, { user }) {
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
api({ user })
.then((user) => { const deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey
const isEnabled = store.rootState.config.webPushNotifications
const notificationVisibility = store.rootState.config.notificationVisibility
registerPushNotifications(isEnabled, vapidPublicKey, token, notificationVisibility)
},
unregisterPushNotifications (store) {
const token = store.state.currentUser.credentials
unregisterPushNotifications(token)
},
addNewUsers ({ commit }, users) {
commit('addNewUsers', users)
},
addNewStatuses (store, { statuses }) {
const users = map(statuses, 'user')
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
store.commit('addNewUsers', users)
store.commit('addNewUsers', retweetedUsers)
each(statuses, (status) => {
// Reconnect users to statuses
store.commit('setUserForStatus', status)
// Set pinned statuses to user
store.commit('setPinnedToUser', status)
})
each(compact(map(statuses, 'retweeted_status')), (status) => {
// Reconnect users to retweets
store.commit('setUserForStatus', status)
// Set pinned retweets to user
store.commit('setPinnedToUser', status)
})
},
addNewNotifications (store, { notifications }) {
const users = map(notifications, 'from_profile')
const targetUsers = map(notifications, 'target').filter(_ => _)
const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
.filter(([k]) => notificationIds.includes(k))
.map(([, val]) => val)
// Reconnect users to notifications
each(relevantNotifications, (notification) => {
store.commit('setUserForNotification', notification)
})
},
searchUsers ({ rootState, commit }, { query }) {
return rootState.api.backendInteractor.searchUsers({ query })
.then((users) => {
commit('addNewUsers', users)
return users
})
},
async signUp (store, userInfo) {
const oauthStore = useOAuthStore()
store.commit('signUpPending')
try {
const token = await oauthStore.ensureAppToken()
const data = await apiService.register(
{ credentials: token, params: { ...userInfo } }
)
if (data.access_token) {
store.commit('signUpSuccess')
oauthStore.setToken(data.access_token)
store.dispatch('loginUser', data.access_token)
return 'ok'
} else { // Request succeeded, but user cannot login yet.
store.commit('signUpNotice', data)
return 'request_sent'
}
} catch (e) {
const errors = e.message
store.commit('signUpFailure', errors)
throw e
}
},
async getCaptcha (store) {
return store.rootState.api.backendInteractor.getCaptcha()
},
logout (store) {
const oauth = useOAuthStore()
const { instance } = store.rootState
// NOTE: No need to verify the app still exists, because if it doesn't,
// the token will be invalid too
return oauth.ensureApp()
.then((app) => {
const params = {
app,
instance: instance.server,
token: oauth.userToken
}
return oauthApi.revokeToken(params)
})
.then(() => {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
oauth.clearToken()
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingLists')
store.dispatch('stopFetchingBookmarkFolders')
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
store.dispatch('resetChats')
useInterfaceStore().setLastTimeline('public-timeline')
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
store.commit('clearServerSideStorage')
})
},
loginUser (store, accessToken) {
return new Promise((resolve, reject) => {
const commit = store.commit
const dispatch = store.dispatch
commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
.then((data) => {
if (!data.error) {
const user = data
// user.credentials = userCredentials
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
user.domainMutes = []
commit('setCurrentUser', user)
useServerSideStorageStore().setServerSideStorage(user)
commit('addNewUsers', [user])
dispatch('fetchEmoji')
getNotificationPermission()
.then(permission => useInterfaceStore().setNotificationPermission(permission))
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
+
+ // Do server-side storage migrations
+
+ const { configMigration } = useServerSideStorageStore().flagStorage
+
+ // Wordfilter migration
+ if (configMigration < 1) {
+ // Debug snippet to clean up storage
+ /*
+ Object.keys(useServerSideStorageStore().prefsStorage.simple.muteFilters).forEach(key => {
+ useServerSideStorageStore().unsetPreference({ path: 'simple.muteFilters.' + key, value: null })
+ })
+ */
+
+ // Convert existing wordfilter into synced one
+ store.rootState.config.muteWords.forEach(word => {
+ const uniqueId = uuidv4()
+
+ useServerSideStorageStore().setPreference({
+ path: 'simple.muteFilters.' + uniqueId,
+ value: {
+ type: 'word',
+ value: word
+ }
+ })
+ })
+ }
+
+
+ // Update the flag
+ useServerSideStorageStore().setflag({ flag: 'configMigration', value: CONFIG_MIGRATION })
useServerSideStorageStore().pushServerSideStorage()
if (user.token) {
dispatch('setWsToken', user.token)
// Initialize the shout socket.
dispatch('initializeSocket')
}
const startPolling = () => {
// Start getting fresh posts.
dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
dispatch('startFetchingNotifications')
// Start fetching chats
dispatch('startFetchingChats')
}
dispatch('startFetchingLists')
dispatch('startFetchingBookmarkFolders')
if (user.locked) {
dispatch('startFetchingFollowRequests')
}
if (store.getters.mergedConfig.useStreamingApi) {
dispatch('fetchTimeline', { timeline: 'friends', since: null })
dispatch('fetchNotifications', { since: null })
dispatch('enableMastoSockets', true).catch((error) => {
console.error('Failed initializing MastoAPI Streaming socket', error)
}).then(() => {
dispatch('fetchChats', { latest: true })
setTimeout(() => dispatch('setNotificationsSilence', false), 10000)
})
} else {
startPolling()
}
// Get user mutes
dispatch('fetchMutes')
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
.then((friends) => commit('addNewUsers', friends))
} else {
const response = data.error
// Authentication failed
commit('endLogin')
// remove authentication token on client/authentication errors
if ([400, 401, 403, 422].includes(response.status)) {
useOAuthStore().clearToken()
}
if (response.status === 401) {
reject(new Error('Wrong username or password'))
} else {
reject(new Error('An error occurred, please try again'))
}
}
commit('endLogin')
resolve()
})
.catch((error) => {
console.error(error)
commit('endLogin')
reject(new Error('Failed to connect to server, try again'))
})
})
}
}
}
export default users
diff --git a/src/stores/serverSideStorage.js b/src/stores/serverSideStorage.js
index e9ab571e3e..1f71803ac2 100644
--- a/src/stores/serverSideStorage.js
+++ b/src/stores/serverSideStorage.js
@@ -1,517 +1,517 @@
import { defineStore } from 'pinia'
import { toRaw } from 'vue'
import {
isEqual,
cloneDeep,
set,
unset,
get,
clamp,
flatten,
groupBy,
findLastIndex,
takeRight,
uniqWith,
merge as _merge
} from 'lodash'
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
export const VERSION = 1
export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
export const COMMAND_TRIM_FLAGS = 1000
export const COMMAND_TRIM_FLAGS_AND_RESET = 1001
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 flags, 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,
- filters: {}
+ muteFilters: {}
},
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) => ({
...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 === 'number' || typeof v === 'boolean') return
if (typeof v === 'object' && v != null) return
console.warn(`Preference simple.${k} as invalid type, 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, 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 source, source of truth irrelevant')
result.recent = cache
result.stale = live
} else {
console.debug('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('Both sources are invalid, start from scratch')
result.needUpload = true
}
const merge = (a, b) => ({
_user: a._user ?? b._user,
_version: a._version ?? b._version,
_timestamp: a._timestamp ?? b._timestamp,
needUpload: b.needUpload ?? a.needUpload,
prefsStorage: _merge(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.prototype.hasOwnProperty.call(entry, 'path') &&
Object.prototype.hasOwnProperty.call(entry, 'operation') &&
Object.prototype.hasOwnProperty.call(entry, 'args') &&
Object.prototype.hasOwnProperty.call(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') || path.startsWith('objectCollections')) {
throw new Error('Illegal operation "set" on a collection')
}
if (path.split(/\./g).length <= 1) {
throw new Error(`Calling set on depth <= 1 (path: ${path}) is not allowed`)
}
set(resultOutput, path, args[0])
break
case 'unset':
if (path.startsWith('collections') || path.startsWith('objectCollections')) {
throw new Error('Illegal operation "unset" on a collection')
}
if (path.split(/\./g).length <= 2) {
throw new Error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`)
}
unset(resultOutput, path)
break
case 'addToCollection':
set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0])))
break
case 'removeFromCollection': {
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:
throw new 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 })
}
} else if (totalFlags.reset > 0 && totalFlags.reset < 9000) {
console.debug('Received command to reset the flags')
allFlagKeys.forEach(flag => { result[flag] = 0 })
}
result.reset = 0
return result
}
export const _doMigrations = (cache) => {
if (!cache) return cache
if (cache._version < VERSION) {
console.debug('Local cached data has older version, seeing if there any migrations that can be applied')
// no migrations right now since we only have one version
console.debug('No migrations found')
}
if (cache._version > VERSION) {
console.debug('Local cached 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({}, 'serverSideStorage', { from: cache._version, to: VERSION }, cache)
}
}
}
return cache
}
export const useServerSideStorageStore = defineStore('serverSideStorage', {
state() {
return cloneDeep(defaultState)
},
actions: {
setFlag ({ flag, value }) {
this.flagStorage[flag] = value
this.dirty = true
},
setPreference ({ path, value }) {
if (path.startsWith('_')) {
throw new Error(`Tried to edit internal (starts with _) field '${path}', ignoring.`)
}
if (path.startsWith('collections') || path.startsWith('objectCollections')) {
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') || path.startsWith('objectCollections')) {
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, value)
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])
} else if (path.startsWith('objectCollections')) {
const { _key } = value
if (!_key && typeof _key !== 'string') {
throw new Error('Object for storage is missing _key field!')
}
const collection = new Set(get(this.prefsStorage, path + '.index'))
collection.add(_key)
set(this.prefsStorage, path + '.index', [...collection])
set(this.prefsStorage, path + '.data.' + _key, value)
}
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 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)
},
clearServerSideStorage () {
const blankState = { ...cloneDeep(defaultState) }
Object.keys(this).forEach(k => {
this[k] = blankState[k]
})
},
setServerSideStorage (userData) {
const live = userData.storage
this.raw = live
let cache = this.cache
if (cache && cache._user !== userData.fqn) {
console.warn('Cache belongs to another user! reinitializing local cache!')
cache = null
}
cache = _doMigrations(cache)
let { recent, stale, needUpload } = _getRecentData(cache, live)
const userNew = userData.created_at > NEW_USER_DATE
const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage
let dirty = false
if (recent === null) {
console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`)
recent = _wrapData({
flagStorage: { ...flagsTemplate },
prefsStorage: { ...defaultState.prefsStorage }
})
}
if (!needUpload && recent && stale) {
console.debug('Checking if data needs merging...')
// discarding timestamps and versions
/* eslint-disable no-unused-vars */
const { _timestamp: _0, _version: _1, ...recentData } = recent
const { _timestamp: _2, _version: _3, ...staleData } = stale
/* eslint-enable no-unused-vars */
dirty = !isEqual(recentData, 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
}
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
},
pushServerSideStorage ({ 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
.updateProfile({ params })
.then((user) => {
this.setServerSideStorage(user)
this.dirty = false
})
}
}
})

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 9, 9:47 AM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784646
Default Alt Text
(56 KB)

Event Timeline