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/actions/accounts.js b/app/soapbox/actions/accounts.js index 55e5926e4..c1e2341fd 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -6,6 +6,11 @@ import { importFetchedAccounts, importErrorWhileFetchingAccountByUsername, } 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'; @@ -97,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 }) => { + return 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])); @@ -162,7 +179,7 @@ export function fetchAccountFail(id, error) { export function followAccount(id, reblogs = true) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const locked = getState().getIn(['accounts', id, 'locked'], false); @@ -179,7 +196,7 @@ export function followAccount(id, reblogs = true) { export function unfollowAccount(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unfollowAccountRequest(id)); @@ -245,7 +262,7 @@ export function unfollowAccountFail(error) { export function blockAccount(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(blockAccountRequest(id)); @@ -260,7 +277,7 @@ export function blockAccount(id) { export function unblockAccount(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unblockAccountRequest(id)); @@ -318,7 +335,7 @@ export function unblockAccountFail(error) { export function muteAccount(id, notifications) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(muteAccountRequest(id)); @@ -333,7 +350,7 @@ export function muteAccount(id, notifications) { export function unmuteAccount(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unmuteAccountRequest(id)); @@ -391,7 +408,7 @@ export function unmuteAccountFail(error) { export function fetchFollowers(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchFollowersRequest(id)); @@ -433,7 +450,7 @@ export function fetchFollowersFail(id, error) { export function expandFollowers(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['user_lists', 'followers', id, 'next']); @@ -481,7 +498,7 @@ export function expandFollowersFail(id, error) { export function fetchFollowing(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchFollowingRequest(id)); @@ -523,7 +540,7 @@ export function fetchFollowingFail(id, error) { export function expandFollowing(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['user_lists', 'following', id, 'next']); @@ -571,7 +588,7 @@ export function expandFollowingFail(id, error) { export function fetchRelationships(accountIds) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const loadedRelationships = getState().get('relationships'); const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null); @@ -616,7 +633,7 @@ export function fetchRelationshipsFail(error) { export function fetchFollowRequests() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchFollowRequestsRequest()); @@ -651,7 +668,7 @@ export function fetchFollowRequestsFail(error) { export function expandFollowRequests() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['user_lists', 'follow_requests', 'next']); @@ -692,7 +709,7 @@ export function expandFollowRequestsFail(error) { export function authorizeFollowRequest(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(authorizeFollowRequestRequest(id)); @@ -728,7 +745,7 @@ export function authorizeFollowRequestFail(id, error) { export function rejectFollowRequest(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(rejectFollowRequestRequest(id)); @@ -763,7 +780,7 @@ export function rejectFollowRequestFail(id, error) { export function pinAccount(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(pinAccountRequest(id)); @@ -777,7 +794,7 @@ export function pinAccount(id) { export function unpinAccount(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unpinAccountRequest(id)); diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index a0bb9b9a2..e99ad9fdb 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -1,14 +1,19 @@ 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'; 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 AUTH_REGISTER_REQUEST = 'AUTH_REGISTER_REQUEST'; -export const AUTH_REGISTER_SUCCESS = 'AUTH_REGISTER_SUCCESS'; -export const AUTH_REGISTER_FAIL = 'AUTH_REGISTER_FAIL'; +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 RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST'; export const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS'; @@ -86,8 +91,9 @@ function createUserToken(username, password) { grant_type: 'password', username: username, password: password, - }).then(response => { - dispatch(authLoggedIn(response.data)); + }).then(({ data: token }) => { + dispatch(authLoggedIn(token)); + return token; }); }; } @@ -121,8 +127,32 @@ 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; + }); + }; +} + +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}`, + }, + }; + + 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 }); }); }; } @@ -147,32 +177,43 @@ export function logIn(username, password) { export function logOut() { return (dispatch, getState) => { const state = getState(); + const me = state.get('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.')); +export function switchAccount(accountId) { + return { type: SWITCH_ACCOUNT, accountId }; +} + +export function fetchOwnAccounts() { + return (dispatch, getState) => { + const state = getState(); + state.getIn(['auth', 'users']).forEach(user => { + const account = state.getIn(['accounts', user.get('id')]); + if (!account) { + dispatch(verifyCredentials(user.get('access_token'))); + } + }); }; } 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 => { + return dispatch(authLoggedIn(token)); }); }; } @@ -285,9 +326,9 @@ export function authAppAuthorized(app) { }; } -export function authLoggedIn(user) { +export function authLoggedIn(token) { return { type: AUTH_LOGGED_IN, - user, + token, }; } diff --git a/app/soapbox/actions/blocks.js b/app/soapbox/actions/blocks.js index d891999bb..5c351b674 100644 --- a/app/soapbox/actions/blocks.js +++ b/app/soapbox/actions/blocks.js @@ -1,6 +1,7 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; @@ -12,7 +13,7 @@ export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; export function fetchBlocks() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchBlocksRequest()); @@ -48,7 +49,7 @@ export function fetchBlocksFail(error) { export function expandBlocks() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['user_lists', 'blocks', 'next']); diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 782dee74a..54519c112 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -13,6 +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/auth'; let cancelFetchComposeSuggestionsAccounts; @@ -157,7 +158,7 @@ export function handleComposeSubmit(dispatch, getState, response, status) { export function submitCompose(routerHistory, group) { return function(dispatch, getState) { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); @@ -216,7 +217,7 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function(dispatch, getState) { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const uploadLimit = getFeatures(getState().get('instance')).attachmentLimit; const media = getState().getIn(['compose', 'media_attachments']); @@ -254,7 +255,7 @@ export function uploadCompose(files) { export function changeUploadCompose(id, params) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(changeUploadComposeRequest()); diff --git a/app/soapbox/actions/conversations.js b/app/soapbox/actions/conversations.js index fc1305156..e019441f9 100644 --- a/app/soapbox/actions/conversations.js +++ b/app/soapbox/actions/conversations.js @@ -4,6 +4,7 @@ import { importFetchedStatuses, importFetchedStatus, } from './importer'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT'; export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT'; @@ -24,7 +25,7 @@ export const unmountConversations = () => ({ }); export const markConversationRead = conversationId => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch({ type: CONVERSATIONS_READ, @@ -35,7 +36,7 @@ export const markConversationRead = conversationId => (dispatch, getState) => { }; export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(expandConversationsRequest()); diff --git a/app/soapbox/actions/domain_blocks.js b/app/soapbox/actions/domain_blocks.js index a5ebb0c99..533885a56 100644 --- a/app/soapbox/actions/domain_blocks.js +++ b/app/soapbox/actions/domain_blocks.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; @@ -18,7 +19,7 @@ export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL'; export function blockDomain(domain) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(blockDomainRequest(domain)); @@ -57,7 +58,7 @@ export function blockDomainFail(domain, error) { export function unblockDomain(domain) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unblockDomainRequest(domain)); @@ -102,7 +103,7 @@ export function unblockDomainFail(domain, error) { export function fetchDomainBlocks() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchDomainBlocksRequest()); @@ -138,7 +139,7 @@ export function fetchDomainBlocksFail(error) { export function expandDomainBlocks() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['domain_lists', 'blocks', 'next']); diff --git a/app/soapbox/actions/emoji_reacts.js b/app/soapbox/actions/emoji_reacts.js index 7b041d4ee..60bb46c2f 100644 --- a/app/soapbox/actions/emoji_reacts.js +++ b/app/soapbox/actions/emoji_reacts.js @@ -1,6 +1,7 @@ import api from '../api'; import { importFetchedAccounts, importFetchedStatus } from './importer'; import { favourite, unfavourite } from './interactions'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST'; export const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS'; @@ -44,7 +45,7 @@ export const simpleEmojiReact = (status, emoji) => { export function fetchEmojiReacts(id, emoji) { return (dispatch, getState) => { - if (!getState().get('me')) return dispatch(noOp()); + if (!isLoggedIn(getState)) return dispatch(noOp()); dispatch(fetchEmojiReactsRequest(id, emoji)); @@ -65,7 +66,7 @@ export function fetchEmojiReacts(id, emoji) { export function emojiReact(status, emoji) { return function(dispatch, getState) { - if (!getState().get('me')) return dispatch(noOp()); + if (!isLoggedIn(getState)) return dispatch(noOp()); dispatch(emojiReactRequest(status, emoji)); @@ -82,7 +83,7 @@ export function emojiReact(status, emoji) { export function unEmojiReact(status, emoji) { return (dispatch, getState) => { - if (!getState().get('me')) return dispatch(noOp()); + if (!isLoggedIn(getState)) return dispatch(noOp()); dispatch(unEmojiReactRequest(status, emoji)); diff --git a/app/soapbox/actions/favourites.js b/app/soapbox/actions/favourites.js index d5b774f12..02ecc81c3 100644 --- a/app/soapbox/actions/favourites.js +++ b/app/soapbox/actions/favourites.js @@ -1,5 +1,6 @@ import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; +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'; @@ -11,7 +12,7 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FA export function fetchFavouritedStatuses() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) { return; @@ -55,7 +56,7 @@ export function fetchFavouritedStatusesFail(error) { export function expandFavouritedStatuses() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['status_lists', 'favourites', 'next'], null); diff --git a/app/soapbox/actions/filters.js b/app/soapbox/actions/filters.js index e3ad557f5..ab2767a14 100644 --- a/app/soapbox/actions/filters.js +++ b/app/soapbox/actions/filters.js @@ -1,5 +1,6 @@ import api from '../api'; import snackbar from 'soapbox/actions/snackbar'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS'; @@ -14,7 +15,7 @@ export const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS'; export const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL'; export const fetchFilters = () => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch({ type: FILTERS_FETCH_REQUEST, diff --git a/app/soapbox/actions/group_editor.js b/app/soapbox/actions/group_editor.js index 4b9245081..b74533a14 100644 --- a/app/soapbox/actions/group_editor.js +++ b/app/soapbox/actions/group_editor.js @@ -1,4 +1,5 @@ import api from '../api'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST'; export const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS'; @@ -27,7 +28,7 @@ export const submit = (routerHistory) => (dispatch, getState) => { export const create = (title, description, coverImage, routerHistory) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(createRequest()); @@ -62,7 +63,7 @@ export const createFail = error => ({ }); export const update = (groupId, title, description, coverImage, routerHistory) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(updateRequest()); diff --git a/app/soapbox/actions/groups.js b/app/soapbox/actions/groups.js index 959b1b0b2..588cbe6e2 100644 --- a/app/soapbox/actions/groups.js +++ b/app/soapbox/actions/groups.js @@ -1,6 +1,7 @@ import api, { getLinks } from '../api'; import { importFetchedAccounts } from './importer'; import { fetchRelationships } from './accounts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST'; export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS'; @@ -51,7 +52,7 @@ export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS'; export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL'; export const fetchGroup = id => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchGroupRelationships([id])); @@ -84,7 +85,7 @@ export const fetchGroupFail = (id, error) => ({ export function fetchGroupRelationships(groupIds) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const loadedRelationships = getState().get('group_relationships'); const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null); @@ -128,7 +129,7 @@ export function fetchGroupRelationshipsFail(error) { }; export const fetchGroups = (tab) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchGroupsRequest()); @@ -157,7 +158,7 @@ export const fetchGroupsFail = error => ({ export function joinGroup(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(joinGroupRequest(id)); @@ -171,7 +172,7 @@ export function joinGroup(id) { export function leaveGroup(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(leaveGroupRequest(id)); @@ -227,7 +228,7 @@ export function leaveGroupFail(error) { export function fetchMembers(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchMembersRequest(id)); @@ -269,7 +270,7 @@ export function fetchMembersFail(id, error) { export function expandMembers(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['user_lists', 'groups', id, 'next']); @@ -317,7 +318,7 @@ export function expandMembersFail(id, error) { export function fetchRemovedAccounts(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchRemovedAccountsRequest(id)); @@ -359,7 +360,7 @@ export function fetchRemovedAccountsFail(id, error) { export function expandRemovedAccounts(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['user_lists', 'groups_removed_accounts', id, 'next']); @@ -407,7 +408,7 @@ export function expandRemovedAccountsFail(id, error) { export function removeRemovedAccount(groupId, id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(removeRemovedAccountRequest(groupId, id)); @@ -446,7 +447,7 @@ export function removeRemovedAccountFail(groupId, id, error) { export function createRemovedAccount(groupId, id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(createRemovedAccountRequest(groupId, id)); @@ -485,7 +486,7 @@ export function createRemovedAccountFail(groupId, id, error) { export function groupRemoveStatus(groupId, id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(groupRemoveStatusRequest(groupId, id)); diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js index b07feac1b..73da37c96 100644 --- a/app/soapbox/actions/interactions.js +++ b/app/soapbox/actions/interactions.js @@ -1,6 +1,7 @@ import api from '../api'; import { importFetchedAccounts, importFetchedStatus } from './importer'; import snackbar from 'soapbox/actions/snackbar'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; @@ -44,7 +45,7 @@ export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; export function reblog(status) { return function(dispatch, getState) { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(reblogRequest(status)); @@ -61,7 +62,7 @@ export function reblog(status) { export function unreblog(status) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unreblogRequest(status)); @@ -126,7 +127,7 @@ export function unreblogFail(status, error) { export function favourite(status) { return function(dispatch, getState) { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(favouriteRequest(status)); @@ -141,7 +142,7 @@ export function favourite(status) { export function unfavourite(status) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unfavouriteRequest(status)); @@ -280,7 +281,7 @@ export function unbookmarkFail(status, error) { export function fetchReblogs(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchReblogsRequest(id)); @@ -317,7 +318,7 @@ export function fetchReblogsFail(id, error) { export function fetchFavourites(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchFavouritesRequest(id)); @@ -354,7 +355,7 @@ export function fetchFavouritesFail(id, error) { export function pin(status) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(pinRequest(status)); @@ -394,7 +395,7 @@ export function pinFail(status, error) { export function unpin(status) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unpinRequest(status)); diff --git a/app/soapbox/actions/lists.js b/app/soapbox/actions/lists.js index d93ddc43c..68171cbe3 100644 --- a/app/soapbox/actions/lists.js +++ b/app/soapbox/actions/lists.js @@ -1,6 +1,7 @@ import api from '../api'; import { importFetchedAccounts } from './importer'; import { showAlertForError } from './alerts'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; @@ -50,7 +51,7 @@ export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS'; export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL'; export const fetchList = id => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; if (getState().getIn(['lists', id])) { return; @@ -80,7 +81,7 @@ export const fetchListFail = (id, error) => ({ }); export const fetchLists = () => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchListsRequest()); @@ -129,7 +130,7 @@ export const changeListEditorTitle = value => ({ }); export const createList = (title, shouldReset) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(createListRequest()); @@ -157,7 +158,7 @@ export const createListFail = error => ({ }); export const updateList = (id, title, shouldReset) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(updateListRequest(id)); @@ -191,7 +192,7 @@ export const resetListEditor = () => ({ }); export const deleteList = id => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(deleteListRequest(id)); @@ -217,7 +218,7 @@ export const deleteListFail = (id, error) => ({ }); export const fetchListAccounts = listId => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchListAccountsRequest(listId)); @@ -246,7 +247,7 @@ export const fetchListAccountsFail = (id, error) => ({ }); export const fetchListSuggestions = q => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const params = { q, @@ -281,7 +282,7 @@ export const addToListEditor = accountId => (dispatch, getState) => { }; export const addToList = (listId, accountId) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(addToListRequest(listId, accountId)); @@ -314,7 +315,7 @@ export const removeFromListEditor = accountId => (dispatch, getState) => { }; export const removeFromList = (listId, accountId) => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(removeFromListRequest(listId, accountId)); @@ -356,7 +357,7 @@ export const setupListAdder = accountId => (dispatch, getState) => { }; export const fetchAccountLists = accountId => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchAccountListsRequest(accountId)); diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js index a05f2b516..5b58b50b1 100644 --- a/app/soapbox/actions/me.js +++ b/app/soapbox/actions/me.js @@ -1,5 +1,6 @@ import api from '../api'; import { importFetchedAccount } from './importer'; +import { verifyCredentials } from './auth'; export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST'; export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS'; @@ -10,23 +11,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, 'access_token']); + + 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/actions/mutes.js b/app/soapbox/actions/mutes.js index 00d791197..7ad66a3c0 100644 --- a/app/soapbox/actions/mutes.js +++ b/app/soapbox/actions/mutes.js @@ -2,6 +2,7 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; import { openModal } from './modal'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; @@ -16,7 +17,7 @@ export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS' export function fetchMutes() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(fetchMutesRequest()); @@ -52,7 +53,7 @@ export function fetchMutesFail(error) { export function expandMutes() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const url = getState().getIn(['user_lists', 'mutes', 'next']); diff --git a/app/soapbox/actions/notifications.js b/app/soapbox/actions/notifications.js index 63d4adab3..07bc3e1cf 100644 --- a/app/soapbox/actions/notifications.js +++ b/app/soapbox/actions/notifications.js @@ -17,6 +17,7 @@ import { } from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFilters, regexFromFilters } from '../selectors'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -156,7 +157,7 @@ const noOp = () => {}; export function expandNotifications({ maxId } = {}, done = noOp) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; const activeFilter = getSettings(getState()).getIn(['notifications', 'quickFilter', 'active']); const notifications = getState().get('notifications'); @@ -222,7 +223,7 @@ export function expandNotificationsFail(error, isLoadingMore) { export function clearNotifications() { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch({ type: NOTIFICATIONS_CLEAR, @@ -256,9 +257,9 @@ export function setFilter(filterType) { export function markReadNotifications() { return (dispatch, getState) => { - const state = getState(); - if (!state.get('me')) return; + if (!isLoggedIn(getState)) return; + const state = getState(); const topNotification = state.getIn(['notifications', 'items'], ImmutableOrderedMap()).first(ImmutableMap()).get('id'); const lastRead = state.getIn(['notifications', 'lastRead']); 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()); diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index b96504647..77ab9b91d 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -2,6 +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/auth'; import uuid from '../uuid'; export const SETTING_CHANGE = 'SETTING_CHANGE'; @@ -147,8 +148,9 @@ export function changeSetting(path, value) { }; const debouncedSave = debounce((dispatch, getState) => { + if (!isLoggedIn(getState)) return; + const state = getState(); - if (!state.get('me')) return; if (getSettings(state).getIn(['saved'])) return; const data = state.get('settings').delete('saved').toJS(); diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index bdf31da8f..f06aefa39 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -4,6 +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/auth'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -141,7 +142,7 @@ export function redraft(status, raw_text) { export function deleteStatus(id, routerHistory, withRedraft = false) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; let status = getState().getIn(['statuses', id]); @@ -233,7 +234,7 @@ export function fetchContextFail(id, error) { export function muteStatus(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(muteStatusRequest(id)); @@ -269,7 +270,7 @@ export function muteStatusFail(id, error) { export function unmuteStatus(id) { return (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch(unmuteStatusRequest(id)); 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/actions/suggestions.js b/app/soapbox/actions/suggestions.js index d84bb758f..788166d8b 100644 --- a/app/soapbox/actions/suggestions.js +++ b/app/soapbox/actions/suggestions.js @@ -1,5 +1,6 @@ import api from '../api'; import { importFetchedAccounts } from './importer'; +import { isLoggedIn } from 'soapbox/utils/auth'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; @@ -43,7 +44,7 @@ export function fetchSuggestionsFail(error) { }; export const dismissSuggestion = accountId => (dispatch, getState) => { - if (!getState().get('me')) return; + if (!isLoggedIn(getState)) return; dispatch({ type: SUGGESTIONS_DISMISS, 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/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index 306303417..61f184523 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'; @@ -11,11 +12,12 @@ 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 } 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 +40,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 +75,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 +99,57 @@ class SidebarMenu extends ImmutablePureComponent { isStaff: false, } + state = { + switcher: false, + } + + handleClose = () => { + this.setState({ switcher: false }); + this.props.onClose(); + } + + 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, intl, account, onClickLogOut, donateUrl, isStaff, otherAccounts } = this.props; + const { switcher } = this.state; if (!account) return null; const acct = account.get('acct'); @@ -89,111 +159,109 @@ class SidebarMenu extends ImmutablePureComponent { return (
-
+
Account Info - +
-
+ {switcher &&
+ {otherAccounts.map(account => this.renderAccount(account))} + + + + {intl.formatMessage(messages.add_account)} + +
} + +
- + {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)} diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index 138d12701..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()); @@ -150,6 +152,30 @@ class SoapboxMount extends React.PureComponent { export default class Soapbox extends React.PureComponent { + printConsoleWarning = () => { + /* eslint-disable no-console */ + console.log('%cStop!', [ + 'color: #ff0000', + 'display: block', + 'font-family: system-ui, -apple-system, BlinkMacSystemFont, Roboto, Ubuntu, "Helvetica Neue", sans-serif', + 'font-size: 50px', + 'font-weight: 800', + 'padding: 4px 0', + ].join(';')); + console.log('%cThis is a browser feature intended for developers. If someone told you to copy-paste something here it is a scam and will give them access to your account.', [ + 'color: #111111', + 'display: block', + 'font-family: system-ui, -apple-system, BlinkMacSystemFont, Roboto, Ubuntu, "Helvetica Neue", sans-serif', + 'font-size: 18px', + 'padding: 4px 0 16px', + ].join(';')); + /* eslint-enable no-console */ + } + + componentDidMount() { + this.printConsoleWarning(); + } + render() { return ( 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`] = ` > 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/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 ({ me: state.get('me'), @@ -33,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(() => { - return dispatch(fetchMe()); + 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 }); @@ -48,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/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') { diff --git a/app/soapbox/features/landing_page/components/registration_form.js b/app/soapbox/features/landing_page/components/registration_form.js index b520f6a9e..4f862c587 100644 --- a/app/soapbox/features/landing_page/components/registration_form.js +++ b/app/soapbox/features/landing_page/components/registration_form.js @@ -132,10 +132,12 @@ class RegistrationForm extends ImmutablePureComponent { refreshCaptcha = () => { 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', '')} />
{ const { dispatch } = this.props; const { username, password } = this.getFormData(event.target); - dispatch(logIn(username, password)).then(() => { - return dispatch(fetchMe()); + dispatch(logIn(username, password)).then(({ access_token }) => { + return dispatch(verifyCredentials(access_token)); }).catch(error => { if (error.response.data.error === 'mfa_required') { this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token }); diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index f45416518..30c5e9686 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -63,9 +63,8 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - backup_codes: state.getIn(['auth', 'backup_codes', 'codes']), settings: getSettings(state), - tokens: state.getIn(['auth', 'tokens']), + tokens: state.getIn(['security', 'tokens']), }); export default @connect(mapStateToProps) 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 68034c076..6e12f46d2 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,20 @@ const mapDispatchToProps = (dispatch) => ({ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => (
    - {account &&
  • } - {/* {account &&
  • ·
  • } */} -
  • - {/*
  • ·
  • */} + {account && <> +
  • +
  • +
  • +
  • +
  • + {isStaff(account) && <> +
  • +
  • + } +
  • +
  • + } +
  • {account &&
  • }
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..7d644591a --- /dev/null +++ b/app/soapbox/features/ui/components/profile_dropdown.js @@ -0,0 +1,120 @@ +import React from 'react'; +import { connect } from 'react-redux'; +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'; +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' }, + 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]); + return account ? list.push(account) : list; + }, 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(); + }; + } + + fetchOwnAccounts = throttle(() => { + this.props.dispatch(fetchOwnAccounts()); + }, 2000); + + componentDidMount() { + this.fetchOwnAccounts(); + } + + componentDidUpdate() { + this.fetchOwnAccounts(); + } + + renderAccount = account => { + return ( +
+
+
+
+ +
+
+
+ ); + } + + render() { + const { intl, account, otherAccounts } = this.props; + const size = this.props.size || 16; + + let menu = []; + + otherAccounts.forEach(account => { + menu.push({ text: this.renderAccount(account), action: this.handleSwitchAccount(account) }); + }); + + if (otherAccounts.size > 0) { + menu.push(null); + } + + 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 ( +
+
+ +
+
+ ); + } + +} + +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 {