From 0162eac662abfbffaee7cb68754890f88b18278d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 23 Mar 2021 19:06:55 -0500 Subject: [PATCH 01/46] Refactor auth to support multiple accounts --- app/soapbox/actions/auth.js | 31 +++++++ app/soapbox/actions/me.js | 16 ++-- app/soapbox/api.js | 11 ++- .../ui/components/profile_dropdown.js | 91 +++++++++++++++++++ .../features/ui/components/tabs_bar.js | 4 +- app/soapbox/reducers/auth.js | 9 +- app/soapbox/reducers/me.js | 1 - app/soapbox/reducers/meta.js | 18 ++-- app/styles/components/dropdown-menu.scss | 1 - 9 files changed, 162 insertions(+), 20 deletions(-) create mode 100644 app/soapbox/features/ui/components/profile_dropdown.js diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index a0bb9b9a2..26662803c 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -1,11 +1,17 @@ import api from '../api'; import snackbar from 'soapbox/actions/snackbar'; +export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; + export const AUTH_APP_CREATED = 'AUTH_APP_CREATED'; export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED'; export const AUTH_LOGGED_IN = 'AUTH_LOGGED_IN'; export const AUTH_LOGGED_OUT = 'AUTH_LOGGED_OUT'; +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_REGISTER_REQUEST = 'AUTH_REGISTER_REQUEST'; export const AUTH_REGISTER_SUCCESS = 'AUTH_REGISTER_SUCCESS'; export const AUTH_REGISTER_FAIL = 'AUTH_REGISTER_FAIL'; @@ -127,6 +133,27 @@ export function otpVerify(code, mfa_token) { }; } +export function verifyCredentials(token) { + return (dispatch, getState) => { + dispatch({ type: VERIFY_CREDENTIALS_REQUEST }); + + const request = { + method: 'get', + url: '/api/v1/accounts/verify_credentials', + headers: { + 'Authorization': `Bearer ${token.get('access_token')}`, + }, + }; + + return api(getState).request(request).then(({ data: account }) => { + dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); + return account; + }).catch(error => { + dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); + }); + }; +} + export function logIn(username, password) { return (dispatch, getState) => { return dispatch(createAppAndToken()).then(() => { @@ -161,6 +188,10 @@ export function logOut() { }; } +export function switchAccount(accountId) { + return { type: SWITCH_ACCOUNT, accountId }; +} + export function register(params) { return (dispatch, getState) => { params.fullname = params.username; diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js index a05f2b516..c0571b27d 100644 --- a/app/soapbox/actions/me.js +++ b/app/soapbox/actions/me.js @@ -1,5 +1,7 @@ import api from '../api'; import { importFetchedAccount } from './importer'; +import { List as ImmutableList } from 'immutable'; +import { verifyCredentials } from './auth'; export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST'; export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS'; @@ -10,23 +12,25 @@ export const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST'; export const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS'; export const ME_PATCH_FAIL = 'ME_PATCH_FAIL'; -const hasToken = getState => getState().hasIn(['auth', 'user', 'access_token']); const noOp = () => new Promise(f => f()); export function fetchMe() { return (dispatch, getState) => { + const state = getState(); - if (!hasToken(getState)) { + const me = state.getIn(['auth', 'me']); + const token = state.getIn(['auth', 'users', me]); + + if (!token) { dispatch({ type: ME_FETCH_SKIP }); return noOp(); }; dispatch(fetchMeRequest()); - - return api(getState).get('/api/v1/accounts/verify_credentials').then(response => { - dispatch(fetchMeSuccess(response.data)); + return dispatch(verifyCredentials(token)).then(account => { + dispatch(fetchMeSuccess(account)); }).catch(error => { dispatch(fetchMeFail(error)); - }); + });; }; } diff --git a/app/soapbox/api.js b/app/soapbox/api.js index f65971e64..dc9be3add 100644 --- a/app/soapbox/api.js +++ b/app/soapbox/api.js @@ -9,8 +9,15 @@ export const getLinks = response => { return LinkHeader.parse(value); }; -const getToken = (getState, authType) => - getState().getIn(['auth', authType, 'access_token']); +const getToken = (getState, authType) => { + const state = getState(); + if (authType === 'app') { + return state.getIn(['auth', 'app', 'access_token']); + } else { + const me = state.get('me'); + return state.getIn(['auth', 'users', me, 'access_token']); + } +}; export default (getState, authType = 'user') => { const accessToken = getToken(getState, authType); diff --git a/app/soapbox/features/ui/components/profile_dropdown.js b/app/soapbox/features/ui/components/profile_dropdown.js new file mode 100644 index 000000000..57114f275 --- /dev/null +++ b/app/soapbox/features/ui/components/profile_dropdown.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { connect } from 'react-redux'; +// import { openModal } from '../../../actions/modal'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import { isStaff } from 'soapbox/utils/accounts'; +import { defineMessages, injectIntl } from 'react-intl'; +import { logOut, switchAccount } from 'soapbox/actions/auth'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +const messages = defineMessages({ + switch: { id: 'profile_dropdown.switch_account', defaultMessage: 'Switch to @{acct}' }, + logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' }, +}); + +const mapStateToProps = state => { + const me = state.get('me'); + + const otherAccounts = + state + .getIn(['auth', 'users']) + .keySeq() + .reduce((list, id) => { + if (id === me) return list; + const account = state.getIn(['accounts', id]) || ImmutableMap({ id: id, acct: id }); + return list.push(account); + }, ImmutableList()); + + return { + account: state.getIn(['accounts', me]), + otherAccounts, + isStaff: isStaff(state.getIn(['accounts', me])), + }; +}; + +class ProfileDropdown extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + size: PropTypes.number, + account: ImmutablePropTypes.map, + otherAccounts: ImmutablePropTypes.list, + isStaff: PropTypes.bool.isRequired, + }; + + static defaultProps = { + isStaff: false, + } + + handleLogOut = e => { + this.props.dispatch(logOut()); + e.preventDefault(); + }; + + handleSwitchAccount = account => { + return e => { + this.props.dispatch(switchAccount(account.get('id'))); + e.preventDefault(); + }; + } + + render() { + const { intl, account, otherAccounts } = this.props; + const size = this.props.size || 16; + + let menu = []; + + otherAccounts.forEach(account => { + menu.push({ text: intl.formatMessage(messages.switch, { acct: account.get('acct') }), action: this.handleSwitchAccount(account) }); + }); + + if (otherAccounts.size > 0) { + menu.push(null); + } + + menu.push({ text: intl.formatMessage(messages.logout, { acct: account.get('acct') }), to: '/auth/sign_out', action: this.handleLogOut }); + + return ( +
+
+ +
+
+ ); + } + +} + +export default injectIntl(connect(mapStateToProps)(ProfileDropdown)); diff --git a/app/soapbox/features/ui/components/tabs_bar.js b/app/soapbox/features/ui/components/tabs_bar.js index fa4a685f4..e11044ba7 100644 --- a/app/soapbox/features/ui/components/tabs_bar.js +++ b/app/soapbox/features/ui/components/tabs_bar.js @@ -8,7 +8,7 @@ import classNames from 'classnames'; import IconWithCounter from 'soapbox/components/icon_with_counter'; import SearchContainer from 'soapbox/features/compose/containers/search_container'; import Avatar from '../../../components/avatar'; -import ActionBar from 'soapbox/features/compose/components/action_bar'; +import ProfileDropdown from './profile_dropdown'; import { openModal } from '../../../actions/modal'; import { openSidebar } from '../../../actions/sidebar'; import Icon from '../../../components/icon'; @@ -126,7 +126,7 @@ class TabsBar extends React.PureComponent {
+ + + + ); + } + +} diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index b6cfa5737..4f27024c1 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -13,6 +13,7 @@ import FocalPointModal from './focal_point_modal'; import HotkeysModal from './hotkeys_modal'; import ComposeModal from './compose_modal'; import UnauthorizedModal from './unauthorized_modal'; +import LoginModal from './login_modal'; import { MuteModal, @@ -37,6 +38,7 @@ const MODAL_COMPONENTS = { 'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }), 'COMPOSE': () => Promise.resolve({ default: ComposeModal }), 'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }), + 'LOGIN': () => Promise.resolve({ default: LoginModal }), }; export default class ModalRoot extends React.PureComponent { diff --git a/app/soapbox/features/ui/components/profile_dropdown.js b/app/soapbox/features/ui/components/profile_dropdown.js index abb257852..c53533d7f 100644 --- a/app/soapbox/features/ui/components/profile_dropdown.js +++ b/app/soapbox/features/ui/components/profile_dropdown.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -// import { openModal } from '../../../actions/modal'; +import { openModal } from '../../../actions/modal'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -11,6 +11,7 @@ import { logOut, switchAccount } from 'soapbox/actions/auth'; import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ + add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, switch: { id: 'profile_dropdown.switch_account', defaultMessage: 'Switch to @{acct}' }, logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' }, }); @@ -62,6 +63,11 @@ class ProfileDropdown extends React.PureComponent { }; } + handleAddAccount = e => { + this.props.dispatch(openModal('LOGIN')); + e.preventDefault(); + } + componentDidMount() { this.props.dispatch(fetchOwnAccounts()); } @@ -84,6 +90,7 @@ class ProfileDropdown extends React.PureComponent { menu.push(null); } + menu.push({ text: intl.formatMessage(messages.add), action: this.handleAddAccount }); menu.push({ text: intl.formatMessage(messages.logout, { acct: account.get('acct') }), to: '/auth/sign_out', action: this.handleLogOut }); return ( diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index e64965da4..a4c82d516 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -193,7 +193,8 @@ .onboarding-modal, .error-modal, -.embed-modal { +.embed-modal, +.login-modal { background: var(--background-color); color: var(--primary-text-color); border-radius: 8px; From 71c62c05b8fe1e384912a9b807e391ec1475a592 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 12:25:45 -0500 Subject: [PATCH 21/46] Move isLoggedIn to utils/auth.js --- app/soapbox/actions/accounts.js | 2 +- app/soapbox/actions/blocks.js | 2 +- app/soapbox/actions/compose.js | 2 +- app/soapbox/actions/conversations.js | 2 +- app/soapbox/actions/domain_blocks.js | 2 +- app/soapbox/actions/emoji_reacts.js | 2 +- app/soapbox/actions/favourites.js | 2 +- app/soapbox/actions/filters.js | 2 +- app/soapbox/actions/group_editor.js | 2 +- app/soapbox/actions/groups.js | 2 +- app/soapbox/actions/interactions.js | 2 +- app/soapbox/actions/lists.js | 2 +- app/soapbox/actions/mutes.js | 2 +- app/soapbox/actions/notifications.js | 2 +- app/soapbox/actions/settings.js | 2 +- app/soapbox/actions/statuses.js | 2 +- app/soapbox/actions/suggestions.js | 2 +- app/soapbox/utils/accounts.js | 2 -- app/soapbox/utils/auth.js | 4 ++++ 19 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 725fb8130..4ac4f2769 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -6,7 +6,7 @@ import { importFetchedAccounts, importErrorWhileFetchingAccountByUsername, } from './importer'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/blocks.js b/app/soapbox/actions/blocks.js index 66d13d4ec..5c351b674 100644 --- a/app/soapbox/actions/blocks.js +++ b/app/soapbox/actions/blocks.js @@ -1,7 +1,7 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index f7af6de51..54519c112 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -13,7 +13,7 @@ import { openModal, closeModal } from './modal'; import { getSettings } from './settings'; import { getFeatures } from 'soapbox/utils/features'; import { uploadMedia } from './media'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; let cancelFetchComposeSuggestionsAccounts; diff --git a/app/soapbox/actions/conversations.js b/app/soapbox/actions/conversations.js index c43fe3efb..e019441f9 100644 --- a/app/soapbox/actions/conversations.js +++ b/app/soapbox/actions/conversations.js @@ -4,7 +4,7 @@ import { importFetchedStatuses, importFetchedStatus, } from './importer'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT'; export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT'; diff --git a/app/soapbox/actions/domain_blocks.js b/app/soapbox/actions/domain_blocks.js index c8da042f0..533885a56 100644 --- a/app/soapbox/actions/domain_blocks.js +++ b/app/soapbox/actions/domain_blocks.js @@ -1,5 +1,5 @@ import api, { getLinks } from '../api'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; diff --git a/app/soapbox/actions/emoji_reacts.js b/app/soapbox/actions/emoji_reacts.js index 206c4fc43..60bb46c2f 100644 --- a/app/soapbox/actions/emoji_reacts.js +++ b/app/soapbox/actions/emoji_reacts.js @@ -1,7 +1,7 @@ import api from '../api'; import { importFetchedAccounts, importFetchedStatus } from './importer'; import { favourite, unfavourite } from './interactions'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST'; export const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS'; diff --git a/app/soapbox/actions/favourites.js b/app/soapbox/actions/favourites.js index d5c362ebf..02ecc81c3 100644 --- a/app/soapbox/actions/favourites.js +++ b/app/soapbox/actions/favourites.js @@ -1,6 +1,6 @@ import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/filters.js b/app/soapbox/actions/filters.js index 9823509e6..ab2767a14 100644 --- a/app/soapbox/actions/filters.js +++ b/app/soapbox/actions/filters.js @@ -1,6 +1,6 @@ import api from '../api'; import snackbar from 'soapbox/actions/snackbar'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/group_editor.js b/app/soapbox/actions/group_editor.js index b165e0236..b74533a14 100644 --- a/app/soapbox/actions/group_editor.js +++ b/app/soapbox/actions/group_editor.js @@ -1,5 +1,5 @@ import api from '../api'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST'; export const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS'; diff --git a/app/soapbox/actions/groups.js b/app/soapbox/actions/groups.js index 14a487370..588cbe6e2 100644 --- a/app/soapbox/actions/groups.js +++ b/app/soapbox/actions/groups.js @@ -1,7 +1,7 @@ import api, { getLinks } from '../api'; import { importFetchedAccounts } from './importer'; import { fetchRelationships } from './accounts'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST'; export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js index bd6f3218d..73da37c96 100644 --- a/app/soapbox/actions/interactions.js +++ b/app/soapbox/actions/interactions.js @@ -1,7 +1,7 @@ import api from '../api'; import { importFetchedAccounts, importFetchedStatus } from './importer'; import snackbar from 'soapbox/actions/snackbar'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; diff --git a/app/soapbox/actions/lists.js b/app/soapbox/actions/lists.js index 13a28ddf0..68171cbe3 100644 --- a/app/soapbox/actions/lists.js +++ b/app/soapbox/actions/lists.js @@ -1,7 +1,7 @@ import api from '../api'; import { importFetchedAccounts } from './importer'; import { showAlertForError } from './alerts'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/mutes.js b/app/soapbox/actions/mutes.js index 1d081584a..7ad66a3c0 100644 --- a/app/soapbox/actions/mutes.js +++ b/app/soapbox/actions/mutes.js @@ -2,7 +2,7 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; import { openModal } from './modal'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.js index 4013934f6..07bc3e1cf 100644 --- a/app/soapbox/actions/notifications.js +++ b/app/soapbox/actions/notifications.js @@ -17,7 +17,7 @@ import { } from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFilters, regexFromFilters } from '../selectors'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 2e72c65ad..77ab9b91d 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -2,7 +2,7 @@ import { debounce } from 'lodash'; import { showAlertForError } from './alerts'; import { patchMe } from 'soapbox/actions/me'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; import uuid from '../uuid'; export const SETTING_CHANGE = 'SETTING_CHANGE'; diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 7712e1bd9..f06aefa39 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -4,7 +4,7 @@ import { evictStatus } from '../storage/modifier'; import { deleteFromTimelines } from './timelines'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; import { openModal } from './modal'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; diff --git a/app/soapbox/actions/suggestions.js b/app/soapbox/actions/suggestions.js index bf6edffb4..788166d8b 100644 --- a/app/soapbox/actions/suggestions.js +++ b/app/soapbox/actions/suggestions.js @@ -1,6 +1,6 @@ import api from '../api'; import { importFetchedAccounts } from './importer'; -import { isLoggedIn } from 'soapbox/utils/accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; diff --git a/app/soapbox/utils/accounts.js b/app/soapbox/utils/accounts.js index 6dd60feae..7e25f868c 100644 --- a/app/soapbox/utils/accounts.js +++ b/app/soapbox/utils/accounts.js @@ -49,5 +49,3 @@ export const isLocal = account => { export const isVerified = account => ( account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified') ); - -export const isLoggedIn = getState => typeof getState().get('me') === 'string'; diff --git a/app/soapbox/utils/auth.js b/app/soapbox/utils/auth.js index 3fb5e7483..6d3bb3d7f 100644 --- a/app/soapbox/utils/auth.js +++ b/app/soapbox/utils/auth.js @@ -1,3 +1,7 @@ +export const isLoggedIn = getState => { + return typeof getState().get('me') === 'string'; +}; + export const getAccessToken = state => { const me = state.getIn(['auth', 'me']); return state.getIn(['auth', 'users', me, 'access_token']); From 4ff1f0fa6338c831ac194d599bcb761dfa76741d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 12:28:53 -0500 Subject: [PATCH 22/46] Add another isLoggedIn condition --- app/soapbox/actions/pin_statuses.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/pin_statuses.js b/app/soapbox/actions/pin_statuses.js index 0a4a320c1..17f1bfd14 100644 --- a/app/soapbox/actions/pin_statuses.js +++ b/app/soapbox/actions/pin_statuses.js @@ -1,5 +1,6 @@ import api from '../api'; import { importFetchedStatuses } from './importer'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; @@ -7,8 +8,8 @@ export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; export function fetchPinnedStatuses() { return (dispatch, getState) => { + if (!isLoggedIn(getState)) return; const me = getState().get('me'); - if (!me) return; dispatch(fetchPinnedStatusesRequest()); From c14fc83ac15d482f091be91084fa8f6ef17540fb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 13:23:11 -0500 Subject: [PATCH 23/46] Improve style of profile dropdown --- .../features/ui/components/profile_dropdown.js | 18 ++++++++++++++++-- app/styles/components/dropdown-menu.scss | 8 ++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/ui/components/profile_dropdown.js b/app/soapbox/features/ui/components/profile_dropdown.js index c53533d7f..0836682ba 100644 --- a/app/soapbox/features/ui/components/profile_dropdown.js +++ b/app/soapbox/features/ui/components/profile_dropdown.js @@ -9,10 +9,11 @@ import { isStaff } from 'soapbox/utils/accounts'; import { defineMessages, injectIntl } from 'react-intl'; import { logOut, switchAccount } from 'soapbox/actions/auth'; import { List as ImmutableList } from 'immutable'; +import Avatar from 'soapbox/components/avatar'; +import DisplayName from 'soapbox/components/display_name'; const messages = defineMessages({ add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, - switch: { id: 'profile_dropdown.switch_account', defaultMessage: 'Switch to @{acct}' }, logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' }, }); @@ -76,6 +77,19 @@ class ProfileDropdown extends React.PureComponent { this.props.dispatch(fetchOwnAccounts()); } + renderAccount = account => { + return ( +
+
+
+
+ +
+
+
+ ); + } + render() { const { intl, account, otherAccounts } = this.props; const size = this.props.size || 16; @@ -83,7 +97,7 @@ class ProfileDropdown extends React.PureComponent { let menu = []; otherAccounts.forEach(account => { - menu.push({ text: intl.formatMessage(messages.switch, { acct: account.get('acct') }), action: this.handleSwitchAccount(account) }); + menu.push({ text: this.renderAccount(account), action: this.handleSwitchAccount(account) }); }); if (otherAccounts.size > 0) { diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index 320a7cb94..4a101b5e4 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -70,6 +70,10 @@ outline: 0; color: #fff; background: var(--brand-color) !important; + + * { + color: #fff; + } } } @@ -79,6 +83,10 @@ height: 1px; background: var(--foreground-color); } + + &__item .account { + line-height: normal; + } } // end .dropdown-menu From 663d375dc5b5e165c03e1a9afbe2e3f6f4a3ed70 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 13:47:01 -0500 Subject: [PATCH 24/46] Throttle fetchOwnAccounts correctly --- app/soapbox/actions/auth.js | 5 ++--- .../features/ui/components/profile_dropdown.js | 11 ++++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 6b0418b8b..6b74c6234 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -1,6 +1,5 @@ import api from '../api'; import { importFetchedAccount } from './importer'; -import { throttle } from 'lodash'; import snackbar from 'soapbox/actions/snackbar'; export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; @@ -197,7 +196,7 @@ export function switchAccount(accountId) { } export function fetchOwnAccounts() { - return throttle((dispatch, getState) => { + return (dispatch, getState) => { const state = getState(); state.getIn(['auth', 'users']).forEach(user => { const account = state.getIn(['accounts', user.get('id')]); @@ -205,7 +204,7 @@ export function fetchOwnAccounts() { dispatch(verifyCredentials(user.get('access_token'))); } }); - }, 2000); + }; } export function register(params) { diff --git a/app/soapbox/features/ui/components/profile_dropdown.js b/app/soapbox/features/ui/components/profile_dropdown.js index 0836682ba..34311ca17 100644 --- a/app/soapbox/features/ui/components/profile_dropdown.js +++ b/app/soapbox/features/ui/components/profile_dropdown.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { openModal } from '../../../actions/modal'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; +import { throttle } from 'lodash'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; @@ -69,12 +70,16 @@ class ProfileDropdown extends React.PureComponent { e.preventDefault(); } - componentDidMount() { + fetchOwnAccounts = throttle(() => { this.props.dispatch(fetchOwnAccounts()); + }, 2000); + + componentDidMount() { + this.fetchOwnAccounts(); } componentDidUpdate() { - this.props.dispatch(fetchOwnAccounts()); + this.fetchOwnAccounts(); } renderAccount = account => { @@ -104,7 +109,7 @@ class ProfileDropdown extends React.PureComponent { menu.push(null); } - menu.push({ text: intl.formatMessage(messages.add), action: this.handleAddAccount }); + menu.push({ text: intl.formatMessage(messages.add), to: '/auth/sign_in' }); menu.push({ text: intl.formatMessage(messages.logout, { acct: account.get('acct') }), to: '/auth/sign_out', action: this.handleLogOut }); return ( From 7a5fb6abb54c86a958411719b5a4c20da686f853 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 14:42:09 -0500 Subject: [PATCH 25/46] Add new account from login page --- app/soapbox/actions/auth.js | 2 +- .../features/auth_login/components/login_page.js | 11 ++++++----- app/soapbox/reducers/auth.js | 16 ++++++++-------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 6b74c6234..83cb82aea 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -191,7 +191,7 @@ export function logOut() { }; } -export function switchAccount(accountId) { +export function switchAccount(accountId,) { return { type: SWITCH_ACCOUNT, accountId }; } diff --git a/app/soapbox/features/auth_login/components/login_page.js b/app/soapbox/features/auth_login/components/login_page.js index 30e0cc94b..1ebdc8dda 100644 --- a/app/soapbox/features/auth_login/components/login_page.js +++ b/app/soapbox/features/auth_login/components/login_page.js @@ -1,10 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Redirect } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; import LoginForm from './login_form'; import OtpAuthForm from './otp_auth_form'; -import { logIn, verifyCredentials } from 'soapbox/actions/auth'; +import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; const mapStateToProps = state => ({ me: state.get('me'), @@ -32,10 +31,14 @@ class LoginPage extends ImmutablePureComponent { } handleSubmit = (event) => { - const { dispatch } = this.props; + const { dispatch, me } = this.props; const { username, password } = this.getFormData(event.target); dispatch(logIn(username, password)).then(({ access_token }) => { return dispatch(verifyCredentials(access_token)); + }).then(account => { + if (typeof me === 'string') { + dispatch(switchAccount(account.id)); + } }).catch(error => { if (error.response.data.error === 'mfa_required') { this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token }); @@ -47,9 +50,7 @@ class LoginPage extends ImmutablePureComponent { } render() { - const { me } = this.props; const { isLoading, mfa_auth_needed, mfa_token } = this.state; - if (me) return ; if (mfa_auth_needed) return ; diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 1a9a72c82..d44767f87 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -117,15 +117,15 @@ const reducer = (state, action) => { }; const maybeReload = (oldState, state, action) => { - const conds = [ - action.type === SWITCH_ACCOUNT, - action.type === VERIFY_CREDENTIALS_FAIL && state.get('me') !== oldState.get('me'), - ]; + if (action.type === SWITCH_ACCOUNT) { + if (location.pathname === '/auth/sign_in') { + location.replace('/'); + } else { + location.reload(); + } + } - // Reload if any of these conditions are true - const shouldReload = conds.some(cond => cond); - - if (shouldReload) { + if (action.type === VERIFY_CREDENTIALS_FAIL && state.get('me') !== oldState.get('me')) { location.reload(); } }; From a5f6fa66acb64924e8192c458994d6a85fe538ab Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 14:44:02 -0500 Subject: [PATCH 26/46] Remove LoginModal for now --- .../features/ui/components/login_modal.js | 66 ------------------- .../features/ui/components/modal_root.js | 2 - 2 files changed, 68 deletions(-) delete mode 100644 app/soapbox/features/ui/components/login_modal.js diff --git a/app/soapbox/features/ui/components/login_modal.js b/app/soapbox/features/ui/components/login_modal.js deleted file mode 100644 index 3f3e7a1fa..000000000 --- a/app/soapbox/features/ui/components/login_modal.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; -import { Link } from 'react-router-dom'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import PropTypes from 'prop-types'; -import Button from '../../../components/button'; -import { SimpleForm, FieldsGroup, Checkbox } from 'soapbox/features/forms'; - -const messages = defineMessages({ - username: { id: 'login.fields.username_placeholder', defaultMessage: 'Username' }, - password: { id: 'login.fields.password_placeholder', defaultMessage: 'Password' }, -}); - -export default @connect() -@injectIntl -class LoginModal extends ImmutablePureComponent { - - render() { - const { intl, isLoading, handleSubmit } = this.props; - - return ( -
-
-
-
-
- -
-
- -
-

- - - -

-
-
-
- -
-
-
- ); - } - -} diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index 4f27024c1..b6cfa5737 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -13,7 +13,6 @@ import FocalPointModal from './focal_point_modal'; import HotkeysModal from './hotkeys_modal'; import ComposeModal from './compose_modal'; import UnauthorizedModal from './unauthorized_modal'; -import LoginModal from './login_modal'; import { MuteModal, @@ -38,7 +37,6 @@ const MODAL_COMPONENTS = { 'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }), 'COMPOSE': () => Promise.resolve({ default: ComposeModal }), 'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }), - 'LOGIN': () => Promise.resolve({ default: LoginModal }), }; export default class ModalRoot extends React.PureComponent { From bbd4edf226b0cf1552e56bad196ec7915e9af89d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 14:52:51 -0500 Subject: [PATCH 27/46] Typofix --- app/soapbox/actions/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 83cb82aea..6b74c6234 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -191,7 +191,7 @@ export function logOut() { }; } -export function switchAccount(accountId,) { +export function switchAccount(accountId) { return { type: SWITCH_ACCOUNT, accountId }; } From 6ead42b06d832fd3b922c140c1ab81fcb76ee784 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 15:15:37 -0500 Subject: [PATCH 28/46] Handle logout --- app/soapbox/actions/auth.js | 13 ++++++------- .../features/ui/components/profile_dropdown.js | 6 ------ app/soapbox/reducers/auth.js | 14 +++++++++++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 6b74c6234..be18746a9 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -177,17 +177,16 @@ export function logIn(username, password) { export function logOut() { return (dispatch, getState) => { const state = getState(); + const me = state.getIn(['auth', 'me']); - dispatch({ type: AUTH_LOGGED_OUT }); - - // Attempt to destroy OAuth token on logout - api(getState).post('/oauth/revoke', { + return api(getState).post('/oauth/revoke', { client_id: state.getIn(['auth', 'app', 'client_id']), client_secret: state.getIn(['auth', 'app', 'client_secret']), - token: state.getIn(['auth', 'user', 'access_token']), + token: state.getIn(['auth', 'users', me, 'access_token']), + }).finally(() => { + dispatch({ type: AUTH_LOGGED_OUT, accountId: me }); + dispatch(snackbar.success('Logged out.')); }); - - dispatch(snackbar.success('Logged out.')); }; } diff --git a/app/soapbox/features/ui/components/profile_dropdown.js b/app/soapbox/features/ui/components/profile_dropdown.js index 34311ca17..7d644591a 100644 --- a/app/soapbox/features/ui/components/profile_dropdown.js +++ b/app/soapbox/features/ui/components/profile_dropdown.js @@ -1,6 +1,5 @@ import React from 'react'; import { connect } from 'react-redux'; -import { openModal } from '../../../actions/modal'; import { fetchOwnAccounts } from 'soapbox/actions/auth'; import { throttle } from 'lodash'; import PropTypes from 'prop-types'; @@ -65,11 +64,6 @@ class ProfileDropdown extends React.PureComponent { }; } - handleAddAccount = e => { - this.props.dispatch(openModal('LOGIN')); - e.preventDefault(); - } - fetchOwnAccounts = throttle(() => { this.props.dispatch(fetchOwnAccounts()); }, 2000); diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index d44767f87..8b5b2e0fc 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -47,7 +47,7 @@ const maybeShiftMe = state => { } }; -const importFailedToken = (state, token) => { +const deleteToken = (state, token) => { return state.withMutations(state => { state.update('tokens', ImmutableMap(), tokens => tokens.delete(token)); state.update('users', ImmutableMap(), users => users.filterNot(user => user.get('access_token') === token)); @@ -55,6 +55,14 @@ const importFailedToken = (state, token) => { }); }; +const deleteUser = (state, accountId) => { + return state.withMutations(state => { + state.update('users', ImmutableMap(), users => users.delete(accountId)); + state.update('tokens', ImmutableMap(), tokens => tokens.filterNot(token => token.get('account') === accountId)); + maybeShiftMe(state); + }); +}; + // Upgrade the initial state const migrateLegacy = state => { if (localState) return state; @@ -104,11 +112,11 @@ const reducer = (state, action) => { case AUTH_LOGGED_IN: return importToken(state, action.token); case AUTH_LOGGED_OUT: - return state.set('user', ImmutableMap()); + return deleteUser(state, action.accountId); case VERIFY_CREDENTIALS_SUCCESS: return importCredentials(state, action.token, action.account); case VERIFY_CREDENTIALS_FAIL: - return importFailedToken(state, action.token); + return deleteToken(state, action.token); case SWITCH_ACCOUNT: return state.set('me', action.accountId); default: From 659cee1c491653eadfdf2eaf85a9279fd14d21c2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 15:59:09 -0500 Subject: [PATCH 29/46] Refresh the page under more general conditions --- app/soapbox/reducers/auth.js | 39 ++++++++++++++++++++++++++--------- app/soapbox/reducers/index.js | 2 +- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 8b5b2e0fc..a6d1d58cb 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -124,17 +124,36 @@ const reducer = (state, action) => { } }; -const maybeReload = (oldState, state, action) => { - if (action.type === SWITCH_ACCOUNT) { - if (location.pathname === '/auth/sign_in') { - location.replace('/'); - } else { - location.reload(); - } - } +// The user has a token stored in their browser +const canAuth = state => { + state = maybeShiftMe(state); + const me = state.get('me'); + const token = state.getIn(['users', me, 'access_token']); + return typeof token === 'string'; +}; - if (action.type === VERIFY_CREDENTIALS_FAIL && state.get('me') !== oldState.get('me')) { - location.reload(); +// Reload, but redirect home if the user is already logged in +const reload = state => { + if (location.pathname === '/auth/sign_in' && canAuth(state)) { + return location.replace('/'); + } else { + return location.reload(); + } +}; + +// `me` has changed from one valid ID to another +const userSwitched = (oldState, state) => { + const validMe = state => typeof state.get('me') === 'string'; + + const stillValid = validMe(oldState) && validMe(state); + const didChange = oldState.get('me') !== state.get('me'); + + return stillValid && didChange; +}; + +const maybeReload = (oldState, state, action) => { + if (userSwitched(oldState, state)) { + reload(state); } }; diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index 116766fab..48ddd77b8 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -109,7 +109,7 @@ const appReducer = combineReducers({ // Clear the state (mostly) when the user logs out const logOut = (state = ImmutableMap()) => { - const whitelist = ['instance', 'soapbox', 'custom_emojis']; + const whitelist = ['instance', 'soapbox', 'custom_emojis', 'auth']; return ImmutableMap( whitelist.reduce((acc, curr) => { From 619d2985478b377ff5bced38519ffb7366e9f232 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 16:39:52 -0500 Subject: [PATCH 30/46] Correctly import settings on login --- app/soapbox/actions/auth.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index be18746a9..3c576e6c5 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -1,6 +1,7 @@ import api from '../api'; import { importFetchedAccount } from './importer'; import snackbar from 'soapbox/actions/snackbar'; +import { ME_FETCH_SUCCESS } from 'soapbox/actions/me'; export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; @@ -150,6 +151,7 @@ export function verifyCredentials(token) { return api(getState).request(request).then(({ data: account }) => { dispatch(importFetchedAccount(account)); dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); + if (account.id === getState().get('me')) dispatch({ type: ME_FETCH_SUCCESS, me: account }); return account; }).catch(error => { dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); From 1e2b0c9eee261c50d0e44d062b2a35c3dcb4cdc9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 17:12:31 -0500 Subject: [PATCH 31/46] Fix tests --- app/soapbox/actions/__tests__/auth-test.js | 20 ------ .../__snapshots__/login_page-test.js.snap | 64 ++++++++++++++++++- app/soapbox/reducers/auth.js | 1 + 3 files changed, 64 insertions(+), 21 deletions(-) delete mode 100644 app/soapbox/actions/__tests__/auth-test.js diff --git a/app/soapbox/actions/__tests__/auth-test.js b/app/soapbox/actions/__tests__/auth-test.js deleted file mode 100644 index 0e1e3d2b4..000000000 --- a/app/soapbox/actions/__tests__/auth-test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - AUTH_LOGGED_OUT, - logOut, -} from '../auth'; -import { ALERT_SHOW } from '../alerts'; -import { Map as ImmutableMap } from 'immutable'; -import { mockStore } from 'soapbox/test_helpers'; - -describe('logOut()', () => { - it('creates expected actions', () => { - const expectedActions = [ - { type: AUTH_LOGGED_OUT }, - { type: ALERT_SHOW, message: 'Logged out.', severity: 'success' }, - ]; - const store = mockStore(ImmutableMap()); - - store.dispatch(logOut()); - return expect(store.getActions()).toEqual(expectedActions); - }); -}); diff --git a/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_page-test.js.snap b/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_page-test.js.snap index c6cdc64e3..82586223c 100644 --- a/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_page-test.js.snap +++ b/app/soapbox/features/auth_login/components/__tests__/__snapshots__/login_page-test.js.snap @@ -64,4 +64,66 @@ exports[` renders correctly on load 1`] = ` `; -exports[` renders correctly on load 2`] = `null`; +exports[` renders correctly on load 2`] = ` +
+
+
+
+ +
+
+ +
+

+ + Trouble logging in? + +

+
+
+
+ +
+
+`; diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index a6d1d58cb..5509371bc 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -69,6 +69,7 @@ const migrateLegacy = state => { return state.withMutations(state => { const app = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:app'))); const user = fromJS(JSON.parse(localStorage.getItem('soapbox:auth:user'))); + if (!user) return; state.set('me', '_legacy'); // Placeholder account ID state.set('app', app); state.set('tokens', ImmutableMap({ From 5c6fa253c7d23e7615e6174501500ab86e286f97 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 18:38:28 -0500 Subject: [PATCH 32/46] ['auth', 'me'] -> 'me' --- app/soapbox/actions/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 3c576e6c5..80f58a950 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -179,7 +179,7 @@ export function logIn(username, password) { export function logOut() { return (dispatch, getState) => { const state = getState(); - const me = state.getIn(['auth', 'me']); + const me = state.get('me'); return api(getState).post('/oauth/revoke', { client_id: state.getIn(['auth', 'app', 'client_id']), From fa4d36b7a7513fd20b278cc1c3453ef7c02a3ea8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 18:56:07 -0500 Subject: [PATCH 33/46] Move ActionBar links into LinkFooter --- .../features/ui/components/link_footer.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/ui/components/link_footer.js b/app/soapbox/features/ui/components/link_footer.js index 68034c076..dcdbf70a2 100644 --- a/app/soapbox/features/ui/components/link_footer.js +++ b/app/soapbox/features/ui/components/link_footer.js @@ -6,6 +6,7 @@ import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { openModal } from '../../../actions/modal'; import { logOut } from 'soapbox/actions/auth'; +import { isStaff } from 'soapbox/utils/accounts'; // FIXME: Let this be configured const sourceCode = { @@ -35,10 +36,19 @@ const mapDispatchToProps = (dispatch) => ({ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => (
    - {account &&
  • } - {/* {account &&
  • ·
  • } */} -
  • - {/*
  • ·
  • */} + {account && <> +
  • +
  • +
  • +
  • + {isStaff(account) && <> +
  • +
  • + } +
  • +
  • + } +
  • {account &&
  • }
From ad3362e3cdfdca19113e74c96c6288694183404c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 20:32:40 -0500 Subject: [PATCH 34/46] auth: persist the state only if changed --- app/soapbox/reducers/auth.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 5509371bc..22e0d166b 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -162,10 +162,12 @@ export default function auth(oldState = initialState, action) { const state = reducer(oldState, action); // Persist the state in localStorage - localStorage.setItem('soapbox:auth', JSON.stringify(state.toJS())); + if (!state.equals(oldState)) { + localStorage.setItem('soapbox:auth', JSON.stringify(state.toJS())); - // Reload the page under some conditions - maybeReload(oldState, state, action); + // Reload the page under some conditions + maybeReload(oldState, state, action); + } return state; }; From c7c0c41ce6f439cf1fbf149d39958ecdc495be34 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 22:37:10 -0500 Subject: [PATCH 35/46] Use our own INIT_STORE action instead of relying on Redux's internal actions --- app/soapbox/actions/store.js | 7 +++++++ app/soapbox/containers/soapbox.js | 2 ++ app/soapbox/reducers/__tests__/auth-test.js | 5 +++-- app/soapbox/reducers/auth.js | 3 ++- 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/actions/store.js diff --git a/app/soapbox/actions/store.js b/app/soapbox/actions/store.js new file mode 100644 index 000000000..d6921d7f1 --- /dev/null +++ b/app/soapbox/actions/store.js @@ -0,0 +1,7 @@ +export const INIT_STORE = 'INIT_STORE'; + +export function initStore() { + return { + type: INIT_STORE, + }; +} diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index 8a3a52f3e..041fced17 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -14,6 +14,7 @@ import { ScrollContext } from 'react-router-scroll-4'; import UI from '../features/ui'; // import Introduction from '../features/introduction'; import { fetchCustomEmojis } from '../actions/custom_emojis'; +import { initStore } from '../actions/store'; import { preload } from '../actions/preload'; import { IntlProvider } from 'react-intl'; import ErrorBoundary from '../components/error_boundary'; @@ -30,6 +31,7 @@ const validLocale = locale => Object.keys(messages).includes(locale); export const store = configureStore(); +store.dispatch(initStore()); store.dispatch(preload()); store.dispatch(fetchMe()); store.dispatch(fetchInstance()); diff --git a/app/soapbox/reducers/__tests__/auth-test.js b/app/soapbox/reducers/__tests__/auth-test.js index 4adce1eae..6ea1111dc 100644 --- a/app/soapbox/reducers/__tests__/auth-test.js +++ b/app/soapbox/reducers/__tests__/auth-test.js @@ -1,5 +1,6 @@ import reducer from '../auth'; import { Map as ImmutableMap, fromJS } from 'immutable'; +import { INIT_STORE } from 'soapbox/actions/store'; import { AUTH_APP_CREATED, AUTH_LOGGED_IN, @@ -18,7 +19,7 @@ describe('auth reducer', () => { })); }); - describe('@@INIT', () => { + describe('INIT_STORE', () => { it('sets `me` to the next available user if blank', () => { const state = fromJS({ me: null, @@ -28,7 +29,7 @@ describe('auth reducer', () => { }, }); - const action = { type: '@@INIT' }; + const action = { type: INIT_STORE }; const result = reducer(state, action); expect(result.get('me')).toEqual('1234'); }); diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 22e0d166b..18d7d1219 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -1,3 +1,4 @@ +import { INIT_STORE } from '../actions/store'; import { AUTH_APP_CREATED, AUTH_LOGGED_IN, @@ -104,7 +105,7 @@ const initialize = state => { const reducer = (state, action) => { switch(action.type) { - case '@@INIT': + case INIT_STORE: return initialize(state); case AUTH_APP_CREATED: return state.set('app', fromJS(action.app)); From 41c7612b47bee707750baa588a1e11d9cc43203c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 25 Mar 2021 23:03:58 -0500 Subject: [PATCH 36/46] Don't refresh when '_legacy' changes --- app/soapbox/reducers/auth.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 18d7d1219..c3cf164ab 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -143,10 +143,14 @@ const reload = state => { } }; +// `me` is a user ID string +const validMe = state => { + const me = state.get('me'); + return typeof me === 'string' && me !== '_legacy'; +}; + // `me` has changed from one valid ID to another const userSwitched = (oldState, state) => { - const validMe = state => typeof state.get('me') === 'string'; - const stillValid = validMe(oldState) && validMe(state); const didChange = oldState.get('me') !== state.get('me'); From 4d1af4764f1ee929d65ef5767322a7934cdd156b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Mar 2021 15:29:15 -0500 Subject: [PATCH 37/46] Refactor registration action --- app/soapbox/actions/accounts.js | 16 ++++++++++++++++ app/soapbox/actions/auth.js | 17 +++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 4ac4f2769..52a06afdc 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -8,6 +8,10 @@ import { } from './importer'; import { isLoggedIn } from 'soapbox/utils/auth'; +export const ACCOUNT_CREATE_REQUEST = 'ACCOUNT_CREATE_REQUEST'; +export const ACCOUNT_CREATE_SUCCESS = 'ACCOUNT_CREATE_SUCCESS'; +export const ACCOUNT_CREATE_FAIL = 'ACCOUNT_CREATE_FAIL'; + export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; @@ -98,6 +102,18 @@ function getFromDB(dispatch, getState, index, id) { }); } +export function createAccount(params) { + return (dispatch, getState) => { + dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); + return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => { + dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token }); + }).catch(error => { + dispatch({ type: ACCOUNT_CREATE_FAIL, error, params }); + throw error; + }); + }; +} + export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 80f58a950..5e1672a6d 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -1,6 +1,7 @@ import api from '../api'; import { importFetchedAccount } from './importer'; import snackbar from 'soapbox/actions/snackbar'; +import { createAccount } from 'soapbox/actions/accounts'; import { ME_FETCH_SUCCESS } from 'soapbox/actions/me'; export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; @@ -14,10 +15,6 @@ 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_REGISTER_REQUEST = 'AUTH_REGISTER_REQUEST'; -export const AUTH_REGISTER_SUCCESS = 'AUTH_REGISTER_SUCCESS'; -export const AUTH_REGISTER_FAIL = 'AUTH_REGISTER_FAIL'; - export const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST'; export const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS'; export const RESET_PASSWORD_FAIL = 'RESET_PASSWORD_FAIL'; @@ -211,15 +208,11 @@ export function fetchOwnAccounts() { export function register(params) { return (dispatch, getState) => { params.fullname = params.username; - dispatch({ type: AUTH_REGISTER_REQUEST }); + return dispatch(createAppAndToken()).then(() => { - return api(getState, 'app').post('/api/v1/accounts', params); - }).then(response => { - dispatch({ type: AUTH_REGISTER_SUCCESS, token: response.data }); - dispatch(authLoggedIn(response.data)); - }).catch(error => { - dispatch({ type: AUTH_REGISTER_FAIL, error }); - throw error; + return dispatch(createAccount(params)); + }).then(token => { + dispatch(authLoggedIn(token)); }); }; } From eae309e150505257d69f9aba006d5e19d0b95abd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Mar 2021 15:30:14 -0500 Subject: [PATCH 38/46] Clear captcha form when registration fails --- .../features/auth_login/components/captcha.js | 12 +++++++----- .../landing_page/components/registration_form.js | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/auth_login/components/captcha.js b/app/soapbox/features/auth_login/components/captcha.js index 74ad686ac..0e5b95815 100644 --- a/app/soapbox/features/auth_login/components/captcha.js +++ b/app/soapbox/features/auth_login/components/captcha.js @@ -81,15 +81,14 @@ class CaptchaField extends React.Component { render() { const { captcha } = this.state; - const { onChange } = this.props; - const { onClick } = this.props; + const { onChange, onClick, ...props } = this.props; switch(captcha.get('type')) { case 'native': return (

{}

- +
); case 'none': @@ -100,12 +99,13 @@ class CaptchaField extends React.Component { } -export const NativeCaptchaField = ({ captcha, onChange, onClick }) => ( +export const NativeCaptchaField = ({ captcha, onChange, onClick, name, value }) => (
captcha { this.setState({ captchaIdempotencyKey: uuidv4() }); + this.setParams({ captcha_solution: '' }); } render() { const { instance, intl } = this.props; + const { params } = this.state; const isOpen = instance.get('registrations'); const isLoading = this.state.captchaLoading || this.state.submissionLoading; @@ -221,6 +223,8 @@ class RegistrationForm extends ImmutablePureComponent { onChange={this.onInputChange} onClick={this.onCaptchaClick} idempotencyKey={this.state.captchaIdempotencyKey} + name='captcha_solution' + value={params.get('captcha_solution', '')} />
Date: Fri, 26 Mar 2021 15:45:46 -0500 Subject: [PATCH 39/46] Fix registration form workflow --- app/soapbox/actions/accounts.js | 2 +- app/soapbox/actions/auth.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 52a06afdc..c1e2341fd 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -106,7 +106,7 @@ export function createAccount(params) { return (dispatch, getState) => { dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => { - dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token }); + return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token }); }).catch(error => { dispatch({ type: ACCOUNT_CREATE_FAIL, error, params }); throw error; diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 5e1672a6d..98deb2e1d 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -212,7 +212,7 @@ export function register(params) { return dispatch(createAppAndToken()).then(() => { return dispatch(createAccount(params)); }).then(token => { - dispatch(authLoggedIn(token)); + return dispatch(authLoggedIn(token)); }); }; } From 597546e98904e241f3e518f2e8a117f9a869f7da Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Mar 2021 16:20:31 -0500 Subject: [PATCH 40/46] Update captcha snapshot --- .../components/__tests__/__snapshots__/captcha-test.js.snap | 1 - 1 file changed, 1 deletion(-) diff --git a/app/soapbox/features/auth_login/components/__tests__/__snapshots__/captcha-test.js.snap b/app/soapbox/features/auth_login/components/__tests__/__snapshots__/captcha-test.js.snap index 0e6cd2204..333d04f8d 100644 --- a/app/soapbox/features/auth_login/components/__tests__/__snapshots__/captcha-test.js.snap +++ b/app/soapbox/features/auth_login/components/__tests__/__snapshots__/captcha-test.js.snap @@ -15,7 +15,6 @@ exports[` renders correctly 1`] = ` > Date: Fri, 26 Mar 2021 16:42:47 -0500 Subject: [PATCH 41/46] Fix MFA --- app/soapbox/actions/auth.js | 5 +++-- .../features/auth_login/components/otp_auth_form.js | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 98deb2e1d..e99ad9fdb 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -127,8 +127,9 @@ export function otpVerify(code, mfa_token) { code: code, challenge_type: 'totp', redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - }).then(response => { - dispatch(authLoggedIn(response.data)); + }).then(({ data: token }) => { + dispatch(authLoggedIn(token)); + return token; }); }; } diff --git a/app/soapbox/features/auth_login/components/otp_auth_form.js b/app/soapbox/features/auth_login/components/otp_auth_form.js index 091d2a48f..438e24bff 100644 --- a/app/soapbox/features/auth_login/components/otp_auth_form.js +++ b/app/soapbox/features/auth_login/components/otp_auth_form.js @@ -2,8 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { otpVerify } from 'soapbox/actions/auth'; -import { fetchMe } from 'soapbox/actions/me'; +import { otpVerify, verifyCredentials } from 'soapbox/actions/auth'; import { SimpleInput } from 'soapbox/features/forms'; import PropTypes from 'prop-types'; @@ -36,9 +35,9 @@ class OtpAuthForm extends ImmutablePureComponent { handleSubmit = (event) => { const { dispatch, mfa_token } = this.props; const { code } = this.getFormData(event.target); - dispatch(otpVerify(code, mfa_token)).then(() => { + dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => { this.setState({ code_error: false }); - return dispatch(fetchMe()); + return dispatch(verifyCredentials(access_token)); }).catch(error => { this.setState({ isLoading: false }); if (error.response.data.error === 'Invalid code') { From c5778472f58fb66e9abd631b471d673f8f354a07 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Mar 2021 16:45:20 -0500 Subject: [PATCH 42/46] .dropdown-menu { max-width: 300px; } --- app/styles/components/dropdown-menu.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/components/dropdown-menu.scss b/app/styles/components/dropdown-menu.scss index 4a101b5e4..93c0d3bdc 100644 --- a/app/styles/components/dropdown-menu.scss +++ b/app/styles/components/dropdown-menu.scss @@ -9,6 +9,7 @@ padding: 4px 0; color: var(--primary-text-color); box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5); + max-width: 300px; &.left { transform-origin: 100% 50%; } &.top { transform-origin: 50% 100%; } &.bottom { transform-origin: 50% 0; } From 16ce14e40331ec7db0d480a147131f502c0e5971 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Mar 2021 22:34:30 -0500 Subject: [PATCH 43/46] Add mobile account switcher --- app/soapbox/components/sidebar_menu.js | 98 ++++++++++++++++++++----- app/styles/components/sidebar-menu.scss | 15 +++- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index 306303417..b4bda0099 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { throttle } from 'lodash'; import { Link, NavLink } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -14,8 +15,10 @@ import { closeSidebar } from '../actions/sidebar'; import { shortNumberFormat } from '../utils/numbers'; import { isStaff } from '../utils/accounts'; import { makeGetAccount } from '../selectors'; -import { logOut } from 'soapbox/actions/auth'; +import { logOut, switchAccount } from 'soapbox/actions/auth'; import ThemeToggle from '../features/ui/components/theme_toggle_container'; +import { fetchOwnAccounts } from 'soapbox/actions/auth'; +import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ followers: { id: 'account.followers', defaultMessage: 'Followers' }, @@ -38,17 +41,30 @@ const messages = defineMessages({ news: { id: 'tabs_bar.news', defaultMessage: 'News' }, donate: { id: 'donate', defaultMessage: 'Donate' }, info: { id: 'column.info', defaultMessage: 'Server information' }, + add_account: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, }); const mapStateToProps = state => { const me = state.get('me'); const getAccount = makeGetAccount(); + const otherAccounts = + state + .getIn(['auth', 'users']) + .keySeq() + .reduce((list, id) => { + if (id === me) return list; + const account = state.getIn(['accounts', id]); + return account ? list.push(account) : list; + }, ImmutableList()); + return { account: getAccount(state, me), sidebarOpen: state.get('sidebar').sidebarOpen, donateUrl: state.getIn(['patron', 'instance', 'url']), isStaff: isStaff(state.getIn(['accounts', me])), + otherAccounts, + }; }; @@ -60,6 +76,12 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(logOut()); e.preventDefault(); }, + fetchOwnAccounts() { + dispatch(fetchOwnAccounts()); + }, + switchAccount(account) { + dispatch(switchAccount(account.get('id'))); + }, }); export default @connect(mapStateToProps, mapDispatchToProps) @@ -78,8 +100,52 @@ class SidebarMenu extends ImmutablePureComponent { isStaff: false, } + state = { + switcher: false, + } + + handleSwitchAccount = account => { + return e => { + this.props.switchAccount(account); + e.preventDefault(); + }; + } + + handleSwitcherClick = e => { + this.setState({ switcher: !this.state.switcher }); + e.preventDefault(); + } + + fetchOwnAccounts = throttle(() => { + this.props.fetchOwnAccounts(); + }, 2000); + + componentDidMount() { + this.fetchOwnAccounts(); + } + + componentDidUpdate() { + this.fetchOwnAccounts(); + } + + renderAccount = account => { + return ( + +
+
+
+
+ +
+
+
+
+ ); + } + render() { - const { sidebarOpen, onClose, intl, account, onClickLogOut, donateUrl, isStaff } = this.props; + const { sidebarOpen, onClose, intl, account, onClickLogOut, donateUrl, isStaff, otherAccounts } = this.props; + const { switcher } = this.state; if (!account) return null; const acct = account.get('acct'); @@ -105,24 +171,22 @@ class SidebarMenu extends ImmutablePureComponent {
-
+ -
- -
- - {shortNumberFormat(account.get('followers_count'))} - {intl.formatMessage(messages.followers)} - - - {shortNumberFormat(account.get('following_count'))} - {intl.formatMessage(messages.follows)} - -
- + +
-
+ {switcher &&
+ {otherAccounts.map(account => this.renderAccount(account))} + + + + {intl.formatMessage(messages.add_account)} + +
} + +
diff --git a/app/styles/components/sidebar-menu.scss b/app/styles/components/sidebar-menu.scss index 2207a2aed..dc4c897a7 100644 --- a/app/styles/components/sidebar-menu.scss +++ b/app/styles/components/sidebar-menu.scss @@ -98,14 +98,23 @@ } &__name { - display: block; + display: flex; margin-top: 10px; + color: var(--primary-text-color); + text-decoration: none; + align-items: center; .display-name__account { display: block; margin-top: 2px; color: var(--primary-text-color--faint); } + + i.fa-caret-up, + i.fa-caret-down { + margin-left: auto; + padding-left: 10px; + } } &__stats { @@ -140,6 +149,10 @@ } } +.sidebar-account { + text-decoration: none; +} + .sidebar-menu-item { display: flex; padding: 16px 18px; From 9bff74b575bb3bd66677d9d2b29a51ca15bd8bd6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Mar 2021 22:42:10 -0500 Subject: [PATCH 44/46] Remove unused import --- app/soapbox/components/sidebar_menu.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index b4bda0099..6a497a967 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -12,7 +12,6 @@ import IconButton from './icon_button'; import Icon from './icon'; import DisplayName from './display_name'; import { closeSidebar } from '../actions/sidebar'; -import { shortNumberFormat } from '../utils/numbers'; import { isStaff } from '../utils/accounts'; import { makeGetAccount } from '../selectors'; import { logOut, switchAccount } from 'soapbox/actions/auth'; From 582649538ccea51a02ab3c14412e171575766773 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 26 Mar 2021 22:57:22 -0500 Subject: [PATCH 45/46] Close account switcher when sidebar closes --- app/soapbox/components/sidebar_menu.js | 45 ++++++++++++++------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index 6a497a967..61f184523 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -103,6 +103,11 @@ class SidebarMenu extends ImmutablePureComponent { switcher: false, } + handleClose = () => { + this.setState({ switcher: false }); + this.props.onClose(); + } + handleSwitchAccount = account => { return e => { this.props.switchAccount(account); @@ -143,7 +148,7 @@ class SidebarMenu extends ImmutablePureComponent { } render() { - const { sidebarOpen, onClose, intl, account, onClickLogOut, donateUrl, isStaff, otherAccounts } = this.props; + const { sidebarOpen, intl, account, onClickLogOut, donateUrl, isStaff, otherAccounts } = this.props; const { switcher } = this.state; if (!account) return null; const acct = account.get('acct'); @@ -154,19 +159,19 @@ class SidebarMenu extends ImmutablePureComponent { return (
-
+
Account Info - +
- +
@@ -179,7 +184,7 @@ class SidebarMenu extends ImmutablePureComponent { {switcher &&
{otherAccounts.map(account => this.renderAccount(account))} - + {intl.formatMessage(messages.add_account)} @@ -192,71 +197,71 @@ class SidebarMenu extends ImmutablePureComponent {
- + {intl.formatMessage(messages.profile)} {donateUrl ? - + {intl.formatMessage(messages.donate)} : ''} - + {intl.formatMessage(messages.lists)} - + {intl.formatMessage(messages.bookmarks)}
- + {intl.formatMessage(messages.follow_requests)} - + {intl.formatMessage(messages.blocks)} - + {intl.formatMessage(messages.domain_blocks)} - + {intl.formatMessage(messages.mutes)} - + {intl.formatMessage(messages.filters)} - { isStaff && + { isStaff && {intl.formatMessage(messages.admin_settings)} } - { isStaff && + { isStaff && {intl.formatMessage(messages.soapbox_config)} } - + {intl.formatMessage(messages.preferences)} - + {intl.formatMessage(messages.import_data)} - + {intl.formatMessage(messages.security)}
- + {intl.formatMessage(messages.info)} From f728491ad00adf462bfe4d161102568a50dafb3c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 27 Mar 2021 10:50:21 -0500 Subject: [PATCH 46/46] Add follow request nav item when account is locked --- .../features/ui/components/features_panel.js | 22 +++++++++++++++++-- .../features/ui/components/link_footer.js | 1 + app/soapbox/features/ui/index.js | 7 +++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/ui/components/features_panel.js b/app/soapbox/features/ui/components/features_panel.js index 5776e0169..f9d971331 100644 --- a/app/soapbox/features/ui/components/features_panel.js +++ b/app/soapbox/features/ui/components/features_panel.js @@ -1,8 +1,11 @@ import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Icon from 'soapbox/components/icon'; +import IconWithCounter from 'soapbox/components/icon_with_counter'; import { NavLink } from 'react-router-dom'; import { injectIntl, defineMessages } from 'react-intl'; +import { OrderedSet as ImmutableOrderedSet } from 'immutable'; const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit Profile' }, @@ -10,18 +13,29 @@ const messages = defineMessages({ security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, lists: { id: 'column.lists', defaultMessage: 'Lists' }, bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, + follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, }); -export default +const mapStateToProps = state => { + const me = state.get('me'); + return { + isLocked: state.getIn(['accounts', me, 'locked']), + followRequestsCount: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableOrderedSet()).count(), + }; +}; + +export default @connect(mapStateToProps) @injectIntl class FeaturesPanel extends React.PureComponent { static propTypes = { intl: PropTypes.object.isRequired, + isLocked: PropTypes.bool, + followRequestsCount: PropTypes.number, }; render() { - const { intl } = this.props; + const { intl, isLocked, followRequestsCount } = this.props; return (
@@ -32,6 +46,10 @@ class FeaturesPanel extends React.PureComponent { {intl.formatMessage(messages.edit_profile)} + {(isLocked || followRequestsCount > 0) && + + {intl.formatMessage(messages.follow_requests)} + } diff --git a/app/soapbox/features/ui/components/link_footer.js b/app/soapbox/features/ui/components/link_footer.js index dcdbf70a2..6e12f46d2 100644 --- a/app/soapbox/features/ui/components/link_footer.js +++ b/app/soapbox/features/ui/components/link_footer.js @@ -41,6 +41,7 @@ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => (
  • +
  • {isStaff(account) && <>
  • diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 8b5c1318c..31131a757 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -21,6 +21,7 @@ import { fetchFilters } from '../../actions/filters'; import { fetchChats } from 'soapbox/actions/chats'; import { clearHeight } from '../../actions/height_cache'; import { openModal } from '../../actions/modal'; +import { fetchFollowRequests } from '../../actions/accounts'; import { WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import TabsBar from './components/tabs_bar'; @@ -459,7 +460,7 @@ class UI extends React.PureComponent { this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); this.props.dispatch(fetchChats()); - // this.props.dispatch(fetchGroups('member')); + if (isStaff(account)) { this.props.dispatch(fetchReports({ state: 'open' })); this.props.dispatch(fetchUsers({ page: 1, filters: 'local,need_approval' })); @@ -467,6 +468,10 @@ class UI extends React.PureComponent { } setTimeout(() => this.props.dispatch(fetchFilters()), 500); + + if (account.get('locked')) { + setTimeout(() => this.props.dispatch(fetchFollowRequests()), 700); + } } this.connectStreaming(); }