diff options
Diffstat (limited to 'src/modules')
| -rw-r--r-- | src/modules/adminSettings.js | 229 | ||||
| -rw-r--r-- | src/modules/api.js | 5 | ||||
| -rw-r--r-- | src/modules/chats.js | 1 | ||||
| -rw-r--r-- | src/modules/config.js | 187 | ||||
| -rw-r--r-- | src/modules/instance.js | 48 | ||||
| -rw-r--r-- | src/modules/interface.js | 258 | ||||
| -rw-r--r-- | src/modules/notifications.js | 169 | ||||
| -rw-r--r-- | src/modules/postStatus.js | 6 | ||||
| -rw-r--r-- | src/modules/profileConfig.js (renamed from src/modules/serverSideConfig.js) | 22 | ||||
| -rw-r--r-- | src/modules/serverSideStorage.js | 1 | ||||
| -rw-r--r-- | src/modules/statuses.js | 198 | ||||
| -rw-r--r-- | src/modules/users.js | 106 |
12 files changed, 960 insertions, 270 deletions
diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js new file mode 100644 index 00000000..ef28f6e0 --- /dev/null +++ b/src/modules/adminSettings.js @@ -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 diff --git a/src/modules/api.js b/src/modules/api.js index fee584e8..3dbead5b 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -202,12 +202,13 @@ const api = { timeline = 'friends', tag = false, userId = false, - listId = false + listId = false, + statusId = false }) { if (store.state.fetchers[timeline]) return const fetcher = store.state.backendInteractor.startFetchingTimeline({ - timeline, store, userId, listId, tag + timeline, store, userId, listId, statusId, tag }) store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, diff --git a/src/modules/chats.js b/src/modules/chats.js index f28c2603..0f1540db 100644 --- a/src/modules/chats.js +++ b/src/modules/chats.js @@ -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 }) { diff --git a/src/modules/config.js b/src/modules/config.js index eb33f95f..835dcce4 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -1,9 +1,21 @@ 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' 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] @@ -23,10 +35,30 @@ export const multiChoiceProperties = [ 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 + 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, @@ -35,10 +67,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, @@ -55,6 +90,7 @@ export const defaultState = { notificationVisibility: { follows: true, mentions: true, + statuses: true, likes: true, repeats: true, moves: true, @@ -64,7 +100,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, @@ -97,6 +147,12 @@ export const defaultState = { sidebarColumnWidth: '25rem', contentColumnWidth: '45rem', notifsColumnWidth: '25rem', + 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 @@ -115,7 +171,18 @@ export const defaultState = { conversationTreeAdvanced: undefined, // instance default conversationOtherRepliesButton: undefined, // instance default conversationTreeFadeAncestors: undefined, // instance default - maxDepthInThread: undefined // instance default + showExtraNotifications: undefined, // instance default + showExtraNotificationsTip: undefined, // instance default + showChatsInExtraNotifications: undefined, // instance default + showAnnouncementsInExtraNotifications: undefined, // instance default + showFollowRequestsInExtraNotifications: undefined, // instance default + maxDepthInThread: undefined, // instance default + autocompleteSelect: undefined, // instance default + closingDrawerMarksAsSeen: undefined, // instance default + unseenAtTop: undefined, // instance default + ignoreInactionableSeen: undefined, // instance default + useAbsoluteTimeFormat: undefined, // instance defualt + absoluteTimeFormatMinAge: undefined // instance default } // caching the instance default properties @@ -145,8 +212,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] @@ -175,32 +246,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': + 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(this.getters.i18n, value) - dispatch('loadUnicodeEmojiData', value) - Cookies.set( - BACKEND_LANGUAGE_COOKIE_NAME, - localeService.internalToBackendLocaleMulti(value) - ) - break - case 'thirdColumnMode': - dispatch('setLayoutWidth', undefined) - break + } + if (name.startsWith('theme3hacks')) { + dispatch('setTheme', { 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(this.getters.i18n, value) + dispatch('loadUnicodeEmojiData', value) + Cookies.set( + BACKEND_LANGUAGE_COOKIE_NAME, + localeService.internalToBackendLocaleMulti(value) + ) + break + case 'thirdColumnMode': + dispatch('setLayoutWidth', undefined) + break + } } } } diff --git a/src/modules/instance.js b/src/modules/instance.js index 16f72583..994f60a5 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -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' @@ -44,7 +42,7 @@ const defaultState = { registrationOpen: true, server: 'http://localhost:4040/', textlimit: 5000, - themeData: undefined, + themeData: undefined, // used for theme editor v2 vapidPublicKey: undefined, // Stuff from static/config.json @@ -71,6 +69,7 @@ const defaultState = { hideSitename: false, hideUserStats: false, muteBotStatuses: false, + muteSensitiveStatuses: false, modalOnRepeat: false, modalOnUnfollow: false, modalOnBlock: true, @@ -97,13 +96,31 @@ const defaultState = { sidebarRight: false, subjectLineBehavior: 'email', theme: 'pleroma-dark', + 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, + useAbsoluteTimeFormat: false, + absoluteTimeFormatMinAge: '0d', // Nasty stuff customEmoji: [], @@ -122,10 +139,13 @@ const defaultState = { // Feature-set, apparently, not everything here is reported... shoutAvailable: false, pleromaChatMessagesAvailable: false, + pleromaCustomEmojiReactionsAvailable: false, gopherAvailable: false, mediaProxyAvailable: false, suggestionsEnabled: false, suggestionsWeb: '', + quotingAvailable: false, + groupActorAvailable: false, // Html stuff instanceSpecificPanelContent: '', @@ -266,9 +286,6 @@ const instance = { dispatch('initializeSocket') } break - case 'theme': - dispatch('setTheme', value) - break } }, async getStaticEmoji ({ commit }) { @@ -357,25 +374,6 @@ const instance = { console.warn(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 diff --git a/src/modules/interface.js b/src/modules/interface.js index a86193ea..57bfe0c6 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -1,7 +1,18 @@ +import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' +import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' + const defaultState = { + localFonts: null, + themeApplied: false, + temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout + temporaryChangesConfirm: () => {}, // used for applying temporary options + temporaryChangesRevert: () => {}, // used for reverting temporary options settingsModalState: 'hidden', - settingsModalLoaded: false, + settingsModalLoadedUser: false, + settingsModalLoadedAdmin: false, settingsModalTargetTab: null, + settingsModalMode: 'user', settings: { currentSaveStateNotice: null, noticeClearTimeout: null, @@ -11,7 +22,8 @@ const defaultState = { cssFilter: window.CSS && window.CSS.supports && ( window.CSS.supports('filter', 'drop-shadow(0 0)') || window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') - ) + ), + localFonts: typeof window.queryLocalFonts === 'function' }, layoutType: 'normal', globalNotices: [], @@ -33,6 +45,20 @@ const interfaceMod = { state.settings.currentSaveStateNotice = { error: true, errorData: error } } }, + setTemporaryChanges (state, { timeoutId, confirm, revert }) { + state.temporaryChangesTimeoutId = timeoutId + state.temporaryChangesConfirm = confirm + state.temporaryChangesRevert = revert + }, + clearTemporaryChanges (state) { + clearTimeout(state.temporaryChangesTimeoutId) + state.temporaryChangesTimeoutId = null + state.temporaryChangesConfirm = () => {} + state.temporaryChangesRevert = () => {} + }, + setThemeApplied (state) { + state.themeApplied = true + }, setNotificationPermission (state, permission) { state.notificationPermission = permission }, @@ -54,10 +80,17 @@ const interfaceMod = { throw new Error('Illegal minimization state of settings modal') } }, - openSettingsModal (state) { + openSettingsModal (state, value) { + state.settingsModalMode = value state.settingsModalState = 'visible' - if (!state.settingsModalLoaded) { - state.settingsModalLoaded = true + if (value === 'user') { + if (!state.settingsModalLoadedUser) { + state.settingsModalLoadedUser = true + } + } else if (value === 'admin') { + if (!state.settingsModalLoadedAdmin) { + state.settingsModalLoadedAdmin = true + } } }, setSettingsModalTargetTab (state, value) { @@ -77,6 +110,10 @@ const interfaceMod = { }, setLastTimeline (state, value) { state.lastTimeline = value + }, + setFontsList (state, value) { + // Set is used here so that we filter out duplicate fonts (possibly same font but with different weight) + state.localFonts = [...(new Set(value.map(font => font.family))).values()] } }, actions: { @@ -92,8 +129,8 @@ const interfaceMod = { closeSettingsModal ({ commit }) { commit('closeSettingsModal') }, - openSettingsModal ({ commit }) { - commit('openSettingsModal') + openSettingsModal ({ commit }, value = 'user') { + commit('openSettingsModal', value) }, togglePeekSettingsModal ({ commit }) { commit('togglePeekSettingsModal') @@ -103,7 +140,7 @@ const interfaceMod = { }, openSettingsModalTab ({ commit }, value) { commit('setSettingsModalTargetTab', value) - commit('openSettingsModal') + commit('openSettingsModal', 'user') }, pushGlobalNotice ( { commit, dispatch, state }, @@ -151,10 +188,215 @@ const interfaceMod = { commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile) } }, + queryLocalFonts ({ commit, dispatch, state }) { + if (state.localFonts !== null) return + commit('setFontsList', []) + if (!state.browserSupport.localFonts) { + return + } + window + .queryLocalFonts() + .then((fonts) => { + commit('setFontsList', fonts) + }) + .catch((e) => { + dispatch('pushGlobalNotice', { + messageKey: 'settings.style.themes3.font.font_list_unavailable', + messageArgs: { + error: e + }, + level: 'error' + }) + }) + }, setLastTimeline ({ commit }, value) { commit('setLastTimeline', value) + }, + setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) { + const { + theme: instanceThemeName + } = rootState.instance + + const { + theme: userThemeName, + customTheme: userThemeSnapshot, + customThemeSource: userThemeSource, + forceThemeRecompilation, + themeDebug, + theme3hacks + } = rootState.config + + const actualThemeName = userThemeName || instanceThemeName + + const forceRecompile = forceThemeRecompilation || recompile + + let promise = null + + if (themeData) { + promise = Promise.resolve(normalizeThemeData(themeData)) + } else if (themeName) { + promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData)) + } else if (userThemeSource || userThemeSnapshot) { + promise = Promise.resolve(normalizeThemeData({ + _pleroma_theme_version: 2, + theme: userThemeSnapshot, + source: userThemeSource + })) + } else if (actualThemeName && actualThemeName !== 'custom') { + promise = getPreset(actualThemeName).then(themeData => { + const realThemeData = normalizeThemeData(themeData) + if (actualThemeName === instanceThemeName) { + // This sole line is the reason why this whole block is above the recompilation check + commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } }) + } + return realThemeData + }) + } else { + throw new Error('Cannot load any theme!') + } + + // If we're not not forced to recompile try using + // cache (tryLoadCache return true if load successful) + if (!forceRecompile && !themeDebug && tryLoadCache()) { + commit('setThemeApplied') + return + } + + promise + .then(realThemeData => { + const theme2ruleset = convertTheme2To3(realThemeData) + + if (saveData) { + commit('setOption', { name: 'theme', value: themeName || actualThemeName }) + commit('setOption', { name: 'customTheme', value: realThemeData }) + commit('setOption', { name: 'customThemeSource', value: realThemeData }) + } + const hacks = [] + + Object.entries(theme3hacks).forEach(([key, value]) => { + switch (key) { + case 'fonts': { + Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => { + if (!font?.family) return + switch (fontKey) { + case 'interface': + hacks.push({ + component: 'Root', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'input': + hacks.push({ + component: 'Input', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'post': + hacks.push({ + component: 'RichContent', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'monospace': + hacks.push({ + component: 'Root', + directives: { + '--monoFont': 'generic | ' + font.family + } + }) + break + } + }) + break + } + case 'underlay': { + if (value !== 'none') { + const newRule = { + component: 'Underlay', + directives: {} + } + if (value === 'opaque') { + newRule.directives.opacity = 1 + newRule.directives.background = '--wallpaper' + } + if (value === 'transparent') { + newRule.directives.opacity = 0 + } + hacks.push(newRule) + } + break + } + } + }) + + const ruleset = [ + ...theme2ruleset, + ...hacks + ] + + applyTheme( + ruleset, + () => commit('setThemeApplied'), + themeDebug + ) + }) + + return promise } } } export default interfaceMod + +export const normalizeThemeData = (input) => { + if (Array.isArray(input)) { + const themeData = { colors: {} } + themeData.colors.bg = input[1] + themeData.colors.fg = input[2] + themeData.colors.text = input[3] + themeData.colors.link = input[4] + themeData.colors.cRed = input[5] + themeData.colors.cGreen = input[6] + themeData.colors.cBlue = input[7] + themeData.colors.cOrange = input[8] + return generatePreset(themeData).theme + } + + let themeData, themeSource + + if (input.themeFileVerison === 1) { + // this might not be even used at all, some leftover of unimplemented code in V2 editor + return generatePreset(input).theme + } else if ( + Object.prototype.hasOwnProperty.call(input, '_pleroma_theme_version') || + Object.prototype.hasOwnProperty.call(input, 'source') || + Object.prototype.hasOwnProperty.call(input, 'theme') + ) { + // We got passed a full theme file + themeData = input.theme + themeSource = input.source + } else if (Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion')) { + // We got passed a source/snapshot + themeData = input + themeSource = input + } + // New theme presets don't have 'theme' property, they use 'source' + + let out // shout, shout let it all out + if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) { + // There are some themes in wild that have completely broken source + out = { ...(themeData || {}), ...themeSource } + } else { + out = themeData + } + + // generatePreset here basically creates/updates "snapshot", + // while also fixing the 2.2 -> 2.3 colors/shadows/etc + return generatePreset(out).theme +} diff --git a/src/modules/notifications.js b/src/modules/notifications.js new file mode 100644 index 00000000..c1f5a63e --- /dev/null +++ b/src/modules/notifications.js @@ -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 diff --git a/src/modules/postStatus.js b/src/modules/postStatus.js index 638c1fb2..d3bea137 100644 --- a/src/modules/postStatus.js +++ b/src/modules/postStatus.js @@ -10,6 +10,9 @@ const postStatus = { }, closePostStatusModal (state) { state.modalActivated = false + }, + resetPostStatusModal (state) { + state.params = null } }, actions: { @@ -18,6 +21,9 @@ const postStatus = { }, closePostStatusModal ({ commit }) { commit('closePostStatusModal') + }, + resetPostStatusModal ({ commit }) { + commit('resetPostStatusModal') } } } diff --git a/src/modules/serverSideConfig.js b/src/modules/profileConfig.js index 476263bc..2cb2014a 100644 --- a/src/modules/serverSideConfig.js +++ b/src/modules/profileConfig.js @@ -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 diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index c933ce8d..c55f54fd 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -419,7 +419,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 } } diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 77dd7e1c..462def22 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -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' const emptyTl = (userId = 0) => ({ @@ -36,21 +31,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(), @@ -120,8 +106,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 @@ -137,22 +139,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)) { @@ -229,6 +215,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us timelineObject.newStatusCount += 1 } + if (status.quote) { + addStatus(status.quote, /* showImmediately = */ false, /* addToTimeline = */ false) + } + return status } @@ -282,20 +272,6 @@ 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 }, @@ -317,52 +293,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') { - 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.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) { @@ -375,7 +305,6 @@ const removeStatus = (state, { timeline, userId }) => { export const mutations = { addNewStatuses, - addNewNotifications, removeStatus, showNewStatuses (state, { timeline }) { const oldTimeline = (state.timelines[timeline]) @@ -397,9 +326,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] @@ -482,31 +408,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 }, @@ -588,23 +489,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] })) @@ -615,9 +502,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] @@ -690,31 +587,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 }), @@ -747,7 +619,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 }) } diff --git a/src/modules/users.js b/src/modules/users.js index a1316ba2..b8f49f15 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -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' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -195,9 +195,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) @@ -244,6 +250,7 @@ export const mutations = { signUpPending (state) { state.signUpPending = true state.signUpErrors = [] + state.signUpNotice = {} }, signUpSuccess (state) { state.signUpPending = false @@ -251,6 +258,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 } } @@ -281,6 +294,7 @@ export const defaultState = { usersByNameObject: {}, signUpPending: false, signUpErrors: [], + signUpNotice: {}, relationships: {} } @@ -320,10 +334,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 }) @@ -346,10 +370,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 }) @@ -472,7 +506,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) @@ -498,9 +532,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) @@ -551,6 +592,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) => { @@ -565,57 +607,57 @@ const users = { commit('setServerSideStorage', user) commit('addNewUsers', [user]) - store.dispatch('fetchEmoji') + dispatch('fetchEmoji') getNotificationPermission() .then(permission => commit('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') 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') - store.dispatch('setLayoutWidth', windowWidth()) - store.dispatch('setLayoutHeight', windowHeight()) + dispatch('setLayoutWidth', windowWidth()) + dispatch('setLayoutHeight', windowHeight()) // Fetch our friends store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) @@ -624,6 +666,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 { @@ -634,7 +682,7 @@ const users = { resolve() }) .catch((error) => { - console.log(error) + console.error(error) commit('endLogin') reject(new Error('Failed to connect to server, try again')) }) |
