From 3c5384f318da0100bbb84d66f3f33fba2e5da8cc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Oct 2021 13:18:55 -0500 Subject: [PATCH 1/7] localforage: add localforage, remember a fetched instance --- app/soapbox/actions/instance.js | 44 +++++++++++++++++++++++++++++-- app/soapbox/containers/soapbox.js | 4 +-- app/soapbox/main.js | 10 +++++++ app/soapbox/reducers/instance.js | 41 +++++++++++++++++++++++++--- package.json | 1 + yarn.lock | 19 +++++++++++++ 6 files changed, 111 insertions(+), 8 deletions(-) diff --git a/app/soapbox/actions/instance.js b/app/soapbox/actions/instance.js index 7e17f6d71..d4e084f51 100644 --- a/app/soapbox/actions/instance.js +++ b/app/soapbox/actions/instance.js @@ -1,12 +1,41 @@ import api from '../api'; import { get } from 'lodash'; import { parseVersion } from 'soapbox/utils/features'; +import { getAuthUserUrl } from 'soapbox/utils/auth'; +import localforage from 'localforage'; + +export const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS'; +export const INSTANCE_REMEMBER_SUCCESS = 'INSTANCE_REMEMBER_SUCCESS'; +export const INSTANCE_FETCH_FAIL = 'INSTANCE_FETCH_FAIL'; -export const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS'; -export const INSTANCE_FETCH_FAIL = 'INSTANCE_FETCH_FAIL'; export const NODEINFO_FETCH_SUCCESS = 'NODEINFO_FETCH_SUCCESS'; export const NODEINFO_FETCH_FAIL = 'NODEINFO_FETCH_FAIL'; +const getMeUrl = state => { + const me = state.get('me'); + return state.getIn(['accounts', me, 'url']); +}; + +// Figure out the appropriate instance to fetch depending on the state +const getHost = state => { + const accountUrl = getMeUrl(state) || getAuthUserUrl(state); + + try { + return new URL(accountUrl).host; + } catch { + return null; + } +}; + +export function rememberInstance(host) { + return (dispatch, getState) => { + return localforage.getItem(`instance:${host}`).then(instance => { + dispatch({ type: INSTANCE_REMEMBER_SUCCESS, instance }); + return instance; + }); + }; +} + export function fetchInstance() { return (dispatch, getState) => { return api(getState).get('/api/v1/instance').then(response => { @@ -21,6 +50,17 @@ export function fetchInstance() { }; } +// Tries to remember the instance from browser storage before fetching it +export function loadInstance() { + return (dispatch, getState) => { + const host = getHost(getState()); + + return dispatch(rememberInstance(host)).then(instance => { + return dispatch(fetchInstance()); + }); + }; +} + export function fetchNodeinfo() { return (dispatch, getState) => { api(getState).get('/nodeinfo/2.1.json').then(response => { diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index 7653ee865..279cad403 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -16,7 +16,7 @@ import UI from '../features/ui'; import { preload } from '../actions/preload'; import { IntlProvider } from 'react-intl'; import ErrorBoundary from '../components/error_boundary'; -import { fetchInstance } from 'soapbox/actions/instance'; +import { loadInstance } from 'soapbox/actions/instance'; import { fetchSoapboxConfig } from 'soapbox/actions/soapbox'; import { fetchMe } from 'soapbox/actions/me'; import PublicLayout from 'soapbox/features/public_layout'; @@ -38,7 +38,7 @@ store.dispatch(preload()); store.dispatch(fetchMe()) .then(() => { // Postpone for authenticated fetch - store.dispatch(fetchInstance()); + store.dispatch(loadInstance()); store.dispatch(fetchSoapboxConfig()); }) .catch(() => {}); diff --git a/app/soapbox/main.js b/app/soapbox/main.js index 91822efef..d517883af 100644 --- a/app/soapbox/main.js +++ b/app/soapbox/main.js @@ -10,6 +10,7 @@ import ReactDOM from 'react-dom'; import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime'; import * as perf from './performance'; import * as monitoring from './monitoring'; +import localforage from 'localforage'; import ready from './ready'; import { NODE_ENV } from 'soapbox/build_config'; @@ -19,6 +20,15 @@ function main() { // Sentry monitoring.start(); + // localForage + // https://localforage.github.io/localForage/#settings-api-config + localforage.config({ + name: 'soapbox', + description: 'Soapbox offline data store', + driver: localforage.INDEXEDDB, + storeName: 'keyvaluepairs', + }); + ready(() => { const mountNode = document.getElementById('soapbox'); diff --git a/app/soapbox/reducers/instance.js b/app/soapbox/reducers/instance.js index 189bfe675..6a3ae5e78 100644 --- a/app/soapbox/reducers/instance.js +++ b/app/soapbox/reducers/instance.js @@ -1,4 +1,5 @@ import { + INSTANCE_REMEMBER_SUCCESS, INSTANCE_FETCH_SUCCESS, INSTANCE_FETCH_FAIL, NODEINFO_FETCH_SUCCESS, @@ -7,6 +8,7 @@ import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload'; import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { ConfigDB } from 'soapbox/utils/config_db'; +import localforage from 'localforage'; const nodeinfoToInstance = nodeinfo => { // Match Pleroma's develop branch @@ -37,9 +39,17 @@ const initialState = ImmutableMap({ version: '0.0.0', }); +const importInstance = (state, instance) => { + return initialState.mergeDeep(instance); +}; + +const importNodeinfo = (state, nodeinfo) => { + return nodeinfoToInstance(nodeinfo).mergeDeep(state); +}; + const preloadImport = (state, action, path) => { - const data = action.data[path]; - return data ? initialState.mergeDeep(fromJS(data)) : state; + const instance = action.data[path]; + return instance ? importInstance(state, fromJS(instance)) : state; }; const getConfigValue = (instanceConfig, key) => { @@ -80,16 +90,39 @@ const handleAuthFetch = state => { }).merge(state); }; +const getHost = instance => { + try { + return new URL(instance.uri).host; + } catch { + try { + return new URL(`https://${instance.uri}`).host; + } catch { + return null; + } + } +}; + +const persistInstance = instance => { + const host = getHost(instance); + + if (host) { + localforage.setItem(`instance:${host}`, instance); + } +}; + export default function instance(state = initialState, action) { switch(action.type) { case PLEROMA_PRELOAD_IMPORT: return preloadImport(state, action, '/api/v1/instance'); + case INSTANCE_REMEMBER_SUCCESS: + return importInstance(state, fromJS(action.instance)); case INSTANCE_FETCH_SUCCESS: - return initialState.mergeDeep(fromJS(action.instance)); + persistInstance(action.instance); + return importInstance(state, fromJS(action.instance)); case INSTANCE_FETCH_FAIL: return action.error.response.status === 401 ? handleAuthFetch(state) : state; case NODEINFO_FETCH_SUCCESS: - return nodeinfoToInstance(fromJS(action.nodeinfo)).mergeDeep(state); + return importNodeinfo(state, fromJS(action.nodeinfo)); case ADMIN_CONFIG_UPDATE_REQUEST: case ADMIN_CONFIG_UPDATE_SUCCESS: return importConfigs(state, fromJS(action.configs)); diff --git a/package.json b/package.json index f6544915c..be051d030 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "jest-transform-stub": "^2.0.0", "jsdoc": "~3.6.7", "line-awesome": "^1.3.0", + "localforage": "^1.10.0", "lodash": "^4.7.11", "mark-loader": "^0.1.6", "marky": "^1.2.1", diff --git a/yarn.lock b/yarn.lock index 62d92d092..06faba1a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4840,6 +4840,11 @@ ignore@^5.1.4, ignore@^5.1.8: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + immutable@^4.0.0-rc.14: version "4.0.0-rc.14" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.14.tgz#29ba96631ec10867d1348515ac4e6bdba462f071" @@ -6048,6 +6053,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + dependencies: + immediate "~3.0.5" + line-awesome@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/line-awesome/-/line-awesome-1.3.0.tgz#51d59fe311ed040d22d8e80d3aa0b9a4b57e6cd3" @@ -6141,6 +6153,13 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +localforage@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" From c8cec8fdac92d1b047cd68902eda3d0d49aadb41 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Oct 2021 15:17:02 -0500 Subject: [PATCH 2/7] localforage: create custom KVStore instance, refactor Instance actions --- app/soapbox/actions/instance.js | 73 +++++++++++++------------------- app/soapbox/main.js | 10 ----- app/soapbox/reducers/auth.js | 8 ++++ app/soapbox/reducers/instance.js | 4 +- app/soapbox/storage/kv_store.js | 24 +++++++++++ 5 files changed, 63 insertions(+), 56 deletions(-) create mode 100644 app/soapbox/storage/kv_store.js diff --git a/app/soapbox/actions/instance.js b/app/soapbox/actions/instance.js index d4e084f51..d92a0604d 100644 --- a/app/soapbox/actions/instance.js +++ b/app/soapbox/actions/instance.js @@ -2,12 +2,17 @@ import api from '../api'; import { get } from 'lodash'; import { parseVersion } from 'soapbox/utils/features'; import { getAuthUserUrl } from 'soapbox/utils/auth'; -import localforage from 'localforage'; +import KVStore from 'soapbox/storage/kv_store'; -export const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS'; +export const INSTANCE_FETCH_REQUEST = 'INSTANCE_FETCH_REQUEST'; +export const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS'; +export const INSTANCE_FETCH_FAIL = 'INSTANCE_FETCH_FAIL'; + +export const INSTANCE_REMEMBER_REQUEST = 'INSTANCE_REMEMBER_REQUEST'; export const INSTANCE_REMEMBER_SUCCESS = 'INSTANCE_REMEMBER_SUCCESS'; -export const INSTANCE_FETCH_FAIL = 'INSTANCE_FETCH_FAIL'; +export const INSTANCE_REMEMBER_FAIL = 'INSTANCE_REMEMBER_FAIL'; +export const NODEINFO_FETCH_REQUEST = 'NODEINFO_FETCH_REQUEST'; export const NODEINFO_FETCH_SUCCESS = 'NODEINFO_FETCH_SUCCESS'; export const NODEINFO_FETCH_FAIL = 'NODEINFO_FETCH_FAIL'; @@ -29,23 +34,32 @@ const getHost = state => { export function rememberInstance(host) { return (dispatch, getState) => { - return localforage.getItem(`instance:${host}`).then(instance => { - dispatch({ type: INSTANCE_REMEMBER_SUCCESS, instance }); + dispatch({ type: INSTANCE_REMEMBER_REQUEST, host }); + return KVStore.getItemOrError(`instance:${host}`).then(instance => { + dispatch({ type: INSTANCE_REMEMBER_SUCCESS, host, instance }); return instance; + }).catch(error => { + dispatch({ type: INSTANCE_REMEMBER_FAIL, host, error, skipAlert: true }); }); }; } +// We may need to fetch nodeinfo on Pleroma < 2.1 +const needsNodeinfo = instance => { + const v = parseVersion(get(instance, 'version')); + return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']); +}; + export function fetchInstance() { return (dispatch, getState) => { - return api(getState).get('/api/v1/instance').then(response => { - dispatch(importInstance(response.data)); - const v = parseVersion(get(response.data, 'version')); - if (v.software === 'Pleroma' && !get(response.data, ['pleroma', 'metadata'])) { + dispatch({ type: INSTANCE_FETCH_REQUEST }); + return api(getState).get('/api/v1/instance').then(({ data: instance }) => { + dispatch({ type: INSTANCE_FETCH_SUCCESS, instance }); + if (needsNodeinfo(instance)) { dispatch(fetchNodeinfo()); // Pleroma < 2.1 backwards compatibility } }).catch(error => { - dispatch(instanceFail(error)); + dispatch({ type: INSTANCE_FETCH_FAIL, error, skipAlert: true }); }); }; } @@ -55,7 +69,7 @@ export function loadInstance() { return (dispatch, getState) => { const host = getHost(getState()); - return dispatch(rememberInstance(host)).then(instance => { + return dispatch(rememberInstance(host)).finally(instance => { return dispatch(fetchInstance()); }); }; @@ -63,40 +77,11 @@ export function loadInstance() { export function fetchNodeinfo() { return (dispatch, getState) => { - api(getState).get('/nodeinfo/2.1.json').then(response => { - dispatch(importNodeinfo(response.data)); + dispatch({ type: NODEINFO_FETCH_REQUEST }); + api(getState).get('/nodeinfo/2.1.json').then(({ data: nodeinfo }) => { + dispatch({ type: NODEINFO_FETCH_SUCCESS, nodeinfo }); }).catch(error => { - dispatch(nodeinfoFail(error)); + dispatch({ type: NODEINFO_FETCH_FAIL, error, skipAlert: true }); }); }; } - -export function importInstance(instance) { - return { - type: INSTANCE_FETCH_SUCCESS, - instance, - }; -} - -export function instanceFail(error) { - return { - type: INSTANCE_FETCH_FAIL, - error, - skipAlert: true, - }; -} - -export function importNodeinfo(nodeinfo) { - return { - type: NODEINFO_FETCH_SUCCESS, - nodeinfo, - }; -} - -export function nodeinfoFail(error) { - return { - type: NODEINFO_FETCH_FAIL, - error, - skipAlert: true, - }; -} diff --git a/app/soapbox/main.js b/app/soapbox/main.js index d517883af..91822efef 100644 --- a/app/soapbox/main.js +++ b/app/soapbox/main.js @@ -10,7 +10,6 @@ import ReactDOM from 'react-dom'; import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime'; import * as perf from './performance'; import * as monitoring from './monitoring'; -import localforage from 'localforage'; import ready from './ready'; import { NODE_ENV } from 'soapbox/build_config'; @@ -20,15 +19,6 @@ function main() { // Sentry monitoring.start(); - // localForage - // https://localforage.github.io/localForage/#settings-api-config - localforage.config({ - name: 'soapbox', - description: 'Soapbox offline data store', - driver: localforage.INDEXEDDB, - storeName: 'keyvaluepairs', - }); - ready(() => { const mountNode = document.getElementById('soapbox'); diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index c5a7cf5bc..30f02d89a 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -13,6 +13,7 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { validId, isURL } from 'soapbox/utils/auth'; import { trim } from 'lodash'; import { FE_SUBDIRECTORY } from 'soapbox/build_config'; +import KVStore from 'soapbox/storage/kv_store'; const defaultState = ImmutableMap({ app: ImmutableMap(), @@ -254,6 +255,12 @@ const importMastodonPreload = (state, data) => { }); }; +const persistAuthAccount = account => { + if (account && account.url) { + KVStore.setItem(`authAccount:${account.url}`, account).catch(console.error); + } +}; + const reducer = (state, action) => { switch(action.type) { case AUTH_APP_CREATED: @@ -265,6 +272,7 @@ const reducer = (state, action) => { case AUTH_LOGGED_OUT: return deleteUser(state, action.account); case VERIFY_CREDENTIALS_SUCCESS: + persistAuthAccount(action.account); return importCredentials(state, action.token, action.account); case VERIFY_CREDENTIALS_FAIL: return [401, 403].includes(action.error.response.status) ? deleteToken(state, action.token) : state; diff --git a/app/soapbox/reducers/instance.js b/app/soapbox/reducers/instance.js index 6a3ae5e78..5866791f2 100644 --- a/app/soapbox/reducers/instance.js +++ b/app/soapbox/reducers/instance.js @@ -8,7 +8,7 @@ import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload'; import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { ConfigDB } from 'soapbox/utils/config_db'; -import localforage from 'localforage'; +import KVStore from 'soapbox/storage/kv_store'; const nodeinfoToInstance = nodeinfo => { // Match Pleroma's develop branch @@ -106,7 +106,7 @@ const persistInstance = instance => { const host = getHost(instance); if (host) { - localforage.setItem(`instance:${host}`, instance); + KVStore.setItem(`instance:${host}`, instance).catch(console.error); } }; diff --git a/app/soapbox/storage/kv_store.js b/app/soapbox/storage/kv_store.js new file mode 100644 index 000000000..3c957fdc1 --- /dev/null +++ b/app/soapbox/storage/kv_store.js @@ -0,0 +1,24 @@ +import localforage from 'localforage'; + +// localForage +// https://localforage.github.io/localForage/#settings-api-config +export const KVStore = localforage.createInstance({ + name: 'soapbox', + description: 'Soapbox offline data store', + driver: localforage.INDEXEDDB, + storeName: 'keyvaluepairs', +}); + +// localForage returns 'null' when a key isn't found. +// In the Redux action flow, we want it to fail harder. +KVStore.getItemOrError = key => { + return KVStore.getItem(key).then(value => { + if (value === null) { + throw new Error(`KVStore: null value for key ${key}`); + } else { + return value; + } + }); +}; + +export default KVStore; From 0b94774fbe83c03c5c3daa268b749079d3c26245 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Oct 2021 16:27:36 -0500 Subject: [PATCH 3/7] localforage: remember auth accounts --- app/soapbox/actions/auth.js | 27 +++++++++++++++++++++++++++ app/soapbox/actions/instance.js | 2 +- app/soapbox/actions/me.js | 5 ++--- app/soapbox/middleware/errors.js | 15 ++++++++------- app/soapbox/reducers/accounts_meta.js | 3 ++- app/soapbox/reducers/me.js | 10 ++++++++-- 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 68b60205c..128df1027 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -19,6 +19,7 @@ import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth'; import sourceCode from 'soapbox/utils/code'; import { getFeatures } from 'soapbox/utils/features'; import { isStandalone } from 'soapbox/utils/state'; +import KVStore from 'soapbox/storage/kv_store'; export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; @@ -31,6 +32,10 @@ export const VERIFY_CREDENTIALS_REQUEST = 'VERIFY_CREDENTIALS_REQUEST'; export const VERIFY_CREDENTIALS_SUCCESS = 'VERIFY_CREDENTIALS_SUCCESS'; export const VERIFY_CREDENTIALS_FAIL = 'VERIFY_CREDENTIALS_FAIL'; +export const AUTH_ACCOUNT_REMEMBER_REQUEST = 'AUTH_ACCOUNT_REMEMBER_REQUEST'; +export const AUTH_ACCOUNT_REMEMBER_SUCCESS = 'AUTH_ACCOUNT_REMEMBER_SUCCESS'; +export const AUTH_ACCOUNT_REMEMBER_FAIL = 'AUTH_ACCOUNT_REMEMBER_FAIL'; + export const messages = defineMessages({ loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' }, invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' }, @@ -157,6 +162,28 @@ export function verifyCredentials(token, accountUrl) { }; } +export function rememberAuthAccount(accountUrl) { + return (dispatch, getState) => { + dispatch({ type: AUTH_ACCOUNT_REMEMBER_REQUEST, accountUrl }); + return KVStore.getItemOrError(`authAccount:${accountUrl}`).then(account => { + dispatch(importFetchedAccount(account)); + dispatch({ type: AUTH_ACCOUNT_REMEMBER_SUCCESS, account, accountUrl }); + if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account)); + return account; + }).catch(error => { + dispatch({ type: AUTH_ACCOUNT_REMEMBER_FAIL, error, accountUrl, skipAlert: true }); + }); + }; +} + +export function loadCredentials(token, accountUrl) { + return (dispatch, getState) => { + return dispatch(rememberAuthAccount(accountUrl)).finally(() => { + return dispatch(verifyCredentials(token, accountUrl)); + }); + }; +} + export function logIn(intl, username, password) { return (dispatch, getState) => { return dispatch(createAppAndToken()).then(() => { diff --git a/app/soapbox/actions/instance.js b/app/soapbox/actions/instance.js index d92a0604d..ec77742b2 100644 --- a/app/soapbox/actions/instance.js +++ b/app/soapbox/actions/instance.js @@ -69,7 +69,7 @@ export function loadInstance() { return (dispatch, getState) => { const host = getHost(getState()); - return dispatch(rememberInstance(host)).finally(instance => { + return dispatch(rememberInstance(host)).finally(() => { return dispatch(fetchInstance()); }); }; diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js index 91388b65c..2dcf36ab9 100644 --- a/app/soapbox/actions/me.js +++ b/app/soapbox/actions/me.js @@ -1,6 +1,6 @@ import api from '../api'; import { importFetchedAccount } from './importer'; -import { verifyCredentials } from './auth'; +import { loadCredentials } from './auth'; import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth'; export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST'; @@ -38,7 +38,7 @@ export function fetchMe() { } dispatch(fetchMeRequest()); - return dispatch(verifyCredentials(token, accountUrl)).catch(error => { + return dispatch(loadCredentials(token, accountUrl)).catch(error => { dispatch(fetchMeFail(error)); }); }; @@ -66,7 +66,6 @@ export function fetchMeRequest() { export function fetchMeSuccess(me) { return (dispatch, getState) => { - dispatch(importFetchedAccount(me)); dispatch({ type: ME_FETCH_SUCCESS, me, diff --git a/app/soapbox/middleware/errors.js b/app/soapbox/middleware/errors.js index 1e36cc55f..edec65a31 100644 --- a/app/soapbox/middleware/errors.js +++ b/app/soapbox/middleware/errors.js @@ -1,15 +1,16 @@ import { showAlertForError } from '../actions/alerts'; -const defaultFailSuffix = 'FAIL'; +const isFailType = type => type.endsWith('_FAIL'); +const isRememberFailType = type => type.endsWith('_REMEMBER_FAIL'); + +const shouldShowError = ({ type, skipAlert }) => { + return !skipAlert && isFailType(type) && !isRememberFailType(type); +}; export default function errorsMiddleware() { return ({ dispatch }) => next => action => { - if (action.type && !action.skipAlert) { - const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); - - if (action.type.match(isFail)) { - dispatch(showAlertForError(action.error)); - } + if (shouldShowError(action)) { + dispatch(showAlertForError(action.error)); } return next(action); diff --git a/app/soapbox/reducers/accounts_meta.js b/app/soapbox/reducers/accounts_meta.js index d26ce0911..731568313 100644 --- a/app/soapbox/reducers/accounts_meta.js +++ b/app/soapbox/reducers/accounts_meta.js @@ -4,7 +4,7 @@ */ import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me'; -import { VERIFY_CREDENTIALS_SUCCESS } from 'soapbox/actions/auth'; +import { VERIFY_CREDENTIALS_SUCCESS, AUTH_ACCOUNT_REMEMBER_SUCCESS } from 'soapbox/actions/auth'; import { Map as ImmutableMap, fromJS } from 'immutable'; const initialState = ImmutableMap(); @@ -24,6 +24,7 @@ export default function accounts_meta(state = initialState, action) { case ME_PATCH_SUCCESS: return importAccount(state, fromJS(action.me)); case VERIFY_CREDENTIALS_SUCCESS: + case AUTH_ACCOUNT_REMEMBER_SUCCESS: return importAccount(state, fromJS(action.account)); default: return state; diff --git a/app/soapbox/reducers/me.js b/app/soapbox/reducers/me.js index d42ace12d..ac6a23402 100644 --- a/app/soapbox/reducers/me.js +++ b/app/soapbox/reducers/me.js @@ -4,7 +4,11 @@ import { ME_FETCH_SKIP, ME_PATCH_SUCCESS, } from '../actions/me'; -import { AUTH_LOGGED_OUT, VERIFY_CREDENTIALS_SUCCESS } from '../actions/auth'; +import { + AUTH_LOGGED_OUT, + AUTH_ACCOUNT_REMEMBER_SUCCESS, + VERIFY_CREDENTIALS_SUCCESS, +} from '../actions/auth'; const initialState = null; @@ -14,11 +18,13 @@ export default function me(state = initialState, action) { case ME_PATCH_SUCCESS: return action.me.id; case VERIFY_CREDENTIALS_SUCCESS: + case AUTH_ACCOUNT_REMEMBER_SUCCESS: return state || action.account.id; - case ME_FETCH_FAIL: case ME_FETCH_SKIP: case AUTH_LOGGED_OUT: return false; + case ME_FETCH_FAIL: + return [401, 403].includes(action.error.response.status) ? false : state; default: return state; } From 97cc7eb804481dccef2097c7f7fbcb467f0bc46f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Oct 2021 16:36:38 -0500 Subject: [PATCH 4/7] Streaming: fail gracefully when WebSocket construction fails (mainly for Bromite browser) --- app/soapbox/stream.js | 56 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/app/soapbox/stream.js b/app/soapbox/stream.js index 15764f4ce..c6d648d7d 100644 --- a/app/soapbox/stream.js +++ b/app/soapbox/stream.js @@ -26,37 +26,45 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ } }; - const subscription = getStream(streamingAPIBaseURL, accessToken, path, { - connected() { - if (pollingRefresh) { - clearPolling(); - } + let subscription; - onConnect(); - }, + // If the WebSocket fails to be created, don't crash the whole page, + // just proceed without a subscription. + try { + subscription = getStream(streamingAPIBaseURL, accessToken, path, { + connected() { + if (pollingRefresh) { + clearPolling(); + } - disconnected() { - if (pollingRefresh) { - polling = setTimeout(() => setupPolling(), randomIntUpTo(40000)); - } + onConnect(); + }, - onDisconnect(); - }, + disconnected() { + if (pollingRefresh) { + polling = setTimeout(() => setupPolling(), randomIntUpTo(40000)); + } - received(data) { - onReceive(data); - }, + onDisconnect(); + }, - reconnected() { - if (pollingRefresh) { - clearPolling(); - pollingRefresh(dispatch); - } + received(data) { + onReceive(data); + }, - onConnect(); - }, + reconnected() { + if (pollingRefresh) { + clearPolling(); + pollingRefresh(dispatch); + } - }); + onConnect(); + }, + + }); + } catch (e) { + console.error(e); + } const disconnect = () => { if (subscription) { From a16b246d41646e28d52998647439d92df69f405f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Oct 2021 17:07:10 -0500 Subject: [PATCH 5/7] ServiceWorker: do serve index.html from the ServiceWorker after all, add appShell option --- webpack/production.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/webpack/production.js b/webpack/production.js index e74d09dc2..f3aaaaafe 100644 --- a/webpack/production.js +++ b/webpack/production.js @@ -74,9 +74,6 @@ module.exports = merge(sharedConfig, { '**/*.ogg', '**/*.oga', '**/*.mp3', - // Don't serve index.html - // https://github.com/bromite/bromite/issues/1294 - 'index.html', '404.html', 'assets-manifest.json', // It would be nice to serve these, but they bloat up sw.js @@ -89,6 +86,7 @@ module.exports = merge(sharedConfig, { minify: true, }, safeToUseOptionalCaches: true, + appShell: join(FE_SUBDIRECTORY, '/'), }), ], }); From 5a19d5261780b1834c84602fba888cb728d07669 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Oct 2021 17:50:03 -0500 Subject: [PATCH 6/7] errorsMiddleware: only alert when the error has a response (eg not when offline) --- app/soapbox/middleware/errors.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/soapbox/middleware/errors.js b/app/soapbox/middleware/errors.js index edec65a31..b5d9a36a3 100644 --- a/app/soapbox/middleware/errors.js +++ b/app/soapbox/middleware/errors.js @@ -3,8 +3,10 @@ import { showAlertForError } from '../actions/alerts'; const isFailType = type => type.endsWith('_FAIL'); const isRememberFailType = type => type.endsWith('_REMEMBER_FAIL'); -const shouldShowError = ({ type, skipAlert }) => { - return !skipAlert && isFailType(type) && !isRememberFailType(type); +const hasResponse = error => Boolean(error && error.response); + +const shouldShowError = ({ type, skipAlert, error }) => { + return !skipAlert && hasResponse(error) && isFailType(type) && !isRememberFailType(type); }; export default function errorsMiddleware() { From e42030daaebad5e7dc5ac50668fa200e28d18b71 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Oct 2021 17:50:35 -0500 Subject: [PATCH 7/7] Always check error.response before error.response.status --- app/soapbox/actions/external_auth.js | 2 +- app/soapbox/reducers/auth.js | 10 +++++++++- app/soapbox/reducers/instance.js | 10 +++++++++- app/soapbox/reducers/me.js | 10 +++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.js index 194b27cb1..d486b4026 100644 --- a/app/soapbox/actions/external_auth.js +++ b/app/soapbox/actions/external_auth.js @@ -20,7 +20,7 @@ const fetchExternalInstance = baseURL => { .get('/api/v1/instance') .then(({ data: instance }) => fromJS(instance)) .catch(error => { - if (error.response.status === 401) { + if (error.response && error.response.status === 401) { // Authenticated fetch is enabled. // Continue with a limited featureset. return ImmutableMap({ version: '0.0.0' }); diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 30f02d89a..8a74a90e2 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -261,6 +261,14 @@ const persistAuthAccount = account => { } }; +const deleteForbiddenToken = (state, error, token) => { + if (error.response && [401, 403].includes(error.response.status)) { + return deleteToken(state, token); + } else { + return state; + } +}; + const reducer = (state, action) => { switch(action.type) { case AUTH_APP_CREATED: @@ -275,7 +283,7 @@ const reducer = (state, action) => { persistAuthAccount(action.account); return importCredentials(state, action.token, action.account); case VERIFY_CREDENTIALS_FAIL: - return [401, 403].includes(action.error.response.status) ? deleteToken(state, action.token) : state; + return deleteForbiddenToken(state, action.error, action.token); case SWITCH_ACCOUNT: return state.set('me', action.account.get('url')); case ME_FETCH_SKIP: diff --git a/app/soapbox/reducers/instance.js b/app/soapbox/reducers/instance.js index 5866791f2..ff94b5123 100644 --- a/app/soapbox/reducers/instance.js +++ b/app/soapbox/reducers/instance.js @@ -110,6 +110,14 @@ const persistInstance = instance => { } }; +const handleInstanceFetchFail = (state, error) => { + if (error.response && error.response.status === 401) { + return handleAuthFetch(state); + } else { + return state; + } +}; + export default function instance(state = initialState, action) { switch(action.type) { case PLEROMA_PRELOAD_IMPORT: @@ -120,7 +128,7 @@ export default function instance(state = initialState, action) { persistInstance(action.instance); return importInstance(state, fromJS(action.instance)); case INSTANCE_FETCH_FAIL: - return action.error.response.status === 401 ? handleAuthFetch(state) : state; + return handleInstanceFetchFail(state, action.error); case NODEINFO_FETCH_SUCCESS: return importNodeinfo(state, fromJS(action.nodeinfo)); case ADMIN_CONFIG_UPDATE_REQUEST: diff --git a/app/soapbox/reducers/me.js b/app/soapbox/reducers/me.js index ac6a23402..021855a14 100644 --- a/app/soapbox/reducers/me.js +++ b/app/soapbox/reducers/me.js @@ -12,6 +12,14 @@ import { const initialState = null; +const handleForbidden = (state, error) => { + if (error.response && [401, 403].includes(error.response.status)) { + return false; + } else { + return state; + } +}; + export default function me(state = initialState, action) { switch(action.type) { case ME_FETCH_SUCCESS: @@ -24,7 +32,7 @@ export default function me(state = initialState, action) { case AUTH_LOGGED_OUT: return false; case ME_FETCH_FAIL: - return [401, 403].includes(action.error.response.status) ? false : state; + return handleForbidden(state, action.error); default: return state; }