Merge branch 'multi-accounts' into 'develop'

Multi-account switcher, fixes #23

Closes #23

See merge request soapbox-pub/soapbox-fe!451
bundle-emoji
Alex Gleason 2021-03-27 16:06:13 +00:00
commit fce37be8a4
50 zmienionych plików z 1002 dodań i 344 usunięć

Wyświetl plik

@ -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);
});
});

Wyświetl plik

@ -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));

Wyświetl plik

@ -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,
};
}

Wyświetl plik

@ -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']);

Wyświetl plik

@ -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());

Wyświetl plik

@ -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());

Wyświetl plik

@ -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']);

Wyświetl plik

@ -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));

Wyświetl plik

@ -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);

Wyświetl plik

@ -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,

Wyświetl plik

@ -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());

Wyświetl plik

@ -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));

Wyświetl plik

@ -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));

Wyświetl plik

@ -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));

Wyświetl plik

@ -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));
});
});;
};
}

Wyświetl plik

@ -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']);

Wyświetl plik

@ -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']);

Wyświetl plik

@ -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());

Wyświetl plik

@ -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();

Wyświetl plik

@ -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));

Wyświetl plik

@ -0,0 +1,7 @@
export const INIT_STORE = 'INIT_STORE';
export function initStore() {
return {
type: INIT_STORE,
};
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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);

Wyświetl plik

@ -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>

Wyświetl plik

@ -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}>

Wyświetl plik

@ -15,7 +15,6 @@ exports[`<NativeCaptchaField /> renders correctly 1`] = `
>
<input
autoComplete="off"
name="captcha_solution"
onChange={[Function]}
placeholder="Enter the pictured text"
required={true}

Wyświetl plik

@ -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>
`;

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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} />;

Wyświetl plik

@ -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') {

Wyświetl plik

@ -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

Wyświetl plik

@ -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 });

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 />

Wyświetl plik

@ -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>

Wyświetl plik

@ -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));

Wyświetl plik

@ -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>

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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: {},
});
});
});

Wyświetl plik

@ -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;
};

Wyświetl plik

@ -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) => {

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}
};

Wyświetl plik

@ -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

Wyświetl plik

@ -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;

Wyświetl plik

@ -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']);
};

Wyświetl plik

@ -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

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;