Merge remote-tracking branch 'origin/develop' into migrate/vuex-to-pinia

This commit is contained in:
Henry Jameson 2025-01-30 18:08:05 +02:00
commit 58e18d48df
489 changed files with 31167 additions and 9871 deletions

View file

@ -0,0 +1,229 @@
import { set, get, cloneDeep, differenceWith, isEqual, flatten } from 'lodash'
export const defaultState = {
frontends: [],
loaded: false,
needsReboot: null,
config: null,
modifiedPaths: null,
descriptions: null,
draft: null,
dbConfigEnabled: null
}
export const newUserFlags = {
...defaultState.flagStorage
}
const adminSettingsStorage = {
state: {
...cloneDeep(defaultState)
},
mutations: {
setInstanceAdminNoDbConfig (state) {
state.loaded = false
state.dbConfigEnabled = false
},
setAvailableFrontends (state, { frontends }) {
state.frontends = frontends.map(f => {
f.installedRefs = f.installed_refs
if (f.name === 'pleroma-fe') {
f.refs = ['master', 'develop']
} else {
f.refs = [f.ref]
}
return f
})
},
updateAdminSettings (state, { config, modifiedPaths }) {
state.loaded = true
state.dbConfigEnabled = true
state.config = config
state.modifiedPaths = modifiedPaths
},
updateAdminDescriptions (state, { descriptions }) {
state.descriptions = descriptions
},
updateAdminDraft (state, { path, value }) {
const [group, key, subkey] = path
const parent = [group, key, subkey]
set(state.draft, path, value)
// force-updating grouped draft to trigger refresh of group settings
if (path.length > parent.length) {
set(state.draft, parent, cloneDeep(get(state.draft, parent)))
}
},
resetAdminDraft (state) {
state.draft = cloneDeep(state.config)
}
},
actions: {
loadFrontendsStuff ({ state, rootState, dispatch, commit }) {
rootState.api.backendInteractor.fetchAvailableFrontends()
.then(frontends => commit('setAvailableFrontends', { frontends }))
},
loadAdminStuff ({ state, rootState, dispatch, commit }) {
rootState.api.backendInteractor.fetchInstanceDBConfig()
.then(backendDbConfig => {
if (backendDbConfig.error) {
if (backendDbConfig.error.status === 400) {
backendDbConfig.error.json().then(errorJson => {
if (/configurable_from_database/.test(errorJson.error)) {
commit('setInstanceAdminNoDbConfig')
}
})
}
} else {
dispatch('setInstanceAdminSettings', { backendDbConfig })
}
})
if (state.descriptions === null) {
rootState.api.backendInteractor.fetchInstanceConfigDescriptions()
.then(backendDescriptions => dispatch('setInstanceAdminDescriptions', { backendDescriptions }))
}
},
setInstanceAdminSettings ({ state, commit, dispatch }, { backendDbConfig }) {
const config = state.config || {}
const modifiedPaths = new Set()
backendDbConfig.configs.forEach(c => {
const path = [c.group, c.key]
if (c.db) {
// Path elements can contain dot, therefore we use ' -> ' as a separator instead
// Using strings for modified paths for easier searching
c.db.forEach(x => modifiedPaths.add([...path, x].join(' -> ')))
}
const convert = (value) => {
if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
return value.reduce((acc, c) => {
return { ...acc, [c.tuple[0]]: convert(c.tuple[1]) }
}, {})
} else {
return value
}
}
set(config, path, convert(c.value))
})
commit('updateAdminSettings', { config, modifiedPaths })
commit('resetAdminDraft')
},
setInstanceAdminDescriptions ({ state, commit, dispatch }, { backendDescriptions }) {
const convert = ({ children, description, label, key = '<ROOT>', group, suggestions }, path, acc) => {
const newPath = group ? [group, key] : [key]
const obj = { description, label, suggestions }
if (Array.isArray(children)) {
children.forEach(c => {
convert(c, newPath, obj)
})
}
set(acc, newPath, obj)
}
const descriptions = {}
backendDescriptions.forEach(d => convert(d, '', descriptions))
commit('updateAdminDescriptions', { descriptions })
},
// This action takes draft state, diffs it with live config state and then pushes
// only differences between the two. Difference detection only work up to subkey (third) level.
pushAdminDraft ({ rootState, state, commit, dispatch }) {
// TODO cleanup paths in modifiedPaths
const convert = (value) => {
if (typeof value !== 'object') {
return value
} else if (Array.isArray(value)) {
return value.map(convert)
} else {
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
}
}
// Getting all group-keys used in config
const allGroupKeys = flatten(
Object
.entries(state.config)
.map(
([group, lv1data]) => Object
.keys(lv1data)
.map((key) => ({ group, key }))
)
)
// Only using group-keys where there are changes detected
const changedGroupKeys = allGroupKeys.filter(({ group, key }) => {
return !isEqual(state.config[group][key], state.draft[group][key])
})
// Here we take all changed group-keys and get all changed subkeys
const changed = changedGroupKeys.map(({ group, key }) => {
const config = state.config[group][key]
const draft = state.draft[group][key]
// We convert group-key value into entries arrays
const eConfig = Object.entries(config)
const eDraft = Object.entries(draft)
// Then those entries array we diff so only changed subkey entries remain
// We use the diffed array to reconstruct the object and then shove it into convert()
return ({ group, key, value: convert(Object.fromEntries(differenceWith(eDraft, eConfig, isEqual))) })
})
rootState.api.backendInteractor.pushInstanceDBConfig({
payload: {
configs: changed
}
})
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
},
pushAdminSetting ({ rootState, state, commit, dispatch }, { path, value }) {
const [group, key, ...rest] = Array.isArray(path) ? path : path.split(/\./g)
const clone = {} // not actually cloning the entire thing to avoid excessive writes
set(clone, rest, value)
// TODO cleanup paths in modifiedPaths
const convert = (value) => {
if (typeof value !== 'object') {
return value
} else if (Array.isArray(value)) {
return value.map(convert)
} else {
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
}
}
rootState.api.backendInteractor.pushInstanceDBConfig({
payload: {
configs: [{
group,
key,
value: convert(clone)
}]
}
})
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
},
resetAdminSetting ({ rootState, state, commit, dispatch }, { path }) {
const [group, key, subkey] = path.split(/\./g)
state.modifiedPaths.delete(path)
return rootState.api.backendInteractor.pushInstanceDBConfig({
payload: {
configs: [{
group,
key,
delete: true,
subkeys: [subkey]
}]
}
})
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
}
}
}
export default adminSettingsStorage

View file

@ -89,6 +89,9 @@ const api = {
const { state, commit, dispatch, rootState } = store
const timelineData = rootState.statuses.timelines.friends
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
state.mastoUserSocket.addEventListener('pleroma:authenticated', () => {
state.mastoUserSocket.subscribe('user')
})
state.mastoUserSocket.addEventListener(
'message',
({ detail: message }) => {
@ -204,12 +207,14 @@ const api = {
timeline = 'friends',
tag = false,
userId = false,
listId = false
listId = false,
statusId = false,
bookmarkFolderId = false
}) {
if (store.state.fetchers[timeline]) return
const fetcher = store.state.backendInteractor.startFetchingTimeline({
timeline, store, userId, listId, tag
timeline, store, userId, listId, statusId, bookmarkFolderId, tag
})
store.commit('addFetcher', { fetcherName: timeline, fetcher })
},
@ -273,6 +278,18 @@ const api = {
store.commit('removeFetcher', { fetcherName: 'lists', fetcher })
},
// Bookmark folders
startFetchingBookmarkFolders (store) {
if (store.state.fetchers.bookmarkFolders) return
const fetcher = store.state.backendInteractor.startFetchingBookmarkFolders({ store })
store.commit('addFetcher', { fetcherName: 'bookmarkFolders', fetcher })
},
stopFetchingBookmarkFolders (store) {
const fetcher = store.state.fetchers.bookmarkFolders
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'bookmarkFolders', fetcher })
},
// Pleroma websocket
setWsToken (store, token) {
store.commit('setWsToken', token)

View file

@ -0,0 +1,66 @@
import { remove, find } from 'lodash'
export const defaultState = {
allFolders: []
}
export const mutations = {
setBookmarkFolders (state, value) {
state.allFolders = value
},
setBookmarkFolder (state, { id, name, emoji, emoji_url: emojiUrl }) {
const entry = find(state.allFolders, { id })
if (!entry) {
state.allFolders.push({ id, name, emoji, emoji_url: emojiUrl })
} else {
entry.name = name
entry.emoji = emoji
entry.emoji_url = emojiUrl
}
},
deleteBookmarkFolder (state, { folderId }) {
remove(state.allFolders, folder => folder.id === folderId)
}
}
const actions = {
setBookmarkFolders ({ commit }, value) {
commit('setBookmarkFolders', value)
},
createBookmarkFolder ({ rootState, commit }, { name, emoji }) {
return rootState.api.backendInteractor.createBookmarkFolder({ name, emoji })
.then((folder) => {
commit('setBookmarkFolder', folder)
return folder
})
},
setBookmarkFolder ({ rootState, commit }, { folderId, name, emoji }) {
return rootState.api.backendInteractor.updateBookmarkFolder({ folderId, name, emoji })
.then((folder) => {
commit('setBookmarkFolder', folder)
return folder
})
},
deleteBookmarkFolder ({ rootState, commit }, { folderId }) {
rootState.api.backendInteractor.deleteBookmarkFolder({ folderId })
commit('deleteBookmarkFolder', { folderId })
}
}
export const getters = {
findBookmarkFolderName: state => id => {
const folder = state.allFolders.find(folder => folder.id === id)
if (!folder) return
return folder.name
}
}
const bookmarkFolders = {
state: defaultState,
mutations,
actions,
getters
}
export default bookmarkFolders

View file

@ -65,6 +65,7 @@ const chats = {
const newChatMessageSideEffects = (chat) => {
maybeShowChatNotification(store, chat)
}
commit('addNewUsers', chats.map(k => k.account).filter(k => k))
commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects })
},
updateChat ({ commit }, { chat }) {

View file

@ -1,11 +1,23 @@
import Cookies from 'js-cookie'
import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
import { applyConfig } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages'
import { set } from 'lodash'
import localeService from '../services/locale/locale.service.js'
import { useI18nStore } from '../stores/i18n.js'
import { useInterfaceStore } from '../stores/interface.js'
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
const APPEARANCE_SETTINGS_KEYS = new Set([
'sidebarColumnWidth',
'contentColumnWidth',
'notifsColumnWidth',
'textSize',
'navbarSize',
'panelHeaderSize',
'forcedRoundness',
'emojiSize',
'emojiReactionsScale'
])
const browserLocale = (window.navigator.language || 'en').split('-')[0]
@ -20,15 +32,40 @@ export const multiChoiceProperties = [
'conversationDisplay', // tree | linear
'conversationOtherRepliesButton', // below | inside
'mentionLinkDisplay', // short | full_for_remote | full
'userPopoverAvatarAction' // close | zoom | open
'userPopoverAvatarAction', // close | zoom | open
'unsavedPostAction' // save | discard | confirm
]
export const defaultState = {
expertLevel: 0, // used to track which settings to show and hide
colors: {},
theme: undefined,
customTheme: undefined,
customThemeSource: undefined,
// 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
}
},
hideISP: false,
hideInstanceWallpaper: false,
hideShoutbox: false,
@ -37,10 +74,13 @@ export const defaultState = {
hideMutedThreads: undefined, // instance default
hideWordFilteredPosts: undefined, // instance default
muteBotStatuses: undefined, // instance default
muteSensitiveStatuses: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
padEmoji: true,
hideAttachments: false,
hideAttachmentsInConv: false,
hideScrobbles: false,
hideScrobblesAfter: '2d',
maxThumbnails: 16,
hideNsfw: true,
preloadImage: true,
@ -57,6 +97,7 @@ export const defaultState = {
notificationVisibility: {
follows: true,
mentions: true,
statuses: true,
likes: true,
repeats: true,
moves: true,
@ -66,7 +107,21 @@ export const defaultState = {
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,
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale,
@ -84,11 +139,14 @@ export const defaultState = {
modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default
modalOnMuteConversation: undefined, // instance default
modalOnMuteDomain: undefined, // instance default
modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default
modalOnDenyFollow: undefined, // instance default
modalOnRemoveUserFromFollowers: undefined, // instance default
modalMobileCenter: undefined,
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: true,
@ -99,7 +157,12 @@ export const defaultState = {
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
emojiReactionsScale: 1.0,
emojiReactionsScale: undefined,
textSize: undefined, // instance default
emojiSize: undefined, // instance default
navbarSize: undefined, // instance default
panelHeaderSize: undefined, // instance default
forcedRoundness: undefined, // instance default
navbarColumnStretch: false,
greentext: undefined, // instance default
useAtIcon: undefined, // instance default
@ -118,8 +181,22 @@ export const defaultState = {
conversationTreeAdvanced: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default
showExtraNotifications: undefined, // instance default
showExtraNotificationsTip: undefined, // instance default
showChatsInExtraNotifications: undefined, // instance default
showAnnouncementsInExtraNotifications: undefined, // instance default
showFollowRequestsInExtraNotifications: undefined, // instance default
maxDepthInThread: undefined, // instance default
autocompleteSelect: undefined // instance default
autocompleteSelect: undefined, // instance default
closingDrawerMarksAsSeen: undefined, // instance default
unseenAtTop: undefined, // instance default
ignoreInactionableSeen: undefined, // instance default
unsavedPostAction: undefined, // instance default
autoSaveDraft: undefined, // instance default
useAbsoluteTimeFormat: undefined, // instance default
absoluteTimeFormatMinAge: undefined, // instance default
absoluteTime12h: undefined, // instance default
imageCompression: true
}
// caching the instance default properties
@ -149,8 +226,12 @@ const config = {
}
},
mutations: {
setOptionTemporarily (state, { name, value }) {
set(state, name, value)
applyConfig(state)
},
setOption (state, { name, value }) {
state[name] = value
set(state, name, value)
},
setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user]
@ -179,33 +260,86 @@ const config = {
setHighlight ({ commit, dispatch }, { user, color, type }) {
commit('setHighlight', { user, color, type })
},
setOptionTemporarily ({ commit, dispatch, state, rootState }, { name, value }) {
if (rootState.interface.temporaryChangesTimeoutId !== null) {
console.warn('Can\'t track more than one temporary change')
return
}
const oldValue = state[name]
commit('setOptionTemporarily', { name, value })
const confirm = () => {
dispatch('setOption', { name, value })
commit('clearTemporaryChanges')
}
const revert = () => {
commit('setOptionTemporarily', { name, value: oldValue })
commit('clearTemporaryChanges')
}
commit('setTemporaryChanges', {
timeoutId: setTimeout(revert, 10000),
confirm,
revert
})
},
setThemeV2 ({ commit, dispatch }, { customTheme, customThemeSource }) {
commit('setOption', { name: 'theme', value: 'custom' })
commit('setOption', { name: 'customTheme', value: customTheme })
commit('setOption', { name: 'customThemeSource', value: customThemeSource })
dispatch('setTheme', { themeData: customThemeSource, recompile: true })
},
setOption ({ commit, dispatch, state }, { name, value }) {
commit('setOption', { name, value })
switch (name) {
case 'theme':
setPreset(value)
break
case 'sidebarColumnWidth':
case 'contentColumnWidth':
case 'notifsColumnWidth':
case 'emojiReactionsScale':
const exceptions = new Set([
'useStreamingApi'
])
if (exceptions.has(name)) {
switch (name) {
case 'useStreamingApi': {
const action = value ? 'enableMastoSockets' : 'disableMastoSockets'
dispatch(action).then(() => {
commit('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
dispatch('disableMastoSockets')
dispatch('setOption', { name: 'useStreamingApi', value: false })
})
break
}
}
} else {
commit('setOption', { name, value })
if (APPEARANCE_SETTINGS_KEYS.has(name)) {
applyConfig(state)
break
case 'customTheme':
case 'customThemeSource':
applyTheme(value)
break
case 'interfaceLanguage':
messages.setLanguage(useI18nStore().i18n, value)
dispatch('loadUnicodeEmojiData', value)
Cookies.set(
BACKEND_LANGUAGE_COOKIE_NAME,
localeService.internalToBackendLocaleMulti(value)
)
break
case 'thirdColumnMode':
useInterfaceStore().setLayoutWidth(undefined)
break
}
if (name.startsWith('theme3hacks')) {
dispatch('applyTheme', { recompile: true })
}
switch (name) {
case 'theme':
if (value === 'custom') break
dispatch('setTheme', { themeName: value, recompile: true, saveData: true })
break
case 'themeDebug': {
dispatch('setTheme', { recompile: true })
break
}
case 'interfaceLanguage':
messages.setLanguage(useI18nStore().i18n, value)
dispatch('loadUnicodeEmojiData', value)
Cookies.set(
BACKEND_LANGUAGE_COOKIE_NAME,
localeService.internalToBackendLocaleMulti(value)
)
break
case 'thirdColumnMode':
useInterfaceStore().setLayoutWidth(undefined)
break
}
}
}
}

86
src/modules/drafts.js Normal file
View file

@ -0,0 +1,86 @@
import { storage } from 'src/lib/storage.js'
export const defaultState = {
drafts: {}
}
export const mutations = {
addOrSaveDraft (state, { draft }) {
state.drafts[draft.id] = draft
},
abandonDraft (state, { id }) {
delete state.drafts[id]
},
loadDrafts (state, data) {
state.drafts = data
}
}
const storageKey = 'pleroma-fe-drafts'
/*
* Note: we do not use the persist state plugin because
* it is not impossible for a user to have two windows at
* the same time. The persist state plugin is just overriding
* everything with the current state. This isn't good because
* if a draft is created in one window and another draft is
* created in another, the draft in the first window will just
* be overriden.
* Here, we can't guarantee 100% atomicity unless one uses
* different keys, which will just pollute the whole storage.
* It is indeed best to have backend support for this.
*/
const getStorageData = async () => ((await storage.getItem(storageKey)) || {})
const saveDraftToStorage = async (draft) => {
const currentData = await getStorageData()
currentData[draft.id] = JSON.parse(JSON.stringify(draft))
await storage.setItem(storageKey, currentData)
}
const deleteDraftFromStorage = async (id) => {
const currentData = await getStorageData()
delete currentData[id]
await storage.setItem(storageKey, currentData)
}
export const actions = {
async addOrSaveDraft (store, { draft }) {
const id = draft.id || (new Date().getTime()).toString()
const draftWithId = { ...draft, id }
store.commit('addOrSaveDraft', { draft: draftWithId })
await saveDraftToStorage(draftWithId)
return id
},
async abandonDraft (store, { id }) {
store.commit('abandonDraft', { id })
await deleteDraftFromStorage(id)
},
async loadDrafts (store) {
const currentData = await getStorageData()
store.commit('loadDrafts', currentData)
}
}
export const getters = {
draftsByTypeAndRefId (state) {
return (type, refId) => {
return Object.values(state.drafts).filter(draft => draft.type === type && draft.refId === refId)
}
},
draftsArray (state) {
return Object.values(state.drafts)
},
draftCount (state) {
return Object.values(state.drafts).length
}
}
const drafts = {
state: defaultState,
mutations,
getters,
actions
}
export default drafts

View file

@ -1,5 +1,3 @@
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
@ -45,7 +43,10 @@ const defaultState = {
registrationOpen: true,
server: 'http://localhost:4040/',
textlimit: 5000,
themeData: undefined,
themesIndex: undefined,
stylesIndex: undefined,
palettesIndex: undefined,
themeData: undefined, // used for theme editor v2
vapidPublicKey: undefined,
// Stuff from static/config.json
@ -72,15 +73,19 @@ const defaultState = {
hideSitename: false,
hideUserStats: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: false,
modalMobileCenter: false,
loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
@ -98,14 +103,36 @@ const defaultState = {
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
palette: null,
style: null,
emojiReactionsScale: 0.5,
textSize: '14px',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
forcedRoundness: -1,
fontsOverride: {},
virtualScrolling: true,
sensitiveByDefault: false,
conversationDisplay: 'linear',
conversationTreeAdvanced: false,
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
showExtraNotifications: true,
showExtraNotificationsTip: true,
showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
unsavedPostAction: 'confirm',
autoSaveDraft: false,
useAbsoluteTimeFormat: false,
absoluteTimeFormatMinAge: '0d',
absoluteTime12h: '24h',
// Nasty stuff
customEmoji: [],
@ -125,10 +152,13 @@ const defaultState = {
shoutAvailable: false,
pleromaChatMessagesAvailable: false,
pleromaCustomEmojiReactionsAvailable: false,
pleromaBookmarkFoldersAvailable: false,
gopherAvailable: false,
mediaProxyAvailable: false,
suggestionsEnabled: false,
suggestionsWeb: '',
quotingAvailable: false,
groupActorAvailable: false,
// Html stuff
instanceSpecificPanelContent: '',
@ -136,6 +166,7 @@ const defaultState = {
// Version Information
backendVersion: '',
backendRepository: '',
frontendVersion: '',
pollsAvailable: false,
@ -269,9 +300,6 @@ const instance = {
dispatch('initializeSocket')
}
break
case 'theme':
dispatch('setTheme', value)
break
}
},
async getStaticEmoji ({ commit }) {
@ -288,8 +316,7 @@ const instance = {
}, {})
commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) })
} catch (e) {
console.warn("Can't load static emoji")
console.warn(e)
console.warn("Can't load static emoji\n", e)
}
},
@ -356,29 +383,9 @@ const instance = {
throw (res)
}
} catch (e) {
console.warn("Can't load custom emojis")
console.warn(e)
console.warn("Can't load custom emojis\n", e)
}
},
setTheme ({ commit, rootState }, themeName) {
commit('setInstanceOption', { name: 'theme', value: themeName })
getPreset(themeName)
.then(themeData => {
commit('setInstanceOption', { name: 'themeData', value: themeData })
// No need to apply theme if there's user theme already
const { customTheme } = rootState.config
if (customTheme) return
// New theme presets don't have 'theme' property, they use 'source'
const themeSource = themeData.source
if (!themeData.theme || (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION)) {
applyTheme(themeSource)
} else {
applyTheme(themeData.theme)
}
})
},
fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) {
state.customEmojiFetched = true
@ -397,8 +404,7 @@ const instance = {
})
commit('setKnownDomains', result)
} catch (e) {
console.warn("Can't load known domains")
console.warn(e)
console.warn("Can't load known domains\n", e)
}
}
}

View file

@ -0,0 +1,169 @@
import apiService from '../services/api/api.service.js'
import {
isStatusNotification,
isValidNotification,
maybeShowNotification
} from '../services/notification_utils/notification_utils.js'
import {
closeDesktopNotification,
closeAllDesktopNotifications
} from '../services/desktop_notification_utils/desktop_notification_utils.js'
const emptyNotifications = () => ({
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false
})
export const defaultState = () => ({
...emptyNotifications()
})
export const notifications = {
state: defaultState(),
mutations: {
addNewNotifications (state, { notifications }) {
notifications.forEach(notification => {
state.data.push(notification)
state.idStore[notification.id] = notification
})
},
clearNotifications (state) {
state = emptyNotifications()
},
updateNotificationsMinMaxId (state, id) {
state.maxId = id > state.maxId ? id : state.maxId
state.minId = id < state.minId ? id : state.minId
},
setNotificationsLoading (state, { value }) {
state.loading = value
},
setNotificationsSilence (state, { value }) {
state.desktopNotificationSilence = value
},
markNotificationsAsSeen (state) {
state.data.forEach((notification) => {
notification.seen = true
})
},
markSingleNotificationAsSeen (state, { id }) {
const notification = state.idStore[id]
if (notification) notification.seen = true
},
dismissNotification (state, { id }) {
state.data = state.data.filter(n => n.id !== id)
delete state.idStore[id]
},
updateNotification (state, { id, updater }) {
const notification = state.idStore[id]
notification && updater(notification)
}
},
actions: {
addNewNotifications (store, { notifications, older }) {
const { commit, dispatch, state, rootState } = store
const validNotifications = notifications.filter((notification) => {
// If invalid notification, update ids but don't add it to store
if (!isValidNotification(notification)) {
console.error('Invalid notification:', notification)
commit('updateNotificationsMinMaxId', notification.id)
return false
}
return true
})
const statusNotifications = validNotifications.filter(notification => isStatusNotification(notification.type) && notification.status)
// Synchronous commit to add all the statuses
commit('addNewStatuses', { statuses: statusNotifications.map(notification => notification.status) })
// Update references to statuses in notifications to ones in the store
statusNotifications.forEach(notification => {
const id = notification.status.id
const referenceStatus = rootState.statuses.allStatusesObject[id]
if (referenceStatus) {
notification.status = referenceStatus
}
})
validNotifications.forEach(notification => {
if (notification.type === 'pleroma:report') {
dispatch('addReport', notification.report)
}
if (notification.type === 'pleroma:emoji_reaction') {
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
// Only add a new notification if we don't have one for the same action
// eslint-disable-next-line no-prototype-builtins
if (!state.idStore.hasOwnProperty(notification.id)) {
commit('updateNotificationsMinMaxId', notification.id)
commit('addNewNotifications', { notifications: [notification] })
maybeShowNotification(store, notification)
} else if (notification.seen) {
state.idStore[notification.id].seen = true
}
})
},
notificationClicked ({ state, dispatch }, { id }) {
const notification = state.idStore[id]
const { type, seen } = notification
if (!seen) {
switch (type) {
case 'mention':
case 'pleroma:report':
case 'follow_request':
break
default:
dispatch('markSingleNotificationAsSeen', { id })
}
}
},
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value })
},
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
markNotificationsAsSeen ({ rootState, state, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
id: state.maxId,
credentials: rootState.users.currentUser.credentials
}).then(() => {
closeAllDesktopNotifications(rootState)
})
},
markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
commit('markSingleNotificationAsSeen', { id })
apiService.markNotificationsAsSeen({
single: true,
id,
credentials: rootState.users.currentUser.credentials
}).then(() => {
closeDesktopNotification(rootState, { id })
})
},
dismissNotificationLocal ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
},
dismissNotification ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
rootState.api.backendInteractor.dismissNotification({ id })
},
updateNotification ({ rootState, commit }, { id, updater }) {
commit('updateNotification', { id, updater })
}
}
}
export default notifications

View file

@ -22,9 +22,9 @@ const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
.updateNotificationSettings({ settings })
.then(result => {
if (result.status === 'success') {
commit('confirmServerSideOption', { name, value })
commit('confirmProfileOption', { name, value })
} else {
commit('confirmServerSideOption', { name, value: oldValue })
commit('confirmProfileOption', { name, value: oldValue })
}
})
}
@ -94,16 +94,16 @@ export const settingsMap = {
export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
const serverSideConfig = {
const profileConfig = {
state: { ...defaultState },
mutations: {
confirmServerSideOption (state, { name, value }) {
confirmProfileOption (state, { name, value }) {
set(state, name, value)
},
wipeServerSideOption (state, { name }) {
wipeProfileOption (state, { name }) {
set(state, name, null)
},
wipeAllServerSideOptions (state) {
wipeAllProfileOptions (state) {
Object.keys(settingsMap).forEach(key => {
set(state, key, null)
})
@ -118,23 +118,23 @@ const serverSideConfig = {
}
},
actions: {
setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) {
setProfileOption ({ rootState, state, commit, dispatch }, { name, value }) {
const oldValue = get(state, name)
const map = settingsMap[name]
if (!map) throw new Error('Invalid server-side setting')
const { set: path = map, api = defaultApi } = map
commit('wipeServerSideOption', { name })
commit('wipeProfileOption', { name })
api({ rootState, commit }, { path, value, oldValue })
.catch((e) => {
console.warn('Error setting server-side option:', e)
commit('confirmServerSideOption', { name, value: oldValue })
commit('confirmProfileOption', { name, value: oldValue })
})
},
logout ({ commit }) {
commit('wipeAllServerSideOptions')
commit('wipeAllProfileOptions')
}
}
}
export default serverSideConfig
export default profileConfig

View file

@ -1,5 +1,17 @@
import { toRaw } from 'vue'
import { isEqual, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight, uniqWith } from 'lodash'
import {
isEqual,
cloneDeep,
set,
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
@ -26,6 +38,7 @@ export const defaultState = {
collapseNav: false
},
collections: {
pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
pinnedNavItems: ['home', 'dms', 'chats']
}
},
@ -77,7 +90,7 @@ const _verifyPrefs = (state) => {
})
}
export const _getRecentData = (cache, live) => {
export const _getRecentData = (cache, live, isTest) => {
const result = { recent: null, stale: null, needUpload: false }
const cacheValid = _checkValidity(cache || {})
const liveValid = _checkValidity(live || {})
@ -110,6 +123,17 @@ export const _getRecentData = (cache, live) => {
console.debug('Both sources are invalid, start from scratch')
result.needUpload = true
}
const merge = (a, b) => ({
_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
}
@ -281,18 +305,18 @@ export const mutations = {
clearServerSideStorage (state, userData) {
state = { ...cloneDeep(defaultState) }
},
setServerSideStorage (state, userData) {
setServerSideStorage (state, userData, test) {
const live = userData.storage
state.raw = live
let cache = state.cache
if (cache && cache._user !== userData.fqn) {
console.warn('cache belongs to another user! reinitializing local cache!')
console.warn('Cache belongs to another user! reinitializing local cache!')
cache = null
}
cache = _doMigrations(cache)
let { recent, stale, needsUpload } = _getRecentData(cache, live)
let { recent, stale, needUpload } = _getRecentData(cache, live)
const userNew = userData.created_at > NEW_USER_DATE
const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage
@ -306,7 +330,7 @@ export const mutations = {
})
}
if (!needsUpload && recent && stale) {
if (!needUpload && recent && stale) {
console.debug('Checking if data needs merging...')
// discarding timestamps and versions
const { _timestamp: _0, _version: _1, ...recentData } = recent
@ -335,7 +359,7 @@ export const mutations = {
recent.flagStorage = { ...flagsTemplate, ...totalFlags }
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
state.dirty = dirty || needsUpload
state.dirty = dirty || needUpload
state.cache = recent
// set local timestamp to smaller one if we don't have any changes
if (stale && recent && !state.dirty) {
@ -419,7 +443,6 @@ const serverSideStorage = {
actions: {
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
const needPush = state.dirty || force
console.log(needPush)
if (!needPush) return
commit('updateCache', { username: rootState.users.currentUser.fqn })
const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }

View file

@ -12,11 +12,6 @@ import {
isArray,
omitBy
} from 'lodash'
import {
isStatusNotification,
isValidNotification,
maybeShowNotification
} from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js'
import { useReportsStore } from '../stores/reports.js'
@ -37,21 +32,12 @@ const emptyTl = (userId = 0) => ({
flushMarker: 0
})
const emptyNotifications = () => ({
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false
})
export const defaultState = () => ({
allStatuses: [],
scrobblesNextFetch: {},
allStatusesObject: {},
conversationsObject: {},
maxId: 0,
notifications: emptyNotifications(),
favorites: new Set(),
timelines: {
mentions: emptyTl(),
@ -121,8 +107,24 @@ const sortTimeline = (timeline) => {
return timeline
}
const getLatestScrobble = (state, user) => {
if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) {
return
}
state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000
apiService.fetchScrobbles({ accountId: user.id }).then((scrobbles) => {
if (scrobbles.length > 0) {
user.latestScrobble = scrobbles[0]
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
}
})
}
// Add status to the global storages (arrays and objects maintaining statuses) except timelines
const addStatusToGlobalStorage = (state, data) => {
getLatestScrobble(state, data.user)
const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data)
if (result.new) {
// Add to conversation
@ -138,22 +140,6 @@ const addStatusToGlobalStorage = (state, data) => {
return result
}
// Remove status from the global storages (arrays and objects maintaining statuses) except timelines
const removeStatusFromGlobalStorage = (state, status) => {
remove(state.allStatuses, { id: status.id })
// TODO: Need to remove from allStatusesObject?
// Remove possible notification
remove(state.notifications.data, ({ action: { id } }) => id === status.id)
// Remove from conversation
const conversationId = status.statusnet_conversation_id
if (state.conversationsObject[conversationId]) {
remove(state.conversationsObject[conversationId], { id: status.id })
}
}
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
// Sanity check
if (!isArray(statuses)) {
@ -230,6 +216,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
timelineObject.newStatusCount += 1
}
if (status.quote) {
addStatus(status.quote, /* showImmediately = */ false, /* addToTimeline = */ false)
}
return status
}
@ -283,26 +273,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
favoriteStatus(favorite)
}
},
deletion: (deletion) => {
const uri = deletion.uri
const status = find(allStatuses, { uri })
if (!status) {
return
}
removeStatusFromGlobalStorage(state, status)
if (timeline) {
remove(timelineObject.statuses, { uri })
remove(timelineObject.visibleStatuses, { uri })
}
},
follow: (follow) => {
// NOOP, it is known status but we don't do anything about it for now
},
default: (unknown) => {
console.log('unknown status type')
console.log(unknown)
console.warn('unknown status type')
console.warn(unknown)
}
}
@ -318,52 +294,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
}
const updateNotificationsMinMaxId = (state, notification) => {
state.notifications.maxId = notification.id > state.notifications.maxId
? notification.id
: state.notifications.maxId
state.notifications.minId = notification.id < state.notifications.minId
? notification.id
: state.notifications.minId
}
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => {
each(notifications, (notification) => {
// If invalid notification, update ids but don't add it to store
if (!isValidNotification(notification)) {
console.error('Invalid notification:', notification)
updateNotificationsMinMaxId(state, notification)
return
}
if (isStatusNotification(notification.type)) {
notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
if (notification.type === 'pleroma:report') {
useReportsStore().addReport(notification.report)
}
if (notification.type === 'pleroma:emoji_reaction') {
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
// Only add a new notification if we don't have one for the same action
// eslint-disable-next-line no-prototype-builtins
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
updateNotificationsMinMaxId(state, notification)
state.notifications.data.push(notification)
state.notifications.idStore[notification.id] = notification
newNotificationSideEffects(notification)
} else if (notification.seen) {
state.notifications.idStore[notification.id].seen = true
}
})
}
const removeStatus = (state, { timeline, userId }) => {
const timelineObject = state.timelines[timeline]
if (userId) {
@ -376,7 +306,6 @@ const removeStatus = (state, { timeline, userId }) => {
export const mutations = {
addNewStatuses,
addNewNotifications,
removeStatus,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
@ -398,9 +327,6 @@ export const mutations = {
const userId = excludeUserId ? state.timelines[timeline].userId : undefined
state.timelines[timeline] = emptyTl(userId)
},
clearNotifications (state) {
state.notifications = emptyNotifications()
},
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
@ -460,10 +386,12 @@ export const mutations = {
setBookmarked (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.bookmarked = value
newStatus.bookmark_folder_id = status.bookmark_folder_id
},
setBookmarkedConfirm (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.bookmarked = status.bookmarked
if (status.pleroma) newStatus.bookmark_folder_id = status.pleroma.bookmark_folder
},
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
@ -483,31 +411,6 @@ export const mutations = {
const newStatus = state.allStatusesObject[id]
newStatus.nsfw = nsfw
},
setNotificationsLoading (state, { value }) {
state.notifications.loading = value
},
setNotificationsSilence (state, { value }) {
state.notifications.desktopNotificationSilence = value
},
markNotificationsAsSeen (state) {
each(state.notifications.data, (notification) => {
notification.seen = true
})
},
markSingleNotificationAsSeen (state, { id }) {
const notification = find(state.notifications.data, n => n.id === id)
if (notification) notification.seen = true
},
dismissNotification (state, { id }) {
state.notifications.data = state.notifications.data.filter(n => n.id !== id)
},
dismissNotifications (state, { finder }) {
state.notifications.data = state.notifications.data.filter(n => finder)
},
updateNotification (state, { id, updater }) {
const notification = find(state.notifications.data, n => n.id === id)
notification && updater(notification)
},
queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
},
@ -589,23 +492,9 @@ export const mutations = {
const statuses = {
state: defaultState(),
actions: {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
addNewStatuses ({ rootState, commit, dispatch, state }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
},
addNewNotifications (store, { notifications, older }) {
const { commit, dispatch, rootGetters } = store
const newNotificationSideEffects = (notification) => {
maybeShowNotification(store, notification)
}
commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
},
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value })
},
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
fetchStatus ({ rootState, dispatch }, id) {
return rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
@ -616,9 +505,19 @@ const statuses = {
fetchStatusHistory ({ rootState, dispatch }, status) {
return apiService.fetchStatusHistory({ status })
},
deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status })
deleteStatus ({ rootState, commit, dispatch }, status) {
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
.then((_) => {
commit('setDeleted', { status })
})
.catch((e) => {
dispatch('pushGlobalNotice', {
level: 'error',
messageKey: 'status.delete_error',
messageArgs: [e.message],
timeout: 5000
})
})
},
deleteStatusById ({ rootState, commit }, id) {
const status = rootState.statuses.allStatusesObject[id]
@ -651,11 +550,11 @@ const statuses = {
rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
muteConversation ({ rootState, commit }, statusId) {
muteConversation ({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
unmuteConversation ({ rootState, commit }, statusId) {
unmuteConversation ({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
@ -673,7 +572,7 @@ const statuses = {
},
bookmark ({ rootState, commit }, status) {
commit('setBookmarked', { status, value: true })
rootState.api.backendInteractor.bookmarkStatus({ id: status.id })
rootState.api.backendInteractor.bookmarkStatus({ id: status.id, folder_id: status.bookmark_folder_id })
.then(status => {
commit('setBookmarkedConfirm', { status })
})
@ -691,31 +590,6 @@ const statuses = {
queueFlushAll ({ rootState, commit }) {
commit('queueFlushAll')
},
markNotificationsAsSeen ({ rootState, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
id: rootState.statuses.notifications.maxId,
credentials: rootState.users.currentUser.credentials
})
},
markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
commit('markSingleNotificationAsSeen', { id })
apiService.markNotificationsAsSeen({
single: true,
id,
credentials: rootState.users.currentUser.credentials
})
},
dismissNotificationLocal ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
},
dismissNotification ({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
rootState.api.backendInteractor.dismissNotification({ id })
},
updateNotification ({ rootState, commit }, { id, updater }) {
commit('updateNotification', { id, updater })
},
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
@ -748,7 +622,7 @@ const statuses = {
)
},
fetchEmojiReactionsBy ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
return rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
emojiReactions => {
commit('addEmojiReactionsBy', { id, emojiReactions, currentUser: rootState.users.currentUser })
}

View file

@ -2,7 +2,7 @@ import backendInteractorService from '../services/backend_interactor_service/bac
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'
import { useInterfaceStore } from '../stores/interface.js'
// TODO: Unify with mergeOrAdd in statuses.js
@ -196,9 +196,15 @@ export const mutations = {
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)
@ -245,6 +251,7 @@ export const mutations = {
signUpPending (state) {
state.signUpPending = true
state.signUpErrors = []
state.signUpNotice = {}
},
signUpSuccess (state) {
state.signUpPending = false
@ -252,6 +259,12 @@ export const mutations = {
signUpFailure (state, errors) {
state.signUpPending = false
state.signUpErrors = errors
state.signUpNotice = {}
},
signUpNotice (state, notice) {
state.signUpPending = false
state.signUpErrors = []
state.signUpNotice = notice
}
}
@ -282,6 +295,7 @@ export const defaultState = {
usersByNameObject: {},
signUpPending: false,
signUpErrors: [],
signUpNotice: {},
relationships: {}
}
@ -321,10 +335,20 @@ const users = {
.then((inLists) => store.commit('updateUserInLists', { id, inLists }))
}
},
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
fetchBlocks (store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.blockIdsMaxId
return store.rootState.api.backendInteractor.fetchBlocks({ maxId })
.then((blocks) => {
store.commit('saveBlockIds', map(blocks, 'id'))
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
})
@ -347,10 +371,20 @@ const users = {
editUserNote (store, args) {
return editUserNote(store, args)
},
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
fetchMutes (store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.muteIdsMaxId
return store.rootState.api.backendInteractor.fetchMutes({ maxId })
.then((mutes) => {
store.commit('saveMuteIds', map(mutes, 'id'))
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
})
@ -419,11 +453,11 @@ const users = {
commit('clearFollowers', userId)
},
subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.subscribeUser({ id })
return rootState.api.backendInteractor.followUser({ id, notify: true })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
unsubscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.unsubscribeUser({ id })
return rootState.api.backendInteractor.followUser({ id, notify: false })
.then((relationship) => commit('updateUserRelationship', [relationship]))
},
toggleActivationStatus ({ rootState, commit }, { user }) {
@ -473,7 +507,7 @@ const users = {
store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.statuses.notifications.idStore
const notificationsObject = store.rootState.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
.filter(([k, val]) => notificationIds.includes(k))
.map(([k, val]) => val)
@ -499,9 +533,16 @@ const users = {
const data = await rootState.api.backendInteractor.register(
{ params: { ...userInfo } }
)
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token)
if (data.access_token) {
store.commit('signUpSuccess')
store.commit('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)
@ -539,6 +580,7 @@ const users = {
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingLists')
store.dispatch('stopFetchingBookmarkFolders')
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
@ -552,6 +594,7 @@ const users = {
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) => {
@ -566,54 +609,55 @@ const users = {
commit('setServerSideStorage', user)
commit('addNewUsers', [user])
store.dispatch('fetchEmoji')
dispatch('fetchEmoji')
getNotificationPermission()
.then(permission => useInterfaceStore().setNotificationPermission(permission))
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
store.dispatch('pushServerSideStorage')
dispatch('pushServerSideStorage')
if (user.token) {
store.dispatch('setWsToken', user.token)
dispatch('setWsToken', user.token)
// Initialize the shout socket.
store.dispatch('initializeSocket')
dispatch('initializeSocket')
}
const startPolling = () => {
// Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
store.dispatch('startFetchingNotifications')
dispatch('startFetchingNotifications')
// Start fetching chats
store.dispatch('startFetchingChats')
dispatch('startFetchingChats')
}
store.dispatch('startFetchingLists')
dispatch('startFetchingLists')
dispatch('startFetchingBookmarkFolders')
if (user.locked) {
store.dispatch('startFetchingFollowRequests')
dispatch('startFetchingFollowRequests')
}
if (store.getters.mergedConfig.useStreamingApi) {
store.dispatch('fetchTimeline', { timeline: 'friends', since: null })
store.dispatch('fetchNotifications', { since: null })
store.dispatch('enableMastoSockets', true).catch((error) => {
dispatch('fetchTimeline', { timeline: 'friends', since: null })
dispatch('fetchNotifications', { since: null })
dispatch('enableMastoSockets', true).catch((error) => {
console.error('Failed initializing MastoAPI Streaming socket', error)
}).then(() => {
store.dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
dispatch('fetchChats', { latest: true })
setTimeout(() => dispatch('setNotificationsSilence', false), 10000)
})
} else {
startPolling()
}
// Get user mutes
store.dispatch('fetchMutes')
dispatch('fetchMutes')
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
@ -625,6 +669,12 @@ const users = {
const response = data.error
// Authentication failed
commit('endLogin')
// remove authentication token on client/authentication errors
if ([400, 401, 403, 422].includes(response.status)) {
commit('clearToken')
}
if (response.status === 401) {
reject(new Error('Wrong username or password'))
} else {
@ -635,7 +685,7 @@ const users = {
resolve()
})
.catch((error) => {
console.log(error)
console.error(error)
commit('endLogin')
reject(new Error('Failed to connect to server, try again'))
})