sforkowany z mirror/soapbox
Merge branch 'multi-accounts' into 'develop'
Multi-account switcher, fixes #23 Closes #23 See merge request soapbox-pub/soapbox-fe!451bundle-emoji
commit
fce37be8a4
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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']);
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export const INIT_STORE = 'INIT_STORE';
|
||||
|
||||
export function initStore() {
|
||||
return {
|
||||
type: INIT_STORE,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 (
|
||||
<a href='#' className='sidebar-account' onClick={this.handleSwitchAccount(account)} key={account.get('id')}>
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name' title={account.get('acct')} href={`/@${account.get('acct')}`} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={classes}>
|
||||
<div className='sidebar-menu__wrapper' role='button' onClick={onClose} />
|
||||
<div className='sidebar-menu__wrapper' role='button' onClick={this.handleClose} />
|
||||
<div className='sidebar-menu'>
|
||||
|
||||
<div className='sidebar-menu-header'>
|
||||
<span className='sidebar-menu-header__title'>Account Info</span>
|
||||
<IconButton title='close' onClick={onClose} icon='close' className='sidebar-menu-header__btn' />
|
||||
<IconButton title='close' onClick={this.handleClose} icon='close' className='sidebar-menu-header__btn' />
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__content'>
|
||||
|
||||
<div className='sidebar-menu-profile'>
|
||||
<div className='sidebar-menu-profile__avatar'>
|
||||
<Link to={`/@${acct}`} title={acct} onClick={onClose}>
|
||||
<Link to={`/@${acct}`} title={acct} onClick={this.handleClose}>
|
||||
<Avatar account={account} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className='sidebar-menu-profile__name'>
|
||||
<a href='#' className='sidebar-menu-profile__name' onClick={this.handleSwitcherClick}>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu-profile__stats'>
|
||||
<NavLink className='sidebar-menu-profile-stat' to={`/@${acct}/followers`} onClick={onClose} title={intl.formatNumber(account.get('followers_count'))}>
|
||||
<strong className='sidebar-menu-profile-stat__value'>{shortNumberFormat(account.get('followers_count'))}</strong>
|
||||
<span className='sidebar-menu-profile-stat__label'>{intl.formatMessage(messages.followers)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-profile-stat' to={`/@${acct}/following`} onClick={onClose} title={intl.formatNumber(account.get('following_count'))}>
|
||||
<strong className='sidebar-menu-profile-stat__value'>{shortNumberFormat(account.get('following_count'))}</strong>
|
||||
<span className='sidebar-menu-profile-stat__label'>{intl.formatMessage(messages.follows)}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<Icon id={switcher ? 'caret-up' : 'caret-down'} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__section sidebar-menu__section--borderless'>
|
||||
{switcher && <div className='sidebar-menu__section'>
|
||||
{otherAccounts.map(account => this.renderAccount(account))}
|
||||
|
||||
<NavLink className='sidebar-menu-item' to='/auth/sign_in' onClick={this.handleClose}>
|
||||
<Icon id='plus' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.add_account)}</span>
|
||||
</NavLink>
|
||||
</div>}
|
||||
|
||||
<div className='sidebar-menu__section'>
|
||||
<div className='sidebar-menu-item theme-toggle'>
|
||||
<ThemeToggle showLabel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__section sidebar-menu__section'>
|
||||
<NavLink className='sidebar-menu-item' to={`/@${acct}`} onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to={`/@${acct}`} onClick={this.handleClose}>
|
||||
<Icon id='user' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.profile)}</span>
|
||||
</NavLink>
|
||||
{donateUrl ?
|
||||
<a className='sidebar-menu-item' href={donateUrl} onClick={onClose}>
|
||||
<a className='sidebar-menu-item' href={donateUrl} onClick={this.handleClose}>
|
||||
<Icon id='dollar' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.donate)}</span>
|
||||
</a>
|
||||
: ''}
|
||||
<NavLink className='sidebar-menu-item' to='/lists' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/lists' onClick={this.handleClose}>
|
||||
<Icon id='list' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/bookmarks' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/bookmarks' onClick={this.handleClose}>
|
||||
<Icon id='bookmark' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.bookmarks)}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__section'>
|
||||
<NavLink className='sidebar-menu-item' to='/follow_requests' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/follow_requests' onClick={this.handleClose}>
|
||||
<Icon id='user-plus' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.follow_requests)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/blocks' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/blocks' onClick={this.handleClose}>
|
||||
<Icon id='ban' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.blocks)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/domain_blocks' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/domain_blocks' onClick={this.handleClose}>
|
||||
<Icon id='ban' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.domain_blocks)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/mutes' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/mutes' onClick={this.handleClose}>
|
||||
<Icon id='times-circle' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/filters' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/filters' onClick={this.handleClose}>
|
||||
<Icon id='filter' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.filters)}</span>
|
||||
</NavLink>
|
||||
{ isStaff && <a className='sidebar-menu-item' href='/pleroma/admin' target='_blank' onClick={onClose}>
|
||||
{ isStaff && <a className='sidebar-menu-item' href='/pleroma/admin' target='_blank' onClick={this.handleClose}>
|
||||
<Icon id='shield' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.admin_settings)}</span>
|
||||
</a> }
|
||||
{ isStaff && <NavLink className='sidebar-menu-item' to='/soapbox/config' onClick={onClose}>
|
||||
{ isStaff && <NavLink className='sidebar-menu-item' to='/soapbox/config' onClick={this.handleClose}>
|
||||
<Icon id='cog' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.soapbox_config)}</span>
|
||||
</NavLink> }
|
||||
<NavLink className='sidebar-menu-item' to='/settings/preferences' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/settings/preferences' onClick={this.handleClose}>
|
||||
<Icon id='cog' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.preferences)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/settings/import' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/settings/import' onClick={this.handleClose}>
|
||||
<Icon id='cloud-upload' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/auth/edit' onClick={onClose}>
|
||||
<NavLink className='sidebar-menu-item' to='/auth/edit' onClick={this.handleClose}>
|
||||
<Icon id='lock' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.security)}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__section'>
|
||||
<Link className='sidebar-menu-item' to='/info' onClick={onClose}>
|
||||
<Link className='sidebar-menu-item' to='/info' onClick={this.handleClose}>
|
||||
<Icon id='info' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.info)}</span>
|
||||
</Link>
|
||||
|
|
|
@ -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 (
|
||||
<Provider store={store}>
|
||||
|
|
|
@ -15,7 +15,6 @@ exports[`<NativeCaptchaField /> renders correctly 1`] = `
|
|||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
name="captcha_solution"
|
||||
onChange={[Function]}
|
||||
placeholder="Enter the pictured text"
|
||||
required={true}
|
||||
|
|
|
@ -64,4 +64,66 @@ exports[`<LoginPage /> renders correctly on load 1`] = `
|
|||
</form>
|
||||
`;
|
||||
|
||||
exports[`<LoginPage /> renders correctly on load 2`] = `null`;
|
||||
exports[`<LoginPage /> renders correctly on load 2`] = `
|
||||
<form
|
||||
className="simple_form new_user"
|
||||
method="post"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<fieldset
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="fields-group"
|
||||
>
|
||||
<div
|
||||
className="input email user_email"
|
||||
>
|
||||
<input
|
||||
aria-label="Username"
|
||||
autoComplete="off"
|
||||
className="string email"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
required={true}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="input password user_password"
|
||||
>
|
||||
<input
|
||||
aria-label="Password"
|
||||
autoComplete="off"
|
||||
className="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required={true}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className="hint subtle-hint"
|
||||
>
|
||||
<a
|
||||
href="/auth/reset_password"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Trouble logging in?
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div
|
||||
className="actions"
|
||||
>
|
||||
<button
|
||||
className="btn button button-primary"
|
||||
name="button"
|
||||
type="submit"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
<p>{<FormattedMessage id='registration.captcha.hint' defaultMessage='Click the image to get a new captcha' />}</p>
|
||||
<NativeCaptchaField captcha={captcha} onChange={onChange} onClick={onClick} />
|
||||
<NativeCaptchaField captcha={captcha} onChange={onChange} onClick={onClick} {...props} />
|
||||
</div>
|
||||
);
|
||||
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 }) => (
|
||||
<div className='captcha' >
|
||||
<img alt='captcha' src={captcha.get('url')} onClick={onClick} />
|
||||
<TextInput
|
||||
placeholder='Enter the pictured text'
|
||||
name='captcha_solution'
|
||||
name={name}
|
||||
value={value}
|
||||
autoComplete='off'
|
||||
onChange={onChange}
|
||||
required
|
||||
|
@ -117,4 +117,6 @@ NativeCaptchaField.propTypes = {
|
|||
captcha: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -1,11 +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 } from 'soapbox/actions/auth';
|
||||
import { fetchMe } from 'soapbox/actions/me';
|
||||
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
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 <Redirect to='/' />;
|
||||
|
||||
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;
|
||||
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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', '')}
|
||||
/>
|
||||
<div className='fields-group'>
|
||||
<Checkbox
|
||||
|
|
|
@ -8,8 +8,7 @@ import SiteLogo from './site_logo';
|
|||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import { logIn } from 'soapbox/actions/auth';
|
||||
import { fetchMe } from 'soapbox/actions/me';
|
||||
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
|
||||
import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
|
@ -55,8 +54,8 @@ class Header extends ImmutablePureComponent {
|
|||
handleSubmit = (event) => {
|
||||
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 });
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (
|
||||
<div className='wtf-panel promo-panel'>
|
||||
|
@ -32,6 +46,10 @@ class FeaturesPanel extends React.PureComponent {
|
|||
{intl.formatMessage(messages.edit_profile)}
|
||||
</NavLink>
|
||||
|
||||
{(isLocked || followRequestsCount > 0) && <NavLink className='promo-panel-item' to='/follow_requests'>
|
||||
<IconWithCounter icon='user-plus' count={followRequestsCount} fixedWidth />
|
||||
{intl.formatMessage(messages.follow_requests)}
|
||||
</NavLink>}
|
||||
|
||||
<NavLink className='promo-panel-item' to='/bookmarks'>
|
||||
<Icon id='bookmark' className='promo-panel-item__icon' fixedWidth />
|
||||
|
|
|
@ -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 }) => (
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{account && <li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li>}
|
||||
{/* {account && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>} */}
|
||||
<li><a href='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a></li>
|
||||
{/* <li><a href='/settings/applications'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> */}
|
||||
{account && <>
|
||||
<li><Link to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocked users' /></Link></li>
|
||||
<li><Link to='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Muted users' /></Link></li>
|
||||
<li><Link to='/filters'><FormattedMessage id='navigation_bar.filters' defaultMessage='Muted words' /></Link></li>
|
||||
<li><Link to='/domain_blocks'><FormattedMessage id='navigation_bar.domain_blocks' defaultMessage='Hidden domains' /></Link></li>
|
||||
<li><Link to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></Link></li>
|
||||
{isStaff(account) && <>
|
||||
<li><a href='/pleroma/admin'><FormattedMessage id='navigation_bar.admin_settings' defaultMessage='Admin settings' /></a></li>
|
||||
<li><Link to='/soapbox/config'><FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' /></Link></li>
|
||||
</>}
|
||||
<li><Link to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></Link></li>
|
||||
<li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li>
|
||||
</>}
|
||||
<li><Link to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></Link></li>
|
||||
{account && <li><Link to='/auth/sign_out' onClick={onClickLogOut}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></Link></li>}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name' title={account.get('acct')} href={`/@${account.get('acct')}`} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className='compose__action-bar' style={{ 'marginTop':'-6px' }}>
|
||||
<div className='compose__action-bar-dropdown'>
|
||||
<DropdownMenuContainer items={menu} icon='chevron-down' size={size} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(connect(mapStateToProps)(ProfileDropdown));
|
|
@ -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 {
|
|||
<div className='tabs-bar__profile'>
|
||||
<Avatar account={account} />
|
||||
<button className='tabs-bar__sidebar-btn' onClick={onOpenSidebar} />
|
||||
<ActionBar account={account} size={34} />
|
||||
<ProfileDropdown account={account} size={34} />
|
||||
</div>
|
||||
<button className='tabs-bar__button-compose button' onClick={onOpenCompose} aria-label={intl.formatMessage(messages.post)}>
|
||||
<span>{intl.formatMessage(messages.post)}</span>
|
||||
|
|
|
@ -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';
|
||||
|
@ -40,6 +41,7 @@ import Icon from 'soapbox/components/icon';
|
|||
import { isStaff } from 'soapbox/utils/accounts';
|
||||
import ChatPanes from 'soapbox/features/chats/components/chat_panes';
|
||||
import ProfileHoverCard from 'soapbox/components/profile_hover_card';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
|
||||
import {
|
||||
Status,
|
||||
|
@ -111,7 +113,7 @@ const mapStateToProps = state => {
|
|||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
accessToken: state.getIn(['auth', 'user', 'access_token']),
|
||||
accessToken: getAccessToken(state),
|
||||
streamingUrl: state.getIn(['instance', 'urls', 'streaming_api']),
|
||||
me,
|
||||
account,
|
||||
|
@ -458,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' }));
|
||||
|
@ -466,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();
|
||||
}
|
||||
|
|
|
@ -1,124 +1,198 @@
|
|||
import reducer from '../auth';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import * as actions from 'soapbox/actions/auth';
|
||||
// import app from 'soapbox/__fixtures__/app.json';
|
||||
import user from 'soapbox/__fixtures__/user.json';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import { INIT_STORE } from 'soapbox/actions/store';
|
||||
import {
|
||||
AUTH_APP_CREATED,
|
||||
AUTH_LOGGED_IN,
|
||||
VERIFY_CREDENTIALS_SUCCESS,
|
||||
VERIFY_CREDENTIALS_FAIL,
|
||||
SWITCH_ACCOUNT,
|
||||
} from 'soapbox/actions/auth';
|
||||
|
||||
describe('auth reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
||||
app: ImmutableMap(),
|
||||
user: ImmutableMap(),
|
||||
tokens: ImmutableList(),
|
||||
users: ImmutableMap(),
|
||||
tokens: ImmutableMap(),
|
||||
me: null,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle AUTH_APP_CREATED', () => {
|
||||
const state = ImmutableMap({ });
|
||||
const auth = {
|
||||
auth: {
|
||||
app: {
|
||||
vapid_key: 'BHczIFh4Wn3Q_7wDgehaB8Ti3Uu8BoyOgXxkOVuEJRuEqxtd9TAno8K9ycz4myiQ1ruiyVfG6xT1JLeXtpxDzUs',
|
||||
token_type: 'Bearer',
|
||||
client_secret: 'HU6RGO4284Edr4zucuWmn8OFjcpVtMsoXJU0-8tpwRM',
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
created_at: 1594050270,
|
||||
name: 'SoapboxFE_2020-07-06T15:43:31.989Z',
|
||||
client_id: 'Q0A2r_9ZcEORMenj9kuDRQc3UVL8ypQRoNJ6XQHWJU8',
|
||||
expires_in: 600,
|
||||
scope: 'read write follow push admin',
|
||||
refresh_token: 'aydRA4eragIhavCdAyg6QQnDJmiMbdc-oEBvHYcW_PQ',
|
||||
website: null,
|
||||
id: '113',
|
||||
access_token: 'pbXS8HkoWodrAt_QE1NENcwqigxgWr3P1RIQCKMN0Os',
|
||||
describe('INIT_STORE', () => {
|
||||
it('sets `me` to the next available user if blank', () => {
|
||||
const state = fromJS({
|
||||
me: null,
|
||||
users: {
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
},
|
||||
user: {
|
||||
access_token: 'UVBP2e17b4pTpb_h8fImIm3F5a66IBVb-JkyZHs4gLE',
|
||||
expires_in: 600,
|
||||
me: 'https://social.teci.world/users/curtis',
|
||||
refresh_token: 'c2DpbVxYZBJDogNn-VBNFES72yXPNUYQCv0CrXGOplY',
|
||||
scope: 'read write follow push admin',
|
||||
token_type: 'Bearer',
|
||||
});
|
||||
|
||||
const action = { type: INIT_STORE };
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('me')).toEqual('1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AUTH_APP_CREATED', () => {
|
||||
it('should copy in the app', () => {
|
||||
const token = { token_type: 'Bearer', access_token: 'ABCDEFG' };
|
||||
const action = { type: AUTH_APP_CREATED, app: token };
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
const expected = fromJS(token);
|
||||
|
||||
expect(result.get('app')).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AUTH_LOGGED_IN', () => {
|
||||
it('should import the token', () => {
|
||||
const token = { token_type: 'Bearer', access_token: 'ABCDEFG' };
|
||||
const action = { type: AUTH_LOGGED_IN, token };
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
const expected = fromJS({ 'ABCDEFG': token });
|
||||
|
||||
expect(result.get('tokens')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should merge the token with existing state', () => {
|
||||
const state = fromJS({
|
||||
tokens: { 'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' } },
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' },
|
||||
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
|
||||
});
|
||||
|
||||
const action = {
|
||||
type: AUTH_LOGGED_IN,
|
||||
token: { token_type: 'Bearer', access_token: 'HIJKLMN' },
|
||||
};
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('tokens')).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VERIFY_CREDENTIALS_SUCCESS', () => {
|
||||
it('should import the user', () => {
|
||||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
};
|
||||
|
||||
const expected = fromJS({
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
});
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('users')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should set the account in the token', () => {
|
||||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
};
|
||||
|
||||
const state = fromJS({
|
||||
tokens: { 'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' } },
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG', account: '1234' },
|
||||
});
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('tokens')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('sets `me` to the account if unset', () => {
|
||||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('me')).toEqual('1234');
|
||||
});
|
||||
|
||||
it('leaves `me` alone if already set', () => {
|
||||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
};
|
||||
|
||||
const state = fromJS({ me: '5678' });
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('me')).toEqual('5678');
|
||||
});
|
||||
});
|
||||
|
||||
describe('VERIFY_CREDENTIALS_FAIL', () => {
|
||||
it('should delete the failed token', () => {
|
||||
const state = fromJS({
|
||||
tokens: {
|
||||
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG' },
|
||||
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
|
||||
},
|
||||
tokens: [],
|
||||
},
|
||||
};
|
||||
const action = {
|
||||
type: actions.AUTH_APP_CREATED,
|
||||
app: auth,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
app: auth,
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'HIJKLMN': { token_type: 'Bearer', access_token: 'HIJKLMN' },
|
||||
});
|
||||
|
||||
const action = { type: VERIFY_CREDENTIALS_FAIL, token: 'ABCDEFG' };
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('tokens')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should delete any users associated with the failed token', () => {
|
||||
const state = fromJS({
|
||||
users: {
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
},
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
});
|
||||
|
||||
const action = { type: VERIFY_CREDENTIALS_FAIL, token: 'ABCDEFG' };
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('users')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should reassign `me` to the next in line', () => {
|
||||
const state = fromJS({
|
||||
me: '1234',
|
||||
users: {
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
},
|
||||
});
|
||||
|
||||
const action = { type: VERIFY_CREDENTIALS_FAIL, token: 'ABCDEFG' };
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('me')).toEqual('5678');
|
||||
});
|
||||
});
|
||||
|
||||
// Fails with TypeError: cannot read property merge of undefined
|
||||
// it('should handle the Action AUTH_APP_AUTHORIZED', () => {
|
||||
// const state = ImmutableMap({
|
||||
// auth: {
|
||||
// app: {
|
||||
// vapid_key: 'oldVapidKey',
|
||||
// token_type: 'Bearer',
|
||||
// client_secret: 'oldClientSecret',
|
||||
// redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
// created_at: 1594764335,
|
||||
// name: 'SoapboxFE_2020-07-14T22:05:17.054Z',
|
||||
// client_id: 'bjiy8AxGKXXesfZcyp_iN-uQVE6Cnl03efWoSdOPh9M',
|
||||
// expires_in: 600,
|
||||
// scope: 'read write follow push admin',
|
||||
// refresh_token: 'oldRefreshToken',
|
||||
// website: null,
|
||||
// id: '134',
|
||||
// access_token: 'oldAccessToken',
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// const action = {
|
||||
// type: actions.AUTH_APP_AUTHORIZED,
|
||||
// app: app,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// app: app,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle the Action AUTH_LOGGED_IN', () => {
|
||||
const state = ImmutableMap({
|
||||
user: {
|
||||
access_token: 'UVBP2e17b4pTpb_h8fImIm3F5a66IBVb-JkyZHs4gLE',
|
||||
expires_in: 600,
|
||||
me: 'https://social.teci.world/users/curtis',
|
||||
refresh_token: 'c2DpbVxYZBJDogNn-VBNFES72yXPNUYQCv0CrXGOplY',
|
||||
scope: 'read write follow push admin',
|
||||
token_type: 'Bearer',
|
||||
},
|
||||
});
|
||||
const action = {
|
||||
type: actions.AUTH_LOGGED_IN,
|
||||
user: user,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
user: user,
|
||||
describe('SWITCH_ACCOUNT', () => {
|
||||
it('sets the value of `me`', () => {
|
||||
const action = { type: SWITCH_ACCOUNT, accountId: '5678' };
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('me')).toEqual('5678');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the Action AUTH_LOGGED_OUT', () => {
|
||||
const state = ImmutableMap({
|
||||
user: {
|
||||
access_token: 'UVBP2e17b4pTpb_h8fImIm3F5a66IBVb-JkyZHs4gLE',
|
||||
expires_in: 600,
|
||||
me: 'https://social.teci.world/users/curtis',
|
||||
refresh_token: 'c2DpbVxYZBJDogNn-VBNFES72yXPNUYQCv0CrXGOplY',
|
||||
scope: 'read write follow push admin',
|
||||
token_type: 'Bearer',
|
||||
},
|
||||
});
|
||||
const action = {
|
||||
type: actions.AUTH_LOGGED_OUT,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
user: {},
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,40 +1,178 @@
|
|||
import { INIT_STORE } from '../actions/store';
|
||||
import {
|
||||
AUTH_APP_CREATED,
|
||||
AUTH_LOGGED_IN,
|
||||
AUTH_APP_AUTHORIZED,
|
||||
AUTH_LOGGED_OUT,
|
||||
FETCH_TOKENS_SUCCESS,
|
||||
REVOKE_TOKEN_SUCCESS,
|
||||
SWITCH_ACCOUNT,
|
||||
VERIFY_CREDENTIALS_SUCCESS,
|
||||
VERIFY_CREDENTIALS_FAIL,
|
||||
} from '../actions/auth';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
app: ImmutableMap(JSON.parse(localStorage.getItem('soapbox:auth:app'))),
|
||||
user: ImmutableMap(JSON.parse(localStorage.getItem('soapbox:auth:user'))),
|
||||
tokens: ImmutableList(),
|
||||
const defaultState = ImmutableMap({
|
||||
app: ImmutableMap(),
|
||||
users: ImmutableMap(),
|
||||
tokens: ImmutableMap(),
|
||||
me: null,
|
||||
});
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
const localState = fromJS(JSON.parse(localStorage.getItem('soapbox:auth')));
|
||||
const initialState = defaultState.merge(localState);
|
||||
|
||||
const importToken = (state, token) => {
|
||||
return state.setIn(['tokens', token.access_token], fromJS(token));
|
||||
};
|
||||
|
||||
const importCredentials = (state, token, account) => {
|
||||
return state.withMutations(state => {
|
||||
state.setIn(['users', account.id], ImmutableMap({
|
||||
id: account.id,
|
||||
access_token: token,
|
||||
}));
|
||||
state.setIn(['tokens', token, 'account'], account.id);
|
||||
state.update('me', null, me => me || account.id);
|
||||
upgradeLegacyId(state, account);
|
||||
});
|
||||
};
|
||||
|
||||
// If `me` doesn't match an existing user, attempt to shift it.
|
||||
const maybeShiftMe = state => {
|
||||
const users = state.get('users', ImmutableMap());
|
||||
const me = state.get('me');
|
||||
|
||||
if (!users.get(me)) {
|
||||
return state.set('me', users.first(ImmutableMap()).get('id'));
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
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));
|
||||
maybeShiftMe(state);
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
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({
|
||||
[user.get('access_token')]: user.set('account', '_legacy'),
|
||||
}));
|
||||
state.set('users', ImmutableMap({
|
||||
'_legacy': ImmutableMap({
|
||||
id: '_legacy',
|
||||
access_token: user.get('access_token'),
|
||||
}),
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
// Upgrade the `_legacy` placeholder ID with a real account
|
||||
const upgradeLegacyId = (state, account) => {
|
||||
if (localState) return state;
|
||||
return state.withMutations(state => {
|
||||
state.update('me', null, me => me === '_legacy' ? account.id : me);
|
||||
state.deleteIn(['users', '_legacy']);
|
||||
});
|
||||
// TODO: Delete `soapbox:auth:app` and `soapbox:auth:user` localStorage?
|
||||
// By this point it's probably safe, but we'll leave it just in case.
|
||||
};
|
||||
|
||||
const initialize = state => {
|
||||
return state.withMutations(state => {
|
||||
maybeShiftMe(state);
|
||||
migrateLegacy(state);
|
||||
});
|
||||
};
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch(action.type) {
|
||||
case INIT_STORE:
|
||||
return initialize(state);
|
||||
case AUTH_APP_CREATED:
|
||||
localStorage.setItem('soapbox:auth:app', JSON.stringify(action.app)); // TODO: Better persistence
|
||||
return state.set('app', ImmutableMap(action.app));
|
||||
return state.set('app', fromJS(action.app));
|
||||
case AUTH_APP_AUTHORIZED:
|
||||
const merged = state.get('app').merge(ImmutableMap(action.app));
|
||||
localStorage.setItem('soapbox:auth:app', JSON.stringify(merged)); // TODO: Better persistence
|
||||
return state.set('app', merged);
|
||||
return state.update('app', ImmutableMap(), app => app.merge(fromJS(action.app)));
|
||||
case AUTH_LOGGED_IN:
|
||||
localStorage.setItem('soapbox:auth:user', JSON.stringify(action.user)); // TODO: Better persistence
|
||||
return state.set('user', ImmutableMap(action.user));
|
||||
return importToken(state, action.token);
|
||||
case AUTH_LOGGED_OUT:
|
||||
localStorage.removeItem('soapbox:auth:user');
|
||||
return state.set('user', ImmutableMap());
|
||||
case FETCH_TOKENS_SUCCESS:
|
||||
return state.set('tokens', fromJS(action.tokens));
|
||||
case REVOKE_TOKEN_SUCCESS:
|
||||
const idx = state.get('tokens').findIndex(t => t.get('id') === action.id);
|
||||
return state.deleteIn(['tokens', idx]);
|
||||
return deleteUser(state, action.accountId);
|
||||
case VERIFY_CREDENTIALS_SUCCESS:
|
||||
return importCredentials(state, action.token, action.account);
|
||||
case VERIFY_CREDENTIALS_FAIL:
|
||||
return deleteToken(state, action.token);
|
||||
case SWITCH_ACCOUNT:
|
||||
return state.set('me', action.accountId);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// 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';
|
||||
};
|
||||
|
||||
// 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` 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 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);
|
||||
}
|
||||
};
|
||||
|
||||
export default function auth(oldState = initialState, action) {
|
||||
const state = reducer(oldState, action);
|
||||
|
||||
// Persist the state in localStorage
|
||||
if (!state.equals(oldState)) {
|
||||
localStorage.setItem('soapbox:auth', JSON.stringify(state.toJS()));
|
||||
|
||||
// Reload the page under some conditions
|
||||
maybeReload(oldState, state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
|
|
@ -51,6 +51,7 @@ import chat_message_lists from './chat_message_lists';
|
|||
import profile_hover_card from './profile_hover_card';
|
||||
import backups from './backups';
|
||||
import admin_log from './admin_log';
|
||||
import security from './security';
|
||||
|
||||
const appReducer = combineReducers({
|
||||
dropdown_menu,
|
||||
|
@ -103,11 +104,12 @@ const appReducer = combineReducers({
|
|||
profile_hover_card,
|
||||
backups,
|
||||
admin_log,
|
||||
security,
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
ME_FETCH_SKIP,
|
||||
ME_PATCH_SUCCESS,
|
||||
} from '../actions/me';
|
||||
import { AUTH_LOGGED_OUT } from '../actions/auth';
|
||||
import { AUTH_LOGGED_OUT, VERIFY_CREDENTIALS_SUCCESS } from '../actions/auth';
|
||||
|
||||
const initialState = null;
|
||||
|
||||
|
@ -13,10 +13,11 @@ export default function me(state = initialState, action) {
|
|||
case ME_FETCH_SUCCESS:
|
||||
case ME_PATCH_SUCCESS:
|
||||
return action.me.id;
|
||||
case VERIFY_CREDENTIALS_SUCCESS:
|
||||
return state || action.account.id;
|
||||
case ME_FETCH_FAIL:
|
||||
case ME_FETCH_SKIP:
|
||||
case AUTH_LOGGED_OUT:
|
||||
localStorage.removeItem('soapbox:auth:user');
|
||||
return false;
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -10,11 +10,12 @@ export default function meta(state = initialState, action) {
|
|||
case ME_FETCH_SUCCESS:
|
||||
case ME_PATCH_SUCCESS:
|
||||
const me = fromJS(action.me);
|
||||
if (me.has('pleroma')) {
|
||||
const pleroPrefs = me.get('pleroma').delete('settings_store');
|
||||
return state.mergeIn(['pleroma'], pleroPrefs);
|
||||
}
|
||||
return state;
|
||||
return state.withMutations(state => {
|
||||
if (me.has('pleroma')) {
|
||||
const pleroPrefs = me.get('pleroma').delete('settings_store');
|
||||
state.mergeIn(['pleroma'], pleroPrefs);
|
||||
}
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
FETCH_TOKENS_SUCCESS,
|
||||
REVOKE_TOKEN_SUCCESS,
|
||||
} from '../actions/auth';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
tokens: ImmutableList(),
|
||||
});
|
||||
|
||||
export default function security(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case FETCH_TOKENS_SUCCESS:
|
||||
return state.set('tokens', fromJS(action.tokens));
|
||||
case REVOKE_TOKEN_SUCCESS:
|
||||
const idx = state.get('tokens').findIndex(t => t.get('id') === action.id);
|
||||
return state.deleteIn(['tokens', idx]);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -17,12 +17,16 @@ const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmoji
|
|||
|
||||
const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
|
||||
|
||||
const importSettings = (state, account) => {
|
||||
account = fromJS(account);
|
||||
const prefs = account.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap());
|
||||
return state.merge(prefs);
|
||||
};
|
||||
|
||||
export default function settings(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ME_FETCH_SUCCESS:
|
||||
const me = fromJS(action.me);
|
||||
let fePrefs = me.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap());
|
||||
return state.merge(fePrefs);
|
||||
return importSettings(state, action.me);
|
||||
case NOTIFICATIONS_FILTER_SET:
|
||||
case SETTING_CHANGE:
|
||||
return state
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
import WebSocketClient from 'websocket.js';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
|
||||
const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
|
||||
|
||||
export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
|
||||
return (dispatch, getState) => {
|
||||
const streamingAPIBaseURL = getState().getIn(['instance', 'urls', 'streaming_api']);
|
||||
const accessToken = getState().getIn(['auth', 'user', 'access_token']);
|
||||
const accessToken = getAccessToken(getState());
|
||||
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
|
||||
|
||||
let polling = null;
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
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']);
|
||||
};
|
|
@ -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; }
|
||||
|
@ -62,7 +63,6 @@
|
|||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-transform: capitalize;
|
||||
color: var(--primary-text-color);
|
||||
|
||||
&:focus,
|
||||
|
@ -71,6 +71,10 @@
|
|||
outline: 0;
|
||||
color: #fff;
|
||||
background: var(--brand-color) !important;
|
||||
|
||||
* {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,6 +84,10 @@
|
|||
height: 1px;
|
||||
background: var(--foreground-color);
|
||||
}
|
||||
|
||||
&__item .account {
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
// end .dropdown-menu
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Ładowanie…
Reference in New Issue