kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge remote-tracking branch 'origin/develop' into groups
commit
340632c9d2
|
@ -60,3 +60,16 @@ docs-deploy:
|
|||
# before_script:
|
||||
# - yarn
|
||||
# - yarn build
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- yarn build
|
||||
- mv static public
|
||||
variables:
|
||||
NODE_ENV: production
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- develop
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
import api from '../api';
|
||||
import { importFetchedAccount, importFetchedAccounts } from './importer';
|
||||
import { showAlertForError } from './alerts';
|
||||
import snackbar from './snackbar';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { ME_PATCH_SUCCESS } from './me';
|
||||
|
||||
export const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE';
|
||||
export const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY';
|
||||
export const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR';
|
||||
|
||||
export const ALIASES_ADD_REQUEST = 'ALIASES_ADD_REQUEST';
|
||||
export const ALIASES_ADD_SUCCESS = 'ALIASES_ADD_SUCCESS';
|
||||
export const ALIASES_ADD_FAIL = 'ALIASES_ADD_FAIL';
|
||||
|
||||
export const ALIASES_REMOVE_REQUEST = 'ALIASES_REMOVE_REQUEST';
|
||||
export const ALIASES_REMOVE_SUCCESS = 'ALIASES_REMOVE_SUCCESS';
|
||||
export const ALIASES_REMOVE_FAIL = 'ALIASES_REMOVE_FAIL';
|
||||
|
||||
const messages = defineMessages({
|
||||
createSuccess: { id: 'aliases.success.add', defaultMessage: 'Account alias created successfully' },
|
||||
removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' },
|
||||
});
|
||||
|
||||
export const fetchAliasesSuggestions = q => (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const params = {
|
||||
q,
|
||||
resolve: true,
|
||||
limit: 4,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchAliasesSuggestionsReady(q, data));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
};
|
||||
|
||||
export const fetchAliasesSuggestionsReady = (query, accounts) => ({
|
||||
type: ALIASES_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const clearAliasesSuggestions = () => ({
|
||||
type: ALIASES_SUGGESTIONS_CLEAR,
|
||||
});
|
||||
|
||||
export const changeAliasesSuggestions = value => ({
|
||||
type: ALIASES_SUGGESTIONS_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const addToAliases = (intl, apId) => (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const alsoKnownAs = getState().getIn(['meta', 'pleroma', 'also_known_as']);
|
||||
|
||||
dispatch(addToAliasesRequest(apId));
|
||||
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, apId] })
|
||||
.then((response => {
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.createSuccess)));
|
||||
dispatch(addToAliasesSuccess(response.data));
|
||||
}))
|
||||
.catch(err => dispatch(addToAliasesFail(err)));
|
||||
};
|
||||
|
||||
export const addToAliasesRequest = (apId) => ({
|
||||
type: ALIASES_ADD_REQUEST,
|
||||
apId,
|
||||
});
|
||||
|
||||
export const addToAliasesSuccess = me => dispatch => {
|
||||
dispatch(importFetchedAccount(me));
|
||||
dispatch({
|
||||
type: ME_PATCH_SUCCESS,
|
||||
me,
|
||||
});
|
||||
dispatch({
|
||||
type: ALIASES_ADD_SUCCESS,
|
||||
});
|
||||
};
|
||||
|
||||
export const addToAliasesFail = (apId, error) => ({
|
||||
type: ALIASES_ADD_FAIL,
|
||||
apId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeFromAliases = (intl, apId) => (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const alsoKnownAs = getState().getIn(['meta', 'pleroma', 'also_known_as']);
|
||||
|
||||
dispatch(removeFromAliasesRequest(apId));
|
||||
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== apId) })
|
||||
.then(response => {
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess)));
|
||||
dispatch(removeFromAliasesSuccess(response.data));
|
||||
})
|
||||
.catch(err => dispatch(removeFromAliasesFail(apId, err)));
|
||||
};
|
||||
|
||||
export const removeFromAliasesRequest = (apId) => ({
|
||||
type: ALIASES_REMOVE_REQUEST,
|
||||
apId,
|
||||
});
|
||||
|
||||
export const removeFromAliasesSuccess = me => dispatch => {
|
||||
dispatch(importFetchedAccount(me));
|
||||
dispatch({
|
||||
type: ME_PATCH_SUCCESS,
|
||||
me,
|
||||
});
|
||||
dispatch({
|
||||
type: ALIASES_REMOVE_SUCCESS,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeFromAliasesFail = (apId, error) => ({
|
||||
type: ALIASES_REMOVE_FAIL,
|
||||
apId,
|
||||
error,
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { baseClient } from '../api';
|
||||
|
||||
export const APP_CREATE_REQUEST = 'APP_CREATE_REQUEST';
|
||||
export const APP_CREATE_SUCCESS = 'APP_CREATE_SUCCESS';
|
||||
export const APP_CREATE_FAIL = 'APP_CREATE_FAIL';
|
||||
|
||||
export const APP_VERIFY_CREDENTIALS_REQUEST = 'APP_VERIFY_CREDENTIALS_REQUEST';
|
||||
export const APP_VERIFY_CREDENTIALS_SUCCESS = 'APP_VERIFY_CREDENTIALS_SUCCESS';
|
||||
export const APP_VERIFY_CREDENTIALS_FAIL = 'APP_VERIFY_CREDENTIALS_FAIL';
|
||||
|
||||
export function createApp(params) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: APP_CREATE_REQUEST, params });
|
||||
return baseClient().post('/api/v1/apps', params).then(({ data: app }) => {
|
||||
dispatch({ type: APP_CREATE_SUCCESS, params, app });
|
||||
return app;
|
||||
}).catch(error => {
|
||||
dispatch({ type: APP_CREATE_FAIL, params, error });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function verifyAppCredentials(token) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: APP_VERIFY_CREDENTIALS_REQUEST, token });
|
||||
return baseClient(token).get('/api/v1/apps/verify_credentials').then(({ data: app }) => {
|
||||
dispatch({ type: APP_VERIFY_CREDENTIALS_SUCCESS, token, app });
|
||||
return app;
|
||||
}).catch(error => {
|
||||
dispatch({ type: APP_VERIFY_CREDENTIALS_FAIL, token, error });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
|
@ -4,6 +4,10 @@ import { importFetchedAccount } from './importer';
|
|||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { createAccount } from 'soapbox/actions/accounts';
|
||||
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
|
||||
import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
|
||||
import { createApp } from 'soapbox/actions/apps';
|
||||
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
|
||||
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||
|
||||
|
@ -32,14 +36,6 @@ export const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST';
|
|||
export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS';
|
||||
export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL';
|
||||
|
||||
export const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST';
|
||||
export const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS';
|
||||
export const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL';
|
||||
|
||||
export const REVOKE_TOKEN_REQUEST = 'REVOKE_TOKEN_REQUEST';
|
||||
export const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS';
|
||||
export const REVOKE_TOKEN_FAIL = 'REVOKE_TOKEN_FAIL';
|
||||
|
||||
const messages = defineMessages({
|
||||
loggedOut: { id: 'auth.logged_out', defaultMessage: 'Logged out.' },
|
||||
invalidCredentials: { id: 'auth.invalid_credentials', defaultMessage: 'Wrong username or password' },
|
||||
|
@ -49,25 +45,22 @@ const noOp = () => () => new Promise(f => f());
|
|||
|
||||
function createAppAndToken() {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch(createApp()).then(() => {
|
||||
return dispatch(createAuthApp()).then(() => {
|
||||
return dispatch(createAppToken());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const appName = () => {
|
||||
const timestamp = (new Date()).toISOString();
|
||||
return `SoapboxFE_${timestamp}`; // TODO: Add commit hash
|
||||
};
|
||||
|
||||
function createApp() {
|
||||
function createAuthApp() {
|
||||
return (dispatch, getState) => {
|
||||
return api(getState, 'app').post('/api/v1/apps', {
|
||||
client_name: appName(),
|
||||
const params = {
|
||||
client_name: sourceCode.displayName,
|
||||
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
scopes: 'read write follow push admin',
|
||||
}).then(response => {
|
||||
return dispatch(authAppCreated(response.data));
|
||||
};
|
||||
|
||||
return dispatch(createApp(params)).then(app => {
|
||||
return dispatch({ type: AUTH_APP_CREATED, app });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -76,13 +69,15 @@ function createAppToken() {
|
|||
return (dispatch, getState) => {
|
||||
const app = getState().getIn(['auth', 'app']);
|
||||
|
||||
return api(getState, 'app').post('/oauth/token', {
|
||||
const params = {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'client_credentials',
|
||||
}).then(response => {
|
||||
return dispatch(authAppAuthorized(response.data));
|
||||
};
|
||||
|
||||
return dispatch(obtainOAuthToken(params)).then(token => {
|
||||
return dispatch({ type: AUTH_APP_AUTHORIZED, app, token });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -90,14 +85,17 @@ function createAppToken() {
|
|||
function createUserToken(username, password) {
|
||||
return (dispatch, getState) => {
|
||||
const app = getState().getIn(['auth', 'app']);
|
||||
return api(getState, 'app').post('/oauth/token', {
|
||||
|
||||
const params = {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'password',
|
||||
username: username,
|
||||
password: password,
|
||||
}).then(({ data: token }) => {
|
||||
};
|
||||
|
||||
return dispatch(obtainOAuthToken(params)).then(token => {
|
||||
dispatch(authLoggedIn(token));
|
||||
return token;
|
||||
});
|
||||
|
@ -111,14 +109,16 @@ export function refreshUserToken() {
|
|||
|
||||
if (!refreshToken) return dispatch(noOp());
|
||||
|
||||
return api(getState, 'app').post('/oauth/token', {
|
||||
const params = {
|
||||
client_id: app.get('client_id'),
|
||||
client_secret: app.get('client_secret'),
|
||||
refresh_token: refreshToken,
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
grant_type: 'refresh_token',
|
||||
}).then(response => {
|
||||
dispatch(authLoggedIn(response.data));
|
||||
};
|
||||
|
||||
return dispatch(obtainOAuthToken(params)).then(token => {
|
||||
dispatch(authLoggedIn(token));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -140,11 +140,13 @@ export function otpVerify(code, mfa_token) {
|
|||
};
|
||||
}
|
||||
|
||||
export function verifyCredentials(token) {
|
||||
export function verifyCredentials(token, accountUrl) {
|
||||
const baseURL = parseBaseURL(accountUrl);
|
||||
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: VERIFY_CREDENTIALS_REQUEST });
|
||||
|
||||
return baseClient(token).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
|
||||
return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
|
||||
dispatch(importFetchedAccount(account));
|
||||
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
|
||||
if (account.id === getState().get('me')) dispatch(fetchMeSuccess(account));
|
||||
|
@ -176,21 +178,26 @@ export function logIn(intl, username, password) {
|
|||
export function logOut(intl) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const me = state.get('me');
|
||||
const account = getLoggedInAccount(state);
|
||||
|
||||
return api(getState).post('/oauth/revoke', {
|
||||
const params = {
|
||||
client_id: state.getIn(['auth', 'app', 'client_id']),
|
||||
client_secret: state.getIn(['auth', 'app', 'client_secret']),
|
||||
token: state.getIn(['auth', 'users', me, 'access_token']),
|
||||
}).finally(() => {
|
||||
dispatch({ type: AUTH_LOGGED_OUT, accountId: me });
|
||||
token: state.getIn(['auth', 'users', account.get('url'), 'access_token']),
|
||||
};
|
||||
|
||||
return dispatch(revokeOAuthToken(params)).finally(() => {
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account });
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.loggedOut)));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function switchAccount(accountId, background = false) {
|
||||
return { type: SWITCH_ACCOUNT, accountId, background };
|
||||
return (dispatch, getState) => {
|
||||
const account = getState().getIn(['accounts', accountId]);
|
||||
dispatch({ type: SWITCH_ACCOUNT, account, background });
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchOwnAccounts() {
|
||||
|
@ -199,7 +206,7 @@ export function fetchOwnAccounts() {
|
|||
state.getIn(['auth', 'users']).forEach(user => {
|
||||
const account = state.getIn(['accounts', user.get('id')]);
|
||||
if (!account) {
|
||||
dispatch(verifyCredentials(user.get('access_token')));
|
||||
dispatch(verifyCredentials(user.get('access_token'), user.get('url')));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -258,13 +265,15 @@ export function changeEmail(email, password) {
|
|||
|
||||
export function deleteAccount(intl, password) {
|
||||
return (dispatch, getState) => {
|
||||
const account = getLoggedInAccount(getState());
|
||||
|
||||
dispatch({ type: DELETE_ACCOUNT_REQUEST });
|
||||
return api(getState).post('/api/pleroma/delete_account', {
|
||||
password,
|
||||
}).then(response => {
|
||||
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: DELETE_ACCOUNT_SUCCESS, response });
|
||||
dispatch({ type: AUTH_LOGGED_OUT });
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account });
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.loggedOut)));
|
||||
}).catch(error => {
|
||||
dispatch({ type: DELETE_ACCOUNT_FAIL, error, skipAlert: true });
|
||||
|
@ -290,42 +299,6 @@ export function changePassword(oldPassword, newPassword, confirmation) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchOAuthTokens() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: FETCH_TOKENS_REQUEST });
|
||||
return api(getState).get('/api/oauth_tokens.json').then(response => {
|
||||
dispatch({ type: FETCH_TOKENS_SUCCESS, tokens: response.data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: FETCH_TOKENS_FAIL });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function revokeOAuthToken(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: REVOKE_TOKEN_REQUEST, id });
|
||||
return api(getState).delete(`/api/oauth_tokens/${id}`).then(response => {
|
||||
dispatch({ type: REVOKE_TOKEN_SUCCESS, id });
|
||||
}).catch(error => {
|
||||
dispatch({ type: REVOKE_TOKEN_FAIL, id });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function authAppCreated(app) {
|
||||
return {
|
||||
type: AUTH_APP_CREATED,
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
||||
export function authAppAuthorized(app) {
|
||||
return {
|
||||
type: AUTH_APP_AUTHORIZED,
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
||||
export function authLoggedIn(token) {
|
||||
return {
|
||||
type: AUTH_LOGGED_IN,
|
||||
|
|
|
@ -2,6 +2,7 @@ import api from '../api';
|
|||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
import { favourite, unfavourite } from './interactions';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
export const EMOJI_REACT_REQUEST = 'EMOJI_REACT_REQUEST';
|
||||
export const EMOJI_REACT_SUCCESS = 'EMOJI_REACT_SUCCESS';
|
||||
|
@ -19,7 +20,7 @@ const noOp = () => () => new Promise(f => f());
|
|||
|
||||
export const simpleEmojiReact = (status, emoji) => {
|
||||
return (dispatch, getState) => {
|
||||
const emojiReacts = status.getIn(['pleroma', 'emoji_reactions']);
|
||||
const emojiReacts = status.getIn(['pleroma', 'emoji_reactions'], ImmutableList());
|
||||
|
||||
if (emoji === '👍' && status.get('favourited')) return dispatch(unfavourite(status));
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import api from '../api';
|
||||
import { importFetchedAccount } from './importer';
|
||||
import { verifyCredentials } from './auth';
|
||||
import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth';
|
||||
|
||||
export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST';
|
||||
export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS';
|
||||
|
@ -13,19 +14,31 @@ export const ME_PATCH_FAIL = 'ME_PATCH_FAIL';
|
|||
|
||||
const noOp = () => new Promise(f => f());
|
||||
|
||||
const getMeId = state => state.get('me') || getAuthUserId(state);
|
||||
|
||||
const getMeUrl = state => {
|
||||
const accountId = getMeId(state);
|
||||
return state.getIn(['accounts', accountId, 'url']) || getAuthUserUrl(state);
|
||||
};
|
||||
|
||||
const getMeToken = state => {
|
||||
// Fallback for upgrading IDs to URLs
|
||||
const accountUrl = getMeUrl(state) || state.getIn(['auth', 'me']);
|
||||
return state.getIn(['auth', 'users', accountUrl, 'access_token']);
|
||||
};
|
||||
|
||||
export function fetchMe() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const me = state.get('me') || state.getIn(['auth', 'me']);
|
||||
const token = state.getIn(['auth', 'users', me, 'access_token']);
|
||||
const token = getMeToken(state);
|
||||
const accountUrl = getMeUrl(state);
|
||||
|
||||
if (!token) {
|
||||
dispatch({ type: ME_FETCH_SKIP }); return noOp();
|
||||
}
|
||||
|
||||
dispatch(fetchMeRequest());
|
||||
return dispatch(verifyCredentials(token)).catch(error => {
|
||||
return dispatch(verifyCredentials(token, accountUrl)).catch(error => {
|
||||
dispatch(fetchMeFail(error));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { baseClient } from '../api';
|
||||
|
||||
export const OAUTH_TOKEN_CREATE_REQUEST = 'OAUTH_TOKEN_CREATE_REQUEST';
|
||||
export const OAUTH_TOKEN_CREATE_SUCCESS = 'OAUTH_TOKEN_CREATE_SUCCESS';
|
||||
export const OAUTH_TOKEN_CREATE_FAIL = 'OAUTH_TOKEN_CREATE_FAIL';
|
||||
|
||||
export const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST';
|
||||
export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS';
|
||||
export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
|
||||
|
||||
export function obtainOAuthToken(params) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
|
||||
return baseClient().post('/oauth/token', params).then(({ data: token }) => {
|
||||
dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token });
|
||||
return token;
|
||||
}).catch(error => {
|
||||
dispatch({ type: OAUTH_TOKEN_CREATE_FAIL, params, error });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function revokeOAuthToken(params) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params });
|
||||
return baseClient().post('/oauth/revoke', params).then(({ data }) => {
|
||||
dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: OAUTH_TOKEN_REVOKE_FAIL, params, error });
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
||||
|
||||
const getPinnedHosts = state => {
|
||||
const settings = getSettings(state);
|
||||
return settings.getIn(['remote_timeline', 'pinnedHosts']);
|
||||
};
|
||||
|
||||
export function pinHost(host) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const pinnedHosts = getPinnedHosts(state);
|
||||
|
||||
return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host)));
|
||||
};
|
||||
}
|
||||
|
||||
export function unpinHost(host) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const pinnedHosts = getPinnedHosts(state);
|
||||
|
||||
return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.delete(host)));
|
||||
};
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import api from '../api';
|
||||
|
||||
export const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST';
|
||||
export const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS';
|
||||
export const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL';
|
||||
|
||||
export const REVOKE_TOKEN_REQUEST = 'REVOKE_TOKEN_REQUEST';
|
||||
export const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS';
|
||||
export const REVOKE_TOKEN_FAIL = 'REVOKE_TOKEN_FAIL';
|
||||
|
||||
export function fetchOAuthTokens() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: FETCH_TOKENS_REQUEST });
|
||||
return api(getState).get('/api/oauth_tokens.json').then(({ data: tokens }) => {
|
||||
dispatch({ type: FETCH_TOKENS_SUCCESS, tokens });
|
||||
}).catch(error => {
|
||||
dispatch({ type: FETCH_TOKENS_FAIL });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function revokeOAuthTokenById(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: REVOKE_TOKEN_REQUEST, id });
|
||||
return api(getState).delete(`/api/oauth_tokens/${id}`).then(() => {
|
||||
dispatch({ type: REVOKE_TOKEN_SUCCESS, id });
|
||||
}).catch(error => {
|
||||
dispatch({ type: REVOKE_TOKEN_FAIL, id });
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,7 +1,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 { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import uuid from '../uuid';
|
||||
import { createSelector } from 'reselect';
|
||||
|
@ -127,6 +127,13 @@ export const defaultSettings = ImmutableMap({
|
|||
}),
|
||||
}),
|
||||
|
||||
account_timeline: ImmutableMap({
|
||||
shows: ImmutableMap({
|
||||
reblog: true,
|
||||
pinned: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
trends: ImmutableMap({
|
||||
show: true,
|
||||
}),
|
||||
|
@ -136,6 +143,10 @@ export const defaultSettings = ImmutableMap({
|
|||
ImmutableMap({ id: 'HOME', uuid: uuid(), params: {} }),
|
||||
ImmutableMap({ id: 'NOTIFICATIONS', uuid: uuid(), params: {} }),
|
||||
]),
|
||||
|
||||
remote_timeline: ImmutableMap({
|
||||
pinnedHosts: ImmutableOrderedSet(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const getSettings = createSelector([
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
import { getAccessToken, getAppToken } from 'soapbox/utils/auth';
|
||||
import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
|
@ -16,8 +17,7 @@ export const getNext = response => {
|
|||
return link ? link.uri : null;
|
||||
};
|
||||
|
||||
const getToken = (getState, authType) => {
|
||||
const state = getState();
|
||||
const getToken = (state, authType) => {
|
||||
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
|
||||
};
|
||||
|
||||
|
@ -29,8 +29,17 @@ const maybeParseJSON = data => {
|
|||
}
|
||||
};
|
||||
|
||||
export const baseClient = accessToken => {
|
||||
const getAuthBaseURL = createSelector([
|
||||
(state, me) => state.getIn(['accounts', me, 'url']),
|
||||
(state, me) => state.getIn(['auth', 'me']),
|
||||
], (accountUrl, authUserUrl) => {
|
||||
const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
|
||||
return baseURL !== window.location.origin ? baseURL : '';
|
||||
});
|
||||
|
||||
export const baseClient = (accessToken, baseURL = '') => {
|
||||
return axios.create({
|
||||
baseURL,
|
||||
headers: Object.assign(accessToken ? {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
} : {}),
|
||||
|
@ -40,6 +49,10 @@ export const baseClient = accessToken => {
|
|||
};
|
||||
|
||||
export default (getState, authType = 'user') => {
|
||||
const accessToken = getToken(getState, authType);
|
||||
return baseClient(accessToken);
|
||||
const state = getState();
|
||||
const accessToken = getToken(state, authType);
|
||||
const me = state.get('me');
|
||||
const baseURL = getAuthBaseURL(state, me);
|
||||
|
||||
return baseClient(accessToken, baseURL);
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ const messages = defineMessages({
|
|||
admin_settings: { id: 'navigation_bar.admin_settings', defaultMessage: 'Admin settings' },
|
||||
soapbox_config: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' },
|
||||
import_data: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
|
||||
account_aliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
|
||||
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
|
@ -258,6 +259,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon id='cloud-upload' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.import_data)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/settings/aliases' onClick={this.handleClose}>
|
||||
<Icon id='suitcase' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.account_aliases)}</span>
|
||||
</NavLink>
|
||||
<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>
|
||||
|
|
|
@ -14,6 +14,7 @@ import EmojiSelector from 'soapbox/components/emoji_selector';
|
|||
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts';
|
||||
import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
|
@ -93,6 +94,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
allowedEmoji: ImmutablePropTypes.list,
|
||||
emojiSelectorFocused: PropTypes.bool,
|
||||
handleEmojiSelectorUnfocus: PropTypes.func.isRequired,
|
||||
features: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -132,16 +134,26 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
isMobile = () => window.matchMedia('only screen and (max-width: 895px)').matches;
|
||||
|
||||
handleLikeButtonHover = e => {
|
||||
if (!this.isMobile()) this.setState({ emojiSelectorVisible: true });
|
||||
const { features } = this.props;
|
||||
|
||||
if (features.emojiReacts && !this.isMobile()) {
|
||||
this.setState({ emojiSelectorVisible: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeButtonLeave = e => {
|
||||
if (!this.isMobile()) this.setState({ emojiSelectorVisible: false });
|
||||
const { features } = this.props;
|
||||
|
||||
if (features.emojiReacts && !this.isMobile()) {
|
||||
this.setState({ emojiSelectorVisible: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeButtonClick = e => {
|
||||
const { features } = this.props;
|
||||
const meEmojiReact = getReactForStatus(this.props.status, this.props.allowedEmoji) || '👍';
|
||||
if (this.isMobile()) {
|
||||
|
||||
if (features.emojiReacts && this.isMobile()) {
|
||||
if (this.state.emojiSelectorVisible) {
|
||||
this.handleReactClick(meEmojiReact)();
|
||||
} else {
|
||||
|
@ -362,7 +374,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { status, intl, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorUnfocus } = this.props;
|
||||
const { status, intl, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorUnfocus, features } = this.props;
|
||||
const { emojiSelectorVisible } = this.state;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
|
@ -427,7 +439,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
>
|
||||
<EmojiSelector
|
||||
onReact={this.handleReactClick}
|
||||
visible={emojiSelectorVisible}
|
||||
visible={features.emojiReacts && emojiSelectorVisible}
|
||||
focused={emojiSelectorFocused}
|
||||
onUnfocus={handleEmojiSelectorUnfocus}
|
||||
/>
|
||||
|
@ -456,11 +468,13 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
me,
|
||||
isStaff: account ? isStaff(account) : false,
|
||||
isAdmin: account ? isAdmin(account) : false,
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ const mapStateToProps = (state) => {
|
|||
demetricator: settings.get('demetricator'),
|
||||
locale: validLocale(locale) ? locale : 'en',
|
||||
themeCss: generateThemeCss(soapboxConfig.get('brandColor')),
|
||||
brandColor: soapboxConfig.get('brandColor'),
|
||||
themeMode: settings.get('themeMode'),
|
||||
halloween: settings.get('halloween'),
|
||||
customCss: soapboxConfig.get('customCss'),
|
||||
|
@ -74,6 +75,7 @@ class SoapboxMount extends React.PureComponent {
|
|||
locale: PropTypes.string.isRequired,
|
||||
themeCss: PropTypes.string,
|
||||
themeMode: PropTypes.string,
|
||||
brandColor: PropTypes.string,
|
||||
customCss: ImmutablePropTypes.list,
|
||||
halloween: PropTypes.bool,
|
||||
dispatch: PropTypes.func,
|
||||
|
@ -134,6 +136,7 @@ class SoapboxMount extends React.PureComponent {
|
|||
{customCss && customCss.map(css => (
|
||||
<link rel='stylesheet' href={css} key={css} />
|
||||
))}
|
||||
<meta name='theme-color' content={this.props.brandColor} />
|
||||
</Helmet>
|
||||
<BrowserRouter>
|
||||
<ScrollContext>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
export default @injectIntl
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='account_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'pinned']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='account_timeline.column_settings.show_pinned' defaultMessage='Show pinned posts' />}
|
||||
/>
|
||||
<SettingToggle
|
||||
prefix='account_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reblog']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { getSettings, changeSetting } from '../../../actions/settings';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: getSettings(state).get('account_timeline'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
onChange(key, checked) {
|
||||
dispatch(changeSetting(['account_timeline', ...key], checked));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
|
@ -4,9 +4,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import PropTypes from 'prop-types';
|
||||
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import StatusList from '../../components/status_list';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
@ -15,47 +17,57 @@ import MissingIndicator from 'soapbox/components/missing_indicator';
|
|||
import { NavLink } from 'react-router-dom';
|
||||
import { fetchPatronAccount } from '../../actions/patron';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { makeGetStatusIds } from 'soapbox/selectors';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const mapStateToProps = (state, { params, withReplies = false }) => {
|
||||
const username = params.username || '';
|
||||
const me = state.get('me');
|
||||
const accounts = state.getIn(['accounts']);
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatusIds = makeGetStatusIds();
|
||||
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
let accountApId = null;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
accountApId = account ? account.get('url') : '';
|
||||
}
|
||||
const mapStateToProps = (state, { params, withReplies = false }) => {
|
||||
const username = params.username || '';
|
||||
const me = state.get('me');
|
||||
const accounts = state.getIn(['accounts']);
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
let accountApId = null;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
accountApId = account ? account.get('url') : '';
|
||||
}
|
||||
|
||||
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
|
||||
const unavailable = (me === accountId) ? false : isBlocked;
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
|
||||
return {
|
||||
accountId,
|
||||
unavailable,
|
||||
accountUsername,
|
||||
accountApId,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableOrderedSet()),
|
||||
featuredStatusIds: withReplies ? ImmutableOrderedSet() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableOrderedSet()),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
me,
|
||||
patronEnabled: soapboxConfig.getIn(['extensions', 'patron', 'enabled']),
|
||||
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
|
||||
const unavailable = (me === accountId) ? false : isBlocked;
|
||||
const showPins = getSettings(state).getIn(['account_timeline', 'shows', 'pinned']) && !withReplies;
|
||||
|
||||
return {
|
||||
accountId,
|
||||
unavailable,
|
||||
accountUsername,
|
||||
accountApId,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
|
||||
featuredStatusIds: showPins ? getStatusIds(state, { type: `account:${accountId}:pinned`, prefix: 'account_timeline' }) : ImmutableOrderedSet(),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
me,
|
||||
patronEnabled: soapboxConfig.getIn(['extensions', 'patron', 'enabled']),
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
export default @connect(makeMapStateToProps)
|
||||
class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -70,6 +82,11 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
animating: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { params: { username }, accountId, accountApId, withReplies, me, patronEnabled } = this.props;
|
||||
|
||||
|
@ -115,8 +132,18 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleToggleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.setState({ collapsed: !this.state.collapsed, animating: true });
|
||||
}
|
||||
|
||||
handleTransitionEnd = () => {
|
||||
this.setState({ animating: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (
|
||||
|
@ -156,6 +183,16 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
<NavLink exact to={`/@${accountUsername}/media`}>
|
||||
<FormattedMessage id='account.media' defaultMessage='Media' />
|
||||
</NavLink>
|
||||
<div className='column-header__buttons'>
|
||||
<button onClick={this.handleToggleClick}>
|
||||
<Icon id='sliders' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('column-header__collapsible', { collapsed, animating })} onTransitionEnd={this.handleTransitionEnd}>
|
||||
<div className='column-header__collapsible-inner'>
|
||||
<ColumnSettingsContainer />
|
||||
</div>
|
||||
</div>
|
||||
<StatusList
|
||||
scrollKey='account_timeline'
|
||||
|
|
|
@ -138,7 +138,7 @@ class Dashboard extends ImmutablePureComponent {
|
|||
<div className='dashwidget'>
|
||||
<h4><FormattedMessage id='admin.dashwidgets.software_header' defaultMessage='Software' /></h4>
|
||||
<ul>
|
||||
<li>Soapbox FE <span className='pull-right'>{sourceCode.version}</span></li>
|
||||
<li>{sourceCode.displayName} <span className='pull-right'>{sourceCode.version}</span></li>
|
||||
<li>{v.software} <span className='pull-right'>{v.version}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { addToAliases } from '../../../actions/aliases';
|
||||
|
||||
const messages = defineMessages({
|
||||
add: { id: 'aliases.account.add', defaultMessage: 'Create alias' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added }) => {
|
||||
const account = getAccount(state, accountId);
|
||||
const apId = account.getIn(['pleroma', 'ap_id']);
|
||||
|
||||
return {
|
||||
account,
|
||||
apId,
|
||||
added: typeof added === 'undefined' ? state.getIn(['meta', 'pleroma', 'also_known_as']).includes(apId) : added,
|
||||
me: state.get('me'),
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onAdd: (intl, apId) => dispatch(addToAliases(intl, apId)),
|
||||
});
|
||||
|
||||
export default @connect(makeMapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
apId: PropTypes.string.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
handleOnAdd = () => this.props.onAdd(this.props.intl, this.props.apId);
|
||||
|
||||
render() {
|
||||
const { account, accountId, intl, added, me } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (!added && accountId !== me) {
|
||||
button = (
|
||||
<div className='account__relationship'>
|
||||
<IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={this.handleOnAdd} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { fetchAliasesSuggestions, clearAliasesSuggestions, changeAliasesSuggestions } from '../../../actions/aliases';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import Button from 'soapbox/components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'aliases.search', defaultMessage: 'Search your old account' },
|
||||
searchTitle: { id: 'tabs_bar.search', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['aliases', 'suggestions', 'value']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: value => dispatch(fetchAliasesSuggestions(value)),
|
||||
onClear: () => dispatch(clearAliasesSuggestions()),
|
||||
onChange: value => dispatch(changeAliasesSuggestions(value)),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class Search extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
|
||||
handleClear = () => {
|
||||
this.props.onClear();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div className='aliases_search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
<Button onClick={this.handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import Search from './components/search';
|
||||
import Account from './components/account';
|
||||
import { removeFromAliases } from '../../actions/aliases';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.aliases', defaultMessage: 'Account aliases' },
|
||||
subheading_add_new: { id: 'column.aliases.subheading_add_new', defaultMessage: 'Add New Alias' },
|
||||
create_error: { id: 'column.aliases.create_error', defaultMessage: 'Error creating alias' },
|
||||
delete_error: { id: 'column.aliases.delete_error', defaultMessage: 'Error deleting alias' },
|
||||
subheading_aliases: { id: 'column.aliases.subheading_aliases', defaultMessage: 'Current aliases' },
|
||||
delete: { id: 'column.aliases.delete', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
aliases: state.getIn(['meta', 'pleroma', 'also_known_as']),
|
||||
searchAccountIds: state.getIn(['aliases', 'suggestions', 'items']),
|
||||
loaded: state.getIn(['aliases', 'suggestions', 'loaded']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Aliases extends ImmutablePureComponent {
|
||||
|
||||
handleFilterDelete = e => {
|
||||
const { dispatch, intl } = this.props;
|
||||
dispatch(removeFromAliases(intl, e.currentTarget.dataset.value));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, aliases, searchAccountIds, loaded } = this.props;
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.aliases' defaultMessage="You haven't created any account alias yet." />;
|
||||
|
||||
return (
|
||||
<Column className='aliases-settings-panel' icon='suitcase' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading_add_new)} />
|
||||
<Search />
|
||||
{
|
||||
loaded && searchAccountIds.size === 0 ? (
|
||||
<div className='aliases__accounts empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.aliases.suggestions' defaultMessage='There are no account suggestions available for the provided term.' />
|
||||
</div>
|
||||
) : (
|
||||
<div className='aliases__accounts'>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading_aliases)} />
|
||||
<div className='aliases-settings-panel'>
|
||||
<ScrollableList
|
||||
scrollKey='aliases'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{aliases.map((alias, i) => (
|
||||
<div key={i} className='alias__container'>
|
||||
<div className='alias__details'>
|
||||
<span className='alias__list-label'><FormattedMessage id='aliases.account_label' defaultMessage='Old account:' /></span>
|
||||
<span className='alias__list-value'>{alias}</span>
|
||||
</div>
|
||||
<div className='alias__delete' role='button' tabIndex='0' onClick={this.handleFilterDelete} data-value={alias} aria-label={intl.formatMessage(messages.delete)}>
|
||||
<Icon className='alias__delete-icon' id='times' size={40} />
|
||||
<span className='alias__delete-label'><FormattedMessage id='aliases.aliases_list_delete' defaultMessage='Unlink alias' /></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,10 +18,6 @@ class ColumnSettings extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='community_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='community_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
|
|
@ -19,9 +19,10 @@ export default class SearchResults extends ImmutablePureComponent {
|
|||
submitted: PropTypes.bool,
|
||||
expandSearch: PropTypes.func.isRequired,
|
||||
selectedFilter: PropTypes.string.isRequired,
|
||||
selectFilter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleLoadMore = () => this.props.expandSearch(this.state.selectedFilter);
|
||||
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
|
||||
|
||||
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
|
||||
|
||||
|
|
|
@ -9,6 +9,40 @@ import classNames from 'classnames';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import Blurhash from 'soapbox/components/blurhash';
|
||||
|
||||
const MIMETYPE_ICONS = {
|
||||
'application/x-freearc': 'file-archive-o',
|
||||
'application/x-bzip': 'file-archive-o',
|
||||
'application/x-bzip2': 'file-archive-o',
|
||||
'application/gzip': 'file-archive-o',
|
||||
'application/vnd.rar': 'file-archive-o',
|
||||
'application/x-tar': 'file-archive-o',
|
||||
'application/zip': 'file-archive-o',
|
||||
'application/x-7z-compressed': 'file-archive-o',
|
||||
'application/x-csh': 'file-code-o',
|
||||
'application/html': 'file-code-o',
|
||||
'text/javascript': 'file-code-o',
|
||||
'application/json': 'file-code-o',
|
||||
'application/ld+json': 'file-code-o',
|
||||
'application/x-httpd-php': 'file-code-o',
|
||||
'application/x-sh': 'file-code-o',
|
||||
'application/xhtml+xml': 'file-code-o',
|
||||
'application/xml': 'file-code-o',
|
||||
'application/epub+zip': 'file-epub-o',
|
||||
'application/vnd.oasis.opendocument.spreadsheet': 'file-excel-o',
|
||||
'application/vnd.ms-excel': 'file-excel-o',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'file-excel-o',
|
||||
'application/pdf': 'file-pdf-o',
|
||||
'application/vnd.oasis.opendocument.presentation': 'file-powerpoint-o',
|
||||
'application/vnd.ms-powerpoint': 'file-powerpoint-o',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'file-powerpoint-o',
|
||||
'text/plain': 'file-text-o',
|
||||
'application/rtf': 'file-text-o',
|
||||
'application/msword': 'file-word-o',
|
||||
'application/x-abiword': 'file-word-o',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'file-word-o',
|
||||
'application/vnd.oasis.opendocument.text': 'file-word-o',
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||
|
@ -100,6 +134,11 @@ class Upload extends ImmutablePureComponent {
|
|||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
const mediaType = media.get('type');
|
||||
const uploadIcon = mediaType === 'unknown' && (
|
||||
<Icon
|
||||
id={MIMETYPE_ICONS[media.getIn(['pleroma', 'mime_type'])] || 'paperclip'}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
|
||||
|
@ -115,7 +154,7 @@ class Upload extends ImmutablePureComponent {
|
|||
>
|
||||
<div className={classNames('compose-form__upload__actions', { active })}>
|
||||
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||
<button className='icon-button' onClick={this.handleOpenModal}><Icon id='search-plus' /> <FormattedMessage id='upload_form.preview' defaultMessage='Preview' /></button>
|
||||
{mediaType !== 'unknown' && <button className='icon-button' onClick={this.handleOpenModal}><Icon id='search-plus' /> <FormattedMessage id='upload_form.preview' defaultMessage='Preview' /></button>}
|
||||
</div>
|
||||
|
||||
<div className={classNames('compose-form__upload-description', { active })}>
|
||||
|
@ -140,6 +179,7 @@ class Upload extends ImmutablePureComponent {
|
|||
<source src={media.get('preview_url')} />
|
||||
</video>
|
||||
)}
|
||||
{uploadIcon}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -7,6 +7,7 @@ import Column from '../../components/column';
|
|||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import HomeColumnHeader from '../../components/home_column_header';
|
||||
import Accordion from 'soapbox/features/ui/components/accordion';
|
||||
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
||||
import { expandPublicTimeline } from '../../actions/timelines';
|
||||
import { connectPublicStream } from '../../actions/streaming';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -101,6 +102,7 @@ class CommunityTimeline extends React.PureComponent {
|
|||
<HomeColumnHeader activeItem='fediverse' active={hasUnread} >
|
||||
<ColumnSettingsContainer />
|
||||
</HomeColumnHeader>
|
||||
<PinnedHostsPicker />
|
||||
{showExplanationBox && <div className='explanation-box'>
|
||||
<Accordion
|
||||
headline={<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const settings = getSettings(state);
|
||||
|
||||
return {
|
||||
pinnedHosts: settings.getIn(['remote_timeline', 'pinnedHosts']),
|
||||
};
|
||||
};
|
||||
|
||||
class PinnedHostPicker extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
pinnedHosts: ImmutablePropTypes.orderedSet,
|
||||
host: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { pinnedHosts, host: activeHost } = this.props;
|
||||
|
||||
if (!pinnedHosts || pinnedHosts.isEmpty()) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='pinned-hosts-picker'>
|
||||
{pinnedHosts.map(host => (
|
||||
<div className={classNames('pinned-host', { 'active': host === activeHost })} key={host}>
|
||||
<Link to={`/timeline/${host}`}>{host}</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PinnedHostPicker);
|
|
@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
|||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import HomeColumnHeader from '../../components/home_column_header';
|
||||
import PinnedHostsPicker from './components/pinned_hosts_picker';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { expandRemoteTimeline } from '../../actions/timelines';
|
||||
import { connectRemoteStream } from '../../actions/streaming';
|
||||
|
@ -26,6 +27,7 @@ const mapStateToProps = (state, props) => {
|
|||
onlyMedia,
|
||||
hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}:${instance}`, 'unread']) > 0,
|
||||
instance,
|
||||
pinned: settings.getIn(['remote_timeline', 'pinnedHosts']).includes(instance),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -44,6 +46,7 @@ class RemoteTimeline extends React.PureComponent {
|
|||
onlyMedia: PropTypes.bool,
|
||||
timelineId: PropTypes.string,
|
||||
instance: PropTypes.string.isRequired,
|
||||
pinned: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -79,19 +82,20 @@ class RemoteTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { intl, hasUnread, onlyMedia, timelineId, instance } = this.props;
|
||||
const { intl, hasUnread, onlyMedia, timelineId, instance, pinned } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<HomeColumnHeader activeItem='fediverse' active={hasUnread} />
|
||||
<div className='timeline-filter-message'>
|
||||
<PinnedHostsPicker host={instance} />
|
||||
{!pinned && <div className='timeline-filter-message'>
|
||||
<IconButton icon='close' onClick={this.handleCloseClick} />
|
||||
<FormattedMessage
|
||||
id='remote_timeline.filter_message'
|
||||
defaultMessage='You are viewing the timeline of {instance}.'
|
||||
values={{ instance }}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
<StatusListContainer
|
||||
scrollKey={`${timelineId}_${instance}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}:${instance}`}
|
||||
|
|
|
@ -15,10 +15,9 @@ import {
|
|||
import {
|
||||
changeEmail,
|
||||
changePassword,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
deleteAccount,
|
||||
} from 'soapbox/actions/auth';
|
||||
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';
|
||||
import { fetchUserMfaSettings } from '../../actions/mfa';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { changeSetting, getSettings } from 'soapbox/actions/settings';
|
||||
|
@ -306,7 +305,7 @@ class AuthTokenList extends ImmutablePureComponent {
|
|||
|
||||
handleRevoke = id => {
|
||||
return e => {
|
||||
this.props.dispatch(revokeOAuthToken(id));
|
||||
this.props.dispatch(revokeOAuthTokenById(id));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { isStaff, isAdmin } from 'soapbox/utils/accounts';
|
|||
import { isUserTouching } from 'soapbox/is_mobile';
|
||||
import EmojiSelector from 'soapbox/components/emoji_selector';
|
||||
import { getReactForStatus } from 'soapbox/utils/emoji_reacts';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
|
@ -54,11 +55,13 @@ const messages = defineMessages({
|
|||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
me,
|
||||
isStaff: account ? isStaff(account) : false,
|
||||
isAdmin: account ? isAdmin(account) : false,
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -103,6 +106,7 @@ class ActionBar extends React.PureComponent {
|
|||
emojiSelectorFocused: PropTypes.bool,
|
||||
handleEmojiSelectorExpand: PropTypes.func.isRequired,
|
||||
handleEmojiSelectorUnfocus: PropTypes.func.isRequired,
|
||||
features: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -146,16 +150,26 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLikeButtonHover = e => {
|
||||
if (!isUserTouching()) this.setState({ emojiSelectorVisible: true });
|
||||
const { features } = this.props;
|
||||
|
||||
if (features.emojiReacts && !isUserTouching()) {
|
||||
this.setState({ emojiSelectorVisible: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeButtonLeave = e => {
|
||||
if (!isUserTouching()) this.setState({ emojiSelectorVisible: false });
|
||||
const { features } = this.props;
|
||||
|
||||
if (features.emojiReacts && !isUserTouching()) {
|
||||
this.setState({ emojiSelectorVisible: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeButtonClick = e => {
|
||||
const { features } = this.props;
|
||||
const meEmojiReact = getReactForStatus(this.props.status, this.props.allowedEmoji) || '👍';
|
||||
if (isUserTouching()) {
|
||||
|
||||
if (features.emojiReacts && isUserTouching()) {
|
||||
if (this.state.emojiSelectorVisible) {
|
||||
this.handleReactClick(meEmojiReact)();
|
||||
} else {
|
||||
|
@ -278,7 +292,7 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { status, intl, me, isStaff, isAdmin, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorExpand, handleEmojiSelectorUnfocus } = this.props;
|
||||
const { status, intl, me, isStaff, isAdmin, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorExpand, handleEmojiSelectorUnfocus, features } = this.props;
|
||||
const { emojiSelectorVisible } = this.state;
|
||||
const ownAccount = status.getIn(['account', 'id']) === me;
|
||||
|
||||
|
@ -391,7 +405,7 @@ class ActionBar extends React.PureComponent {
|
|||
>
|
||||
<EmojiSelector
|
||||
onReact={this.handleReactClick}
|
||||
visible={emojiSelectorVisible}
|
||||
visible={features.emojiReacts && emojiSelectorVisible}
|
||||
focused={emojiSelectorFocused}
|
||||
onUnfocus={handleEmojiSelectorUnfocus}
|
||||
/>
|
||||
|
|
|
@ -4,28 +4,27 @@ import React from 'react';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { makeGetRemoteInstance } from 'soapbox/selectors';
|
||||
import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { isAdmin } from 'soapbox/utils/accounts';
|
||||
import { pinHost, unpinHost } from 'soapbox/actions/remote_timeline';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
|
||||
const getRemoteInstance = makeGetRemoteInstance();
|
||||
|
||||
const messages = defineMessages({
|
||||
editFederation: { id: 'remote_instance.edit_federation', defaultMessage: 'Edit federation' },
|
||||
pinHost: { id: 'remote_instance.pin_host', defaultMessage: 'Pin {host}' },
|
||||
unpinHost: { id: 'remote_instance.unpin_host', defaultMessage: 'Unpin {host}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { host }) => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
const settings = getSettings(state);
|
||||
|
||||
return {
|
||||
instance: state.get('instance'),
|
||||
remoteInstance: getRemoteInstance(state, host),
|
||||
isAdmin: isAdmin(account),
|
||||
pinned: settings.getIn(['remote_timeline', 'pinnedHosts']).includes(host),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -38,40 +37,43 @@ class InstanceInfoPanel extends ImmutablePureComponent {
|
|||
host: PropTypes.string.isRequired,
|
||||
instance: ImmutablePropTypes.map,
|
||||
remoteInstance: ImmutablePropTypes.map,
|
||||
isAdmin: PropTypes.bool,
|
||||
pinned: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleEditFederation = e => {
|
||||
const { dispatch, host } = this.props;
|
||||
dispatch(openModal('EDIT_FEDERATION', { host }));
|
||||
handlePinHost = e => {
|
||||
const { dispatch, host, pinned } = this.props;
|
||||
|
||||
if (!pinned) {
|
||||
dispatch(pinHost(host));
|
||||
} else {
|
||||
dispatch(unpinHost(host));
|
||||
}
|
||||
}
|
||||
|
||||
makeMenu = () => {
|
||||
const { intl } = this.props;
|
||||
const { intl, host, pinned } = this.props;
|
||||
|
||||
return [{
|
||||
text: intl.formatMessage(messages.editFederation),
|
||||
action: this.handleEditFederation,
|
||||
text: intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host }),
|
||||
action: this.handlePinHost,
|
||||
}];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { remoteInstance, isAdmin } = this.props;
|
||||
const { remoteInstance, pinned } = this.props;
|
||||
const menu = this.makeMenu();
|
||||
const icon = pinned ? 'thumb-tack' : 'globe-w';
|
||||
|
||||
return (
|
||||
<div className='wtf-panel instance-federation-panel'>
|
||||
<div className='wtf-panel-header'>
|
||||
<i role='img' alt='gavel' className='fa fa-gavel wtf-panel-header__icon' />
|
||||
<i role='img' alt={icon} className={`fa fa-${icon} wtf-panel-header__icon`} />
|
||||
<span className='wtf-panel-header__label'>
|
||||
<span><FormattedMessage id='remote_instance.federation_panel.heading' defaultMessage='Federation Restrictions' /></span>
|
||||
<span>{remoteInstance.get('host')}</span>
|
||||
</span>
|
||||
{isAdmin && <div className='wtf-panel__menu'>
|
||||
<div className='wtf-panel__menu'>
|
||||
<DropdownMenu items={menu} icon='ellipsis-v' size={18} direction='right' />
|
||||
</div>}
|
||||
</div>
|
||||
<div className='wtf-panel__content'>
|
||||
<InstanceRestrictions remoteInstance={remoteInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { makeGetRemoteInstance } from 'soapbox/selectors';
|
||||
import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { isAdmin } from 'soapbox/utils/accounts';
|
||||
|
||||
const getRemoteInstance = makeGetRemoteInstance();
|
||||
|
||||
const messages = defineMessages({
|
||||
editFederation: { id: 'remote_instance.edit_federation', defaultMessage: 'Edit federation' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { host }) => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
|
||||
return {
|
||||
instance: state.get('instance'),
|
||||
remoteInstance: getRemoteInstance(state, host),
|
||||
isAdmin: isAdmin(account),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps, null, null, { forwardRef: true })
|
||||
@injectIntl
|
||||
class InstanceModerationPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
host: PropTypes.string.isRequired,
|
||||
instance: ImmutablePropTypes.map,
|
||||
remoteInstance: ImmutablePropTypes.map,
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleEditFederation = e => {
|
||||
const { dispatch, host } = this.props;
|
||||
dispatch(openModal('EDIT_FEDERATION', { host }));
|
||||
}
|
||||
|
||||
makeMenu = () => {
|
||||
const { intl } = this.props;
|
||||
|
||||
return [{
|
||||
text: intl.formatMessage(messages.editFederation),
|
||||
action: this.handleEditFederation,
|
||||
}];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { remoteInstance, isAdmin } = this.props;
|
||||
const menu = this.makeMenu();
|
||||
|
||||
return (
|
||||
<div className='wtf-panel instance-federation-panel'>
|
||||
<div className='wtf-panel-header'>
|
||||
<i role='img' alt='gavel' className='fa fa-gavel wtf-panel-header__icon' />
|
||||
<span className='wtf-panel-header__label'>
|
||||
<span><FormattedMessage id='remote_instance.federation_panel.heading' defaultMessage='Federation Restrictions' /></span>
|
||||
</span>
|
||||
{isAdmin && <div className='wtf-panel__menu'>
|
||||
<DropdownMenu items={menu} icon='ellipsis-v' size={18} direction='right' />
|
||||
</div>}
|
||||
</div>
|
||||
<div className='wtf-panel__content'>
|
||||
<InstanceRestrictions remoteInstance={remoteInstance} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -38,6 +38,7 @@ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => (
|
|||
{isAdmin(account) && <li><a href='/pleroma/admin'><FormattedMessage id='navigation_bar.admin_settings' defaultMessage='AdminFE' /></a></li>}
|
||||
{isAdmin(account) && <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><Link to='/settings/aliases'><FormattedMessage id='navigation_bar.account_aliases' defaultMessage='Account aliases' /></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>
|
||||
|
@ -49,7 +50,7 @@ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => (
|
|||
id='getting_started.open_source_notice'
|
||||
defaultMessage='{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).'
|
||||
values={{
|
||||
code_name: sourceCode.name,
|
||||
code_name: sourceCode.displayName,
|
||||
code_link: <a href={sourceCode.url} rel='noopener' target='_blank'>{sourceCode.repository}</a>,
|
||||
code_version: sourceCode.version,
|
||||
}}
|
||||
|
|
|
@ -15,6 +15,7 @@ import Icon from '../../../components/icon';
|
|||
import ThemeToggle from '../../ui/components/theme_toggle_container';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { isStaff } from 'soapbox/utils/accounts';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
post: { id: 'tabs_bar.post', defaultMessage: 'Post' },
|
||||
|
@ -32,6 +33,7 @@ class TabsBar extends React.PureComponent {
|
|||
dashboardCount: PropTypes.number,
|
||||
notificationCount: PropTypes.number,
|
||||
chatsCount: PropTypes.number,
|
||||
features: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
|
@ -52,7 +54,7 @@ class TabsBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
getNavLinks() {
|
||||
const { intl: { formatMessage }, logo, account, dashboardCount, notificationCount, chatsCount } = this.props;
|
||||
const { intl: { formatMessage }, logo, account, dashboardCount, notificationCount, chatsCount, features } = this.props;
|
||||
const links = [];
|
||||
if (logo) {
|
||||
links.push(
|
||||
|
@ -73,7 +75,7 @@ class TabsBar extends React.PureComponent {
|
|||
<span><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></span>
|
||||
</NavLink>);
|
||||
}
|
||||
if (account) {
|
||||
if (features.chats && account) {
|
||||
links.push(
|
||||
<NavLink key='chats' className='tabs-bar__link tabs-bar__link--chats' to='/chats' data-preview-title-id='column.chats'>
|
||||
<IconWithCounter icon='comment' count={chatsCount} />
|
||||
|
@ -155,12 +157,15 @@ const mapStateToProps = state => {
|
|||
const me = state.get('me');
|
||||
const reportsCount = state.getIn(['admin', 'openReports']).count();
|
||||
const approvalCount = state.getIn(['admin', 'awaitingApproval']).count();
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
logo: getSoapboxConfig(state).get('logo'),
|
||||
notificationCount: state.getIn(['notifications', 'unread']),
|
||||
chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||
dashboardCount: reportsCount + approvalCount,
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,25 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import StatusList from '../../../components/status_list';
|
||||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { makeGetStatusIds } from 'soapbox/selectors';
|
||||
import { debounce } from 'lodash';
|
||||
import { dequeueTimeline } from 'soapbox/actions/timelines';
|
||||
import { scrollTopTimeline } from '../../../actions/timelines';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { shouldFilter } from 'soapbox/utils/timelines';
|
||||
|
||||
const makeGetStatusIds = () => createSelector([
|
||||
(state, { type }) => getSettings(state).get(type, ImmutableMap()),
|
||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableOrderedSet()),
|
||||
(state) => state.get('statuses'),
|
||||
(state) => state.get('me'),
|
||||
], (columnSettings, statusIds, statuses, me) => {
|
||||
return statusIds.filter(id => {
|
||||
const status = statuses.get(id);
|
||||
if (!status) return true;
|
||||
return !shouldFilter(status, columnSettings);
|
||||
});
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatusIds = makeGetStatusIds();
|
||||
|
|
|
@ -41,6 +41,7 @@ import Icon from 'soapbox/components/icon';
|
|||
import { isStaff, isAdmin } from 'soapbox/utils/accounts';
|
||||
import ProfileHoverCard from 'soapbox/components/profile_hover_card';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import {
|
||||
Status,
|
||||
|
@ -97,6 +98,7 @@ import {
|
|||
ScheduledStatuses,
|
||||
UserIndex,
|
||||
FederationRestrictions,
|
||||
Aliases,
|
||||
} from './util/async-components';
|
||||
|
||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
|
@ -113,6 +115,7 @@ const messages = defineMessages({
|
|||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
|
@ -120,6 +123,7 @@ const mapStateToProps = state => {
|
|||
streamingUrl: state.getIn(['instance', 'urls', 'streaming_api']),
|
||||
me,
|
||||
account,
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -258,6 +262,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/settings/preferences' page={DefaultPage} component={Preferences} content={children} />
|
||||
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
|
||||
<WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />
|
||||
<WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />
|
||||
<WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} />
|
||||
<WrappedRoute path='/soapbox/config' page={DefaultPage} component={SoapboxConfig} content={children} />
|
||||
|
||||
|
@ -297,6 +302,7 @@ class UI extends React.PureComponent {
|
|||
me: SoapboxPropTypes.me,
|
||||
streamingUrl: PropTypes.string,
|
||||
account: PropTypes.object,
|
||||
features: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -406,7 +412,7 @@ class UI extends React.PureComponent {
|
|||
});
|
||||
|
||||
componentDidMount() {
|
||||
const { account } = this.props;
|
||||
const { account, features } = this.props;
|
||||
if (!account) return;
|
||||
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
|
@ -427,7 +433,10 @@ class UI extends React.PureComponent {
|
|||
if (account) {
|
||||
this.props.dispatch(expandHomeTimeline());
|
||||
this.props.dispatch(expandNotifications());
|
||||
this.props.dispatch(fetchChats());
|
||||
|
||||
if (features.chats) {
|
||||
this.props.dispatch(fetchChats());
|
||||
}
|
||||
|
||||
if (isStaff(account)) {
|
||||
this.props.dispatch(fetchReports({ state: 'open' }));
|
||||
|
@ -572,7 +581,7 @@ class UI extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { streamingUrl } = this.props;
|
||||
const { streamingUrl, features } = this.props;
|
||||
const { draggingOver, mobile } = this.state;
|
||||
const { intl, children, location, dropdownMenuIsOpen, me } = this.props;
|
||||
|
||||
|
@ -630,7 +639,7 @@ class UI extends React.PureComponent {
|
|||
<ModalContainer />
|
||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||
{me && <SidebarMenu />}
|
||||
{me && !mobile && (
|
||||
{me && features.chats && !mobile && (
|
||||
<BundleContainer fetchComponent={ChatPanes}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
|
|
|
@ -249,3 +249,7 @@ export function UserIndex() {
|
|||
export function FederationRestrictions() {
|
||||
return import(/* webpackChunkName: "features/federation_restrictions" */'../../federation_restrictions');
|
||||
}
|
||||
|
||||
export function Aliases() {
|
||||
return import(/* webpackChunkName: "features/aliases" */'../../aliases');
|
||||
}
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
"account.share": "Udostępnij profil @{name}",
|
||||
"account.show_reblogs": "Pokazuj podbicia od @{name}",
|
||||
"account.subscribe": "Subskrybuj wpisy @{name}",
|
||||
"account.unblock": "Odblokuj @{name}",
|
||||
"account.subscribed": "Subscribed",
|
||||
"account.unblock": "Zasubskrybowano",
|
||||
"account.unblock_domain": "Odblokuj domenę {domain}",
|
||||
"account.unendorse": "Przestań polecać",
|
||||
"account.unfollow": "Przestań śledzić",
|
||||
|
@ -50,8 +51,6 @@
|
|||
"account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}",
|
||||
"account.unsubscribe": "Przestań subskrybować wpisy @{name}",
|
||||
"account_gallery.none": "Brak zawartości multimedialnej do wyświetlenia.",
|
||||
"auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło",
|
||||
"auth.logged_out": "Wylogowano.",
|
||||
"admin.awaiting_approval.approved_message": "Przyjęto {acct}!",
|
||||
"admin.awaiting_approval.empty_message": "Nikt nie oczekuje przyjęcia. Gdy zarejestruje się nowy użytkownik, możesz zatwierdzić go tutaj.",
|
||||
"admin.awaiting_approval.rejected_message": "Odrzucono {acct}!",
|
||||
|
@ -95,7 +94,7 @@
|
|||
"admin.users.actions.promote_to_admin": "Mianuj @{name} administratorem",
|
||||
"admin.users.actions.promote_to_admin_message": "Mianowano @{acct} administratorem",
|
||||
"admin.users.actions.promote_to_moderator": "Mianuj @{name} moderatorem",
|
||||
"admin.users.actions.promote_to_moderatorem_message": "Mianowano @{acct} moderatorem",
|
||||
"admin.users.actions.promote_to_moderator_message": "Mianowano @{acct} moderatorem",
|
||||
"admin.users.actions.unverify_user": "Cofnij weryfikację @{name}",
|
||||
"admin.users.actions.verify_user": "Weryfikuj @{name}",
|
||||
"admin.users.user_deactivated_message": "Zdezaktywowano @{acct}",
|
||||
|
@ -110,13 +109,14 @@
|
|||
"alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
|
||||
"alert.unexpected.return_home": "Wróć na stronę główną",
|
||||
"alert.unexpected.title": "O nie!",
|
||||
"audio.close": "Zamknij dźwięk",
|
||||
"audio.expand": "Rozwiń dźwięk",
|
||||
"audio.hide": "Ukryj dźwięk",
|
||||
"audio.mute": "Wycisz",
|
||||
"audio.pause": "Pauzuj",
|
||||
"audio.play": "Odtwórz",
|
||||
"audio.unmute": "Cofnij wyciszenie",
|
||||
"aliases.account.add": "Utwórz alias",
|
||||
"aliases.account_label": "Stare konto:",
|
||||
"aliases.aliases_list_delete": "Odłącz alias",
|
||||
"aliases.search": "Szukaj swojego starego konta",
|
||||
"aliases.success.add": "Pomyślnie utworzono alias konta",
|
||||
"aliases.success.remove": "Pomyślnie usunięto alias konta",
|
||||
"auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło",
|
||||
"auth.logged_out": "Wylogowano.",
|
||||
"backups.actions.create": "Utwórz kopię zapasową",
|
||||
"backups.empty_message": "Nie znaleziono kopii zapasaowych. {action}",
|
||||
"backups.empty_message.action": "Chcesz utworzyć?",
|
||||
|
@ -143,14 +143,20 @@
|
|||
"column.admin.moderation_log": "Dziennik moderacyjny",
|
||||
"column.admin.reports": "Zgłoszenia",
|
||||
"column.admin.reports.menu.moderation_log": "Dziennik moderacji",
|
||||
"column.aliases": "Aliasy kont",
|
||||
"column.aliases.create_error": "Błąd tworzenia aliasu",
|
||||
"column.aliases.delete": "Usuń",
|
||||
"column.aliases.delete_error": "Błąd usuwania aliasu",
|
||||
"column.aliases.subheading_add_new": "Dodaj nowy alias",
|
||||
"column.aliases.subheading_aliases": "Istniejące aliasy",
|
||||
"column.backups": "Kopie zapasowe",
|
||||
"column.blocks": "Zablokowani użytkownicy",
|
||||
"column.bookmarks": "Załadki",
|
||||
"column.chats": "Rozmowy",
|
||||
"column.community": "Lokalna oś czasu",
|
||||
"column.crypto_donate": "Przekaż kryptowalutę",
|
||||
"column.direct": "Wiadomości bezpośrednie",
|
||||
"column.domain_blocks": "Ukryte domeny",
|
||||
"column.crypto_donate": "Przekaż kryptowalutę",
|
||||
"column.edit_profile": "Edytuj profil",
|
||||
"column.federation_restrictions": "Ograniczenia federacji",
|
||||
"column.filters": "Wyciszone słowa",
|
||||
|
@ -195,6 +201,7 @@
|
|||
"column_header.hide_settings": "Ukryj ustawienia",
|
||||
"column_header.show_settings": "Pokaż ustawienia",
|
||||
"community.column_settings.media_only": "Tylko zawartość multimedialna",
|
||||
"compose.invalid_schedule": "Musisz zaplanować wpis przynajmniej 5 minut wcześniej.",
|
||||
"compose_form.direct_message_warning": "Ten wpis będzie widoczny tylko dla wszystkich wspomnianych użytkowników.",
|
||||
"compose_form.direct_message_warning_learn_more": "Dowiedz się więcej",
|
||||
"compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.",
|
||||
|
@ -250,10 +257,11 @@
|
|||
"confirmations.reply.message": "W ten sposób utracisz wpis który obecnie tworzysz. Czy na pewno chcesz to zrobić?",
|
||||
"confirmations.unfollow.confirm": "Przestań śledzić",
|
||||
"confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?",
|
||||
"crypto_donate_panel.actions.more": "Naciśnij, aby zobaczyć {count} more {count, plural, one {portfel} few {portfele} many {portfeli} other {portfeli}}",
|
||||
"crypto_donate.explanation_box.message": "{siteTitle} przyjmuje darowizny w kryptowalutach. Możesz wysłać darowiznę na jeden z poniższych adresów. Dziękujemy za Wasze wsparcie!",
|
||||
"crypto_donate_panel.intro.message": "{siteTitle} przyjmuje darowizny w kryptowalutach, aby utrzymać naszą usługę. Dziękujemy za Wasze wsparcie!",
|
||||
"crypto_donate.explanation_box.title": "Przeaż darowiznę w kryptowalutach",
|
||||
"crypto_donate_panel.actions.more": "Naciśnij, aby zobaczyć {count} more {count, plural, one {portfel} few {portfele} many {portfeli} other {portfeli}}",
|
||||
"crypto_donate_panel.heading": "Przekaż kryptowalutę",
|
||||
"crypto_donate_panel.intro.message": "{siteTitle} przyjmuje darowizny w kryptowalutach, aby utrzymać naszą usługę. Dziękujemy za Wasze wsparcie!",
|
||||
"datepicker.hint": "Zaplanowano publikację na…",
|
||||
"donate": "Przekaż darowiznę",
|
||||
"donate_crypto": "Przekaż kryptowalutę",
|
||||
|
@ -277,7 +285,7 @@
|
|||
"edit_profile.fields.meta_fields_label": "Metadane profilu",
|
||||
"edit_profile.fields.stranger_notifications_label": "Blokuj powiadomienia od nieznajomych",
|
||||
"edit_profile.fields.verified_display_name": "Zweryfikowani użytkownicy nie mogą zmieniać nazwy wyświetlanej",
|
||||
"edit_profile.hints.accepts_email_list_label": "Otrzymuj wiadomości i nowości marketingowe",
|
||||
"edit_profile.hints.accepts_email_list": "Otrzymuj wiadomości i nowości marketingowe.",
|
||||
"edit_profile.hints.avatar": "PNG, GIF lub JPG. Maksymalnie 2 MB. Zostanie zmniejszony do 400x400px",
|
||||
"edit_profile.hints.bot": "To konto podejmuje głównie zautomatyzowane działania i może nie być nadzorowane",
|
||||
"edit_profile.hints.header": "PNG, GIF lub JPG. Maksymalnie 2 MB. Zostanie zmniejszony do 1500x500px",
|
||||
|
@ -306,6 +314,8 @@
|
|||
"emoji_button.travel": "Podróże i miejsca",
|
||||
"empty_column.account_timeline": "Brak wpisów tutaj!",
|
||||
"empty_column.account_unavailable": "Profil niedostępny",
|
||||
"empty_column.aliases": "Nie utworzyłeś(-aś) jeszcze żadnego aliasu konta.",
|
||||
"empty_column.aliases.suggestions": "Brak propozycji kont dla podanej frazy.",
|
||||
"empty_column.blocks": "Nie zablokowałeś(-aś) jeszcze żadnego użytkownika.",
|
||||
"empty_column.bookmarks": "Nie masz jeszcze żadnej zakładki. Kiedy dodasz jakąś, pojawi się ona tutaj.",
|
||||
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
|
||||
|
@ -334,11 +344,14 @@
|
|||
"federation_restriction.full_media_removal": "Pełne usunięcie mediów",
|
||||
"federation_restriction.media_nsfw": "Załączniki oznaczone jako NSFW",
|
||||
"federation_restriction.partial_media_removal": "Częściowe usunięcie mediów",
|
||||
"federation_restrictions.empty_message": "{siteTitle} nie nakłada restrykcji na żadne instancje.",
|
||||
"federation_restrictions.explanation_box.message": "Zwykle serwery w Fediwersum mogą swobodnie porozumiewać się. {siteTitle} nakłada pewne ograniczenia na następujące serwery.",
|
||||
"federation_restrictions.explanation_box.title": "Zasady dla poszczególnych instancji",
|
||||
"federation_restrictions.not_disclosed_message": "{siteTitle} nie udostępnia informacji o ograniczeniach federacji przez API.",
|
||||
"fediverse_tab.explanation_box.dismiss": "Nie pokazuj ponownie",
|
||||
"fediverse_tab.explanation_box.explanation": "{site_title} jest częścią Fediwersum, sieci społecznościowej na którą składają się tysiące niezależnie funkcjonujących stron (serwerów). Wpisy które tu widzisz pochodzą z serwerów podmiotów trzecich. Możesz do woli wchodzić z nimi w interakcje lub blokować serwery których nie lubisz. Zwracaj uwagę na pełną nazwę użytkownika po znaku @, aby wiedzieć z jakiego serwera pochodzi on. Aby widzieć tylko wpisy z {site_title}, odwiedź kartę {local}.",
|
||||
"fediverse_tab.explanation_box.title": "Czym jest Fediverse?",
|
||||
"filters.added": "Dodano filtr.",
|
||||
"filters.context_header": "Konteksty filtru",
|
||||
"filters.context_hint": "Jedno lub więcej miejsc, gdzie filtr powinien zostać zaaplikowany",
|
||||
"filters.filters_list_context_label": "Konteksty filtru:",
|
||||
|
@ -348,12 +361,12 @@
|
|||
"filters.filters_list_hide": "Ukrywaj",
|
||||
"filters.filters_list_phrase_label": "Słowo kluczowe lub fraza:",
|
||||
"filters.filters_list_whole-word": "Całe słowo",
|
||||
"filters.added": "Dodano filtr.",
|
||||
"filters.deleted": "Usunięto filtr.",
|
||||
"filters.removed": "Usunięto filtr.",
|
||||
"follow_request.authorize": "Autoryzuj",
|
||||
"follow_request.reject": "Odrzuć",
|
||||
"forms.copy": "Kopiuj",
|
||||
"getting_started.open_source_notice": "{code_name} jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitLabie tutaj: {code_link} (v{code_version}).",
|
||||
"group.detail.archived_group": "Zarchiwizowana grupa",
|
||||
"group.members.empty": "Ta grupa nie ma żadnych członków.",
|
||||
"group.removed_accounts.empty": "Ta grupa nie ma żadnych usuniętych kont.",
|
||||
"groups.card.join": "Dołącz",
|
||||
|
@ -362,13 +375,21 @@
|
|||
"groups.card.roles.member": "Jesteś członkiem",
|
||||
"groups.card.view": "Zobacz",
|
||||
"groups.create": "Utwórz grupę",
|
||||
"groups.detail.role_admin": "Jesteś administratorem",
|
||||
"groups.edit": "Edytuj",
|
||||
"groups.form.coverImage": "Wyślij obraz baneru (nieobowiązkowe)",
|
||||
"groups.form.coverImageChange": "Wybrano obraz baneru",
|
||||
"groups.form.create": "Utwórz grupę",
|
||||
"groups.form.description": "Opis",
|
||||
"groups.form.title": "Tytuł",
|
||||
"groups.form.update": "Aktualizuj grupę",
|
||||
"groups.join": "Dołącz do grupy",
|
||||
"groups.leave": "Opuść grupę",
|
||||
"groups.removed_accounts": "Usunięte konta",
|
||||
"groups.sidebar-panel.item.no_recent_activity": "Brak ostatniej aktywności",
|
||||
"groups.sidebar-panel.item.view": "nowe wpisy",
|
||||
"groups.sidebar-panel.show_all": "Pokaż wszystkie",
|
||||
"groups.sidebar-panel.title": "Grupy do których należysz",
|
||||
"groups.tab_admin": "Zarządzaj",
|
||||
"groups.tab_featured": "Wyróżnione",
|
||||
"groups.tab_member": "Członek",
|
||||
|
@ -407,9 +428,7 @@
|
|||
"keyboard_shortcuts.back": "aby cofnąć się",
|
||||
"keyboard_shortcuts.blocked": "aby przejść do listy zablokowanych użytkowników",
|
||||
"keyboard_shortcuts.boost": "aby podbić wpis",
|
||||
"keyboard_shortcuts.column": "aby przejść do wpisu z jednej z kolumn",
|
||||
"keyboard_shortcuts.compose": "aby przejść do pola tworzenia wpisu",
|
||||
"keyboard_shortcuts.direct": "aby otworzyć kolumnę wiadomości bezpośrednich",
|
||||
"keyboard_shortcuts.down": "aby przejść na dół listy",
|
||||
"keyboard_shortcuts.enter": "aby otworzyć wpis",
|
||||
"keyboard_shortcuts.favourite": "aby dodać do ulubionych",
|
||||
|
@ -428,7 +447,6 @@
|
|||
"keyboard_shortcuts.reply": "aby odpowiedzieć",
|
||||
"keyboard_shortcuts.requests": "aby przejść do listy próśb o możliwość śledzenia",
|
||||
"keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",
|
||||
"keyboard_shortcuts.start": "aby otworzyć kolumnę „Rozpocznij”",
|
||||
"keyboard_shortcuts.toggle_hidden": "aby wyświetlić lub ukryć wpis spod CW",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "by pokazać/ukryć multimedia",
|
||||
"keyboard_shortcuts.toot": "aby utworzyć nowy wpis",
|
||||
|
@ -484,8 +502,10 @@
|
|||
"morefollows.followers_label": "…i {count} więcej {count, plural, one {obserwujący(-a)} few {obserwujących} many {obserwujących} other {obserwujących}} na zdalnych stronach.",
|
||||
"morefollows.following_label": "…i {count} więcej {count, plural, one {obserwowany(-a)} few {obserwowanych} many {obserwowanych} other {obserwowanych}} na zdalnych stronach.",
|
||||
"mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
|
||||
"navigation_bar.account_aliases": "Aliasy kont",
|
||||
"navigation_bar.admin_settings": "Ustawienia administracyjne",
|
||||
"navigation_bar.blocks": "Zablokowani użytkownicy",
|
||||
"navigation_bar.bookmarks": "Zakładki",
|
||||
"navigation_bar.compose": "Utwórz nowy wpis",
|
||||
"navigation_bar.domain_blocks": "Ukryte domeny",
|
||||
"navigation_bar.favourites": "Ulubione",
|
||||
|
@ -494,7 +514,9 @@
|
|||
"navigation_bar.import_data": "Importuj dane",
|
||||
"navigation_bar.info": "Szczegółowe informacje",
|
||||
"navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
|
||||
"navigation_bar.lists": "Lists",
|
||||
"navigation_bar.logout": "Wyloguj",
|
||||
"navigation_bar.messages": "Messages",
|
||||
"navigation_bar.mutes": "Wyciszeni użytkownicy",
|
||||
"navigation_bar.pins": "Przypięte wpisy",
|
||||
"navigation_bar.preferences": "Preferencje",
|
||||
|
@ -613,6 +635,7 @@
|
|||
"relative_time.minutes": "{number} min.",
|
||||
"relative_time.seconds": "{number} s.",
|
||||
"remote_instance.edit_federation": "Edytuj federację",
|
||||
"remote_instance.federation_panel.heading": "Federation Restrictions",
|
||||
"remote_instance.federation_panel.no_restrictions_message": "{siteTitle} nie nakłada ograniczeń na {host}.",
|
||||
"remote_instance.federation_panel.restricted_message": "{siteTitle} blokuje wszystkie aktywności z {host}.",
|
||||
"remote_instance.federation_panel.some_restrictions_message": "{siteTitle} nakłada pewne ograniczenia na {host}.",
|
||||
|
@ -641,8 +664,6 @@
|
|||
"search_results.hashtags": "Hashtagi",
|
||||
"search_results.statuses": "Wpisy",
|
||||
"search_results.top": "Góra",
|
||||
"search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}",
|
||||
"search_results.total.has_more": "{count, number} Ponad {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}",
|
||||
"security.codes.fail": "Nie udało się uzyskać zapasowych kodów",
|
||||
"security.confirm.fail": "Nieprawidłowy kod lub hasło. Spróbuj ponownie.",
|
||||
"security.delete_account.fail": "Nie udało się usunąć konta.",
|
||||
|
@ -788,7 +809,6 @@
|
|||
"upload_error.limit": "Przekroczono limit plików do wysłania.",
|
||||
"upload_error.poll": "Dołączanie plików nie dozwolone z głosowaniami.",
|
||||
"upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
|
||||
"upload_form.focus": "Zmień podgląd",
|
||||
"upload_form.preview": "Podgląd",
|
||||
"upload_form.undo": "Usuń",
|
||||
"upload_progress.label": "Wysyłanie…",
|
||||
|
|
|
@ -8,6 +8,7 @@ import FeaturesPanel from 'soapbox/features/ui/components/features_panel';
|
|||
import LinkFooter from 'soapbox/features/ui/components/link_footer';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import InstanceInfoPanel from 'soapbox/features/ui/components/instance_info_panel';
|
||||
import InstanceModerationPanel from 'soapbox/features/ui/components/instance_moderation_panel';
|
||||
import { federationRestrictionsDisclosed } from 'soapbox/utils/state';
|
||||
import { isAdmin } from 'soapbox/utils/accounts';
|
||||
|
||||
|
@ -38,7 +39,8 @@ class RemoteInstancePage extends ImmutablePureComponent {
|
|||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
{(disclosed || isAdmin) && <InstanceInfoPanel host={host} />}
|
||||
<InstanceInfoPanel host={host} />
|
||||
{(disclosed || isAdmin) && <InstanceModerationPanel host={host} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -65,17 +65,20 @@ describe('auth reducer', () => {
|
|||
|
||||
describe('AUTH_LOGGED_OUT', () => {
|
||||
it('deletes the user', () => {
|
||||
const action = { type: AUTH_LOGGED_OUT, accountId: '1234' };
|
||||
const action = {
|
||||
type: AUTH_LOGGED_OUT,
|
||||
account: fromJS({ url: 'https://gleasonator.com/users/alex' }),
|
||||
};
|
||||
|
||||
const state = fromJS({
|
||||
users: {
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
},
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
});
|
||||
|
||||
const result = reducer(state, action);
|
||||
|
@ -84,16 +87,20 @@ describe('auth reducer', () => {
|
|||
|
||||
it('sets `me` to the next available user', () => {
|
||||
const state = fromJS({
|
||||
me: '1234',
|
||||
me: 'https://gleasonator.com/users/alex',
|
||||
users: {
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
},
|
||||
});
|
||||
|
||||
const action = { type: AUTH_LOGGED_OUT, accountId: '1234' };
|
||||
const action = {
|
||||
type: AUTH_LOGGED_OUT,
|
||||
account: fromJS({ url: 'https://gleasonator.com/users/alex' }),
|
||||
};
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('me')).toEqual('5678');
|
||||
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -102,11 +109,11 @@ describe('auth reducer', () => {
|
|||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||
};
|
||||
|
||||
const expected = fromJS({
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||
});
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
|
@ -117,7 +124,7 @@ describe('auth reducer', () => {
|
|||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||
};
|
||||
|
||||
const state = fromJS({
|
||||
|
@ -125,7 +132,12 @@ describe('auth reducer', () => {
|
|||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'ABCDEFG': { token_type: 'Bearer', access_token: 'ABCDEFG', account: '1234' },
|
||||
'ABCDEFG': {
|
||||
token_type: 'Bearer',
|
||||
access_token: 'ABCDEFG',
|
||||
account: '1234',
|
||||
me: 'https://gleasonator.com/users/alex',
|
||||
},
|
||||
});
|
||||
|
||||
const result = reducer(state, action);
|
||||
|
@ -136,49 +148,82 @@ describe('auth reducer', () => {
|
|||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('me')).toEqual('1234');
|
||||
expect(result.get('me')).toEqual('https://gleasonator.com/users/alex');
|
||||
});
|
||||
|
||||
it('leaves `me` alone if already set', () => {
|
||||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||
};
|
||||
|
||||
const state = fromJS({ me: '5678' });
|
||||
const state = fromJS({ me: 'https://gleasonator.com/users/benis' });
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('me')).toEqual('5678');
|
||||
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
|
||||
});
|
||||
|
||||
it('deletes mismatched users', () => {
|
||||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234' },
|
||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||
};
|
||||
|
||||
const state = fromJS({
|
||||
users: {
|
||||
'4567': { id: '4567', access_token: 'ABCDEFG' },
|
||||
'8901': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5432': { id: '5432', access_token: 'HIJKLMN' },
|
||||
'https://gleasonator.com/users/mk': { id: '4567', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/mk' },
|
||||
'https://gleasonator.com/users/curtis': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/curtis' },
|
||||
'https://gleasonator.com/users/benis': { id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
},
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5432': { id: '5432', access_token: 'HIJKLMN' },
|
||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||
'https://gleasonator.com/users/benis': { id: '5432', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
});
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('users')).toEqual(expected);
|
||||
});
|
||||
|
||||
it('upgrades from an ID to a URL', () => {
|
||||
const action = {
|
||||
type: VERIFY_CREDENTIALS_SUCCESS,
|
||||
token: 'ABCDEFG',
|
||||
account: { id: '1234', url: 'https://gleasonator.com/users/alex' },
|
||||
};
|
||||
|
||||
const state = fromJS({
|
||||
me: '1234',
|
||||
users: {
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5432': { id: '5432', access_token: 'HIJKLMN' },
|
||||
},
|
||||
tokens: {
|
||||
'ABCDEFG': { access_token: 'ABCDEFG', account: '1234' },
|
||||
},
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
me: 'https://gleasonator.com/users/alex',
|
||||
users: {
|
||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||
'5432': { id: '5432', access_token: 'HIJKLMN' },
|
||||
},
|
||||
tokens: {
|
||||
'ABCDEFG': { access_token: 'ABCDEFG', account: '1234', me: 'https://gleasonator.com/users/alex' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VERIFY_CREDENTIALS_FAIL', () => {
|
||||
|
@ -207,13 +252,13 @@ describe('auth reducer', () => {
|
|||
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' },
|
||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
},
|
||||
});
|
||||
|
||||
const expected = fromJS({
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
});
|
||||
|
||||
const action = {
|
||||
|
@ -228,10 +273,10 @@ describe('auth reducer', () => {
|
|||
|
||||
it('should reassign `me` to the next in line', () => {
|
||||
const state = fromJS({
|
||||
me: '1234',
|
||||
me: 'https://gleasonator.com/users/alex',
|
||||
users: {
|
||||
'1234': { id: '1234', access_token: 'ABCDEFG' },
|
||||
'5678': { id: '5678', access_token: 'HIJKLMN' },
|
||||
'https://gleasonator.com/users/alex': { id: '1234', access_token: 'ABCDEFG', url: 'https://gleasonator.com/users/alex' },
|
||||
'https://gleasonator.com/users/benis': { id: '5678', access_token: 'HIJKLMN', url: 'https://gleasonator.com/users/benis' },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -242,21 +287,25 @@ describe('auth reducer', () => {
|
|||
};
|
||||
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('me')).toEqual('5678');
|
||||
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SWITCH_ACCOUNT', () => {
|
||||
it('sets the value of `me`', () => {
|
||||
const action = { type: SWITCH_ACCOUNT, accountId: '5678' };
|
||||
const action = {
|
||||
type: SWITCH_ACCOUNT,
|
||||
account: fromJS({ url: 'https://gleasonator.com/users/benis' }),
|
||||
};
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('me')).toEqual('5678');
|
||||
expect(result.get('me')).toEqual('https://gleasonator.com/users/benis');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ME_FETCH_SKIP', () => {
|
||||
it('sets `me` to null', () => {
|
||||
const state = fromJS({ me: '1234' });
|
||||
const state = fromJS({ me: 'https://gleasonator.com/users/alex' });
|
||||
const action = { type: ME_FETCH_SKIP };
|
||||
const result = reducer(state, action);
|
||||
expect(result.get('me')).toEqual(null);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import {
|
||||
ALIASES_SUGGESTIONS_READY,
|
||||
ALIASES_SUGGESTIONS_CLEAR,
|
||||
ALIASES_SUGGESTIONS_CHANGE,
|
||||
} from '../actions/aliases';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
suggestions: ImmutableMap({
|
||||
value: '',
|
||||
loaded: false,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default function aliasesReducer(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ALIASES_SUGGESTIONS_CHANGE:
|
||||
return state
|
||||
.setIn(['suggestions', 'value'], action.value)
|
||||
.setIn(['suggestions', 'loaded'], false);
|
||||
case ALIASES_SUGGESTIONS_READY:
|
||||
return state
|
||||
.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)))
|
||||
.setIn(['suggestions', 'loaded'], true);
|
||||
case ALIASES_SUGGESTIONS_CLEAR:
|
||||
return state.update('suggestions', suggestions => suggestions.withMutations(map => {
|
||||
map.set('items', ImmutableList());
|
||||
map.set('value', '');
|
||||
map.set('loaded', false);
|
||||
}));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '../actions/auth';
|
||||
import { ME_FETCH_SKIP } from '../actions/me';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { validId, isURL } from 'soapbox/utils/auth';
|
||||
|
||||
const defaultState = ImmutableMap({
|
||||
app: ImmutableMap(),
|
||||
|
@ -17,8 +18,6 @@ const defaultState = ImmutableMap({
|
|||
me: null,
|
||||
});
|
||||
|
||||
const validId = id => typeof id === 'string' && id !== 'null' && id !== 'undefined';
|
||||
|
||||
const getSessionUser = () => {
|
||||
const id = sessionStorage.getItem('soapbox:auth:me');
|
||||
return validId(id) ? id : undefined;
|
||||
|
@ -39,14 +38,24 @@ const validUser = user => {
|
|||
// Finds the first valid user in the state
|
||||
const firstValidUser = state => state.get('users', ImmutableMap()).find(validUser);
|
||||
|
||||
// For legacy purposes. IDs get upgraded to URLs further down.
|
||||
const getUrlOrId = user => {
|
||||
try {
|
||||
const { id, url } = user.toJS();
|
||||
return url || id;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 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');
|
||||
const user = state.getIn(['users', me]);
|
||||
|
||||
if (!validUser(users.get(me))) {
|
||||
if (!validUser(user)) {
|
||||
const nextUser = firstValidUser(state);
|
||||
return state.set('me', nextUser ? nextUser.get('id') : null);
|
||||
return state.set('me', getUrlOrId(nextUser));
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
|
@ -59,7 +68,7 @@ const setSessionUser = state => state.update('me', null, me => {
|
|||
state.getIn(['users', me]),
|
||||
]).find(validUser);
|
||||
|
||||
return user ? user.get('id') : null;
|
||||
return getUrlOrId(user);
|
||||
});
|
||||
|
||||
// Upgrade the initial state
|
||||
|
@ -83,13 +92,22 @@ const migrateLegacy = state => {
|
|||
});
|
||||
};
|
||||
|
||||
const isUpgradingUrlId = state => {
|
||||
const me = state.get('me');
|
||||
const user = state.getIn(['users', me]);
|
||||
return validId(me) && user && !isURL(me);
|
||||
};
|
||||
|
||||
// Checks the state and makes it valid
|
||||
const sanitizeState = state => {
|
||||
// Skip sanitation during ID to URL upgrade
|
||||
if (isUpgradingUrlId(state)) return state;
|
||||
|
||||
return state.withMutations(state => {
|
||||
// Remove invalid users, ensure ID match
|
||||
state.update('users', ImmutableMap(), users => (
|
||||
users.filter((user, id) => (
|
||||
validUser(user) && user.get('id') === id
|
||||
users.filter((user, url) => (
|
||||
validUser(user) && user.get('url') === url
|
||||
))
|
||||
));
|
||||
// Remove mismatched tokens
|
||||
|
@ -135,33 +153,49 @@ const importToken = (state, token) => {
|
|||
const upgradeLegacyId = (state, account) => {
|
||||
if (localState) return state;
|
||||
return state.withMutations(state => {
|
||||
state.update('me', null, me => me === '_legacy' ? account.id : me);
|
||||
state.update('me', null, me => me === '_legacy' ? account.url : 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.
|
||||
};
|
||||
|
||||
// Users are now stored by their ActivityPub ID instead of their
|
||||
// primary key to support auth against multiple hosts.
|
||||
const upgradeNonUrlId = (state, account) => {
|
||||
const me = state.get('me');
|
||||
if (isURL(me)) return state;
|
||||
|
||||
return state.withMutations(state => {
|
||||
state.update('me', null, me => me === account.id ? account.url : me);
|
||||
state.deleteIn(['users', account.id]);
|
||||
});
|
||||
};
|
||||
|
||||
// Returns a predicate function for filtering a mismatched user/token
|
||||
const userMismatch = (token, account) => {
|
||||
return (user, id) => {
|
||||
return (user, url) => {
|
||||
const sameToken = user.get('access_token') === token;
|
||||
const differentId = id !== account.id || user.get('id') !== account.id;
|
||||
const differentUrl = url !== account.url || user.get('url') !== account.url;
|
||||
const differentId = user.get('id') !== account.id;
|
||||
|
||||
return sameToken && differentId;
|
||||
return sameToken && (differentUrl || differentId);
|
||||
};
|
||||
};
|
||||
|
||||
const importCredentials = (state, token, account) => {
|
||||
return state.withMutations(state => {
|
||||
state.setIn(['users', account.id], ImmutableMap({
|
||||
state.setIn(['users', account.url], ImmutableMap({
|
||||
id: account.id,
|
||||
access_token: token,
|
||||
url: account.url,
|
||||
}));
|
||||
state.setIn(['tokens', token, 'account'], account.id);
|
||||
state.setIn(['tokens', token, 'me'], account.url);
|
||||
state.update('users', ImmutableMap(), users => users.filterNot(userMismatch(token, account)));
|
||||
state.update('me', null, me => me || account.id);
|
||||
state.update('me', null, me => me || account.url);
|
||||
upgradeLegacyId(state, account);
|
||||
upgradeNonUrlId(state, account);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -173,10 +207,12 @@ const deleteToken = (state, token) => {
|
|||
});
|
||||
};
|
||||
|
||||
const deleteUser = (state, accountId) => {
|
||||
const deleteUser = (state, account) => {
|
||||
const accountUrl = account.get('url');
|
||||
|
||||
return state.withMutations(state => {
|
||||
state.update('users', ImmutableMap(), users => users.delete(accountId));
|
||||
state.update('tokens', ImmutableMap(), tokens => tokens.filterNot(token => token.get('account') === accountId));
|
||||
state.update('users', ImmutableMap(), users => users.delete(accountUrl));
|
||||
state.update('tokens', ImmutableMap(), tokens => tokens.filterNot(token => token.get('me') === accountUrl));
|
||||
maybeShiftMe(state);
|
||||
});
|
||||
};
|
||||
|
@ -186,17 +222,17 @@ const reducer = (state, action) => {
|
|||
case AUTH_APP_CREATED:
|
||||
return state.set('app', fromJS(action.app));
|
||||
case AUTH_APP_AUTHORIZED:
|
||||
return state.update('app', ImmutableMap(), app => app.merge(fromJS(action.app)));
|
||||
return state.update('app', ImmutableMap(), app => app.merge(fromJS(action.token)));
|
||||
case AUTH_LOGGED_IN:
|
||||
return importToken(state, action.token);
|
||||
case AUTH_LOGGED_OUT:
|
||||
return deleteUser(state, action.accountId);
|
||||
return deleteUser(state, action.account);
|
||||
case VERIFY_CREDENTIALS_SUCCESS:
|
||||
return importCredentials(state, action.token, action.account);
|
||||
case VERIFY_CREDENTIALS_FAIL:
|
||||
return action.error.response.status === 403 ? deleteToken(state, action.token) : state;
|
||||
case SWITCH_ACCOUNT:
|
||||
return state.set('me', action.accountId);
|
||||
return state.set('me', action.account.get('url'));
|
||||
case ME_FETCH_SKIP:
|
||||
return state.set('me', null);
|
||||
default:
|
||||
|
@ -214,10 +250,14 @@ const validMe = state => {
|
|||
|
||||
// `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');
|
||||
const me = state.get('me');
|
||||
const oldMe = oldState.get('me');
|
||||
|
||||
return stillValid && didChange;
|
||||
const stillValid = validMe(oldState) && validMe(state);
|
||||
const didChange = oldMe !== me;
|
||||
const userUpgradedUrl = state.getIn(['users', me, 'id']) === oldMe;
|
||||
|
||||
return stillValid && didChange && !userUpgradedUrl;
|
||||
};
|
||||
|
||||
const maybeReload = (oldState, state, action) => {
|
||||
|
|
|
@ -53,6 +53,7 @@ import backups from './backups';
|
|||
import admin_log from './admin_log';
|
||||
import security from './security';
|
||||
import scheduled_statuses from './scheduled_statuses';
|
||||
import aliases from './aliases';
|
||||
|
||||
const appReducer = combineReducers({
|
||||
dropdown_menu,
|
||||
|
@ -107,6 +108,7 @@ const appReducer = combineReducers({
|
|||
admin_log,
|
||||
security,
|
||||
scheduled_statuses,
|
||||
aliases,
|
||||
});
|
||||
|
||||
// Clear the state (mostly) when the user logs out
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
FETCH_TOKENS_SUCCESS,
|
||||
REVOKE_TOKEN_SUCCESS,
|
||||
} from '../actions/auth';
|
||||
} from '../actions/security';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
|
|
|
@ -6,6 +6,9 @@ import {
|
|||
} from 'immutable';
|
||||
import { getDomain } from 'soapbox/utils/accounts';
|
||||
import ConfigDB from 'soapbox/utils/config_db';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { shouldFilter } from 'soapbox/utils/timelines';
|
||||
import { validId } from 'soapbox/utils/auth';
|
||||
|
||||
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
||||
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
||||
|
@ -205,15 +208,27 @@ export const makeGetReport = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const getAuthUserIds = createSelector([
|
||||
state => state.getIn(['auth', 'users'], ImmutableMap()),
|
||||
], authUsers => {
|
||||
return authUsers.reduce((ids, authUser) => {
|
||||
try {
|
||||
const id = authUser.get('id');
|
||||
return validId(id) ? ids.add(id) : ids;
|
||||
} catch {
|
||||
return ids;
|
||||
}
|
||||
}, ImmutableOrderedSet());
|
||||
});
|
||||
|
||||
export const makeGetOtherAccounts = () => {
|
||||
return createSelector([
|
||||
state => state.get('accounts'),
|
||||
state => state.getIn(['auth', 'users']),
|
||||
getAuthUserIds,
|
||||
state => state.get('me'),
|
||||
],
|
||||
(accounts, authUsers, me) => {
|
||||
return authUsers
|
||||
.keySeq()
|
||||
(accounts, authUserIds, me) => {
|
||||
return authUserIds
|
||||
.reduce((list, id) => {
|
||||
if (id === me) return list;
|
||||
const account = accounts.get(id);
|
||||
|
@ -262,3 +277,16 @@ export const makeGetRemoteInstance = () => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const makeGetStatusIds = () => createSelector([
|
||||
(state, { type, prefix }) => getSettings(state).get(prefix || type, ImmutableMap()),
|
||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableOrderedSet()),
|
||||
(state) => state.get('statuses'),
|
||||
(state) => state.get('me'),
|
||||
], (columnSettings, statusIds, statuses, me) => {
|
||||
return statusIds.filter(id => {
|
||||
const status = statuses.get(id);
|
||||
if (!status) return true;
|
||||
return !shouldFilter(status, columnSettings);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,59 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
export const validId = id => typeof id === 'string' && id !== 'null' && id !== 'undefined';
|
||||
|
||||
export const isURL = url => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseBaseURL = url => {
|
||||
try {
|
||||
return new URL(url).origin;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getLoggedInAccount = state => {
|
||||
const me = state.get('me');
|
||||
return state.getIn(['accounts', me]);
|
||||
};
|
||||
|
||||
export const isLoggedIn = getState => {
|
||||
return typeof getState().get('me') === 'string';
|
||||
return validId(getState().get('me'));
|
||||
};
|
||||
|
||||
export const getAppToken = state => state.getIn(['auth', 'app', 'access_token']);
|
||||
|
||||
export const getUserToken = (state, accountId) => {
|
||||
return state.getIn(['auth', 'users', accountId, 'access_token']);
|
||||
const accountUrl = state.getIn(['accounts', accountId, 'url']);
|
||||
return state.getIn(['auth', 'users', accountUrl, 'access_token']);
|
||||
};
|
||||
|
||||
export const getAccessToken = state => {
|
||||
const me = state.get('me');
|
||||
return getUserToken(state, me);
|
||||
};
|
||||
|
||||
export const getAuthUserId = state => {
|
||||
const me = state.getIn(['auth', 'me']);
|
||||
|
||||
return ImmutableList([
|
||||
state.getIn(['auth', 'users', me, 'id']),
|
||||
me,
|
||||
]).find(validId);
|
||||
};
|
||||
|
||||
export const getAuthUserUrl = state => {
|
||||
const me = state.getIn(['auth', 'me']);
|
||||
|
||||
return ImmutableList([
|
||||
state.getIn(['auth', 'users', me, 'url']),
|
||||
me,
|
||||
]).find(isURL);
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ const version = pkg => {
|
|||
|
||||
module.exports = {
|
||||
name: pkg.name,
|
||||
displayName: pkg.displayName,
|
||||
url: pkg.repository.url,
|
||||
repository: shortRepoName(pkg.repository.url),
|
||||
version: version(pkg),
|
||||
|
|
|
@ -16,6 +16,7 @@ export const getFeatures = createSelector([
|
|||
focalPoint: v.software === 'Mastodon' && gte(v.compatVersion, '2.3.0'),
|
||||
importMutes: v.software === 'Pleroma' && gte(v.version, '2.2.0'),
|
||||
emailList: f.includes('email_list'),
|
||||
chats: v.software === 'Pleroma' && gte(v.version, '2.1.0'),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// Fonts from elsewhere
|
||||
@import '~fork-awesome/css/fork-awesome.css';
|
||||
@import '~fontsource-roboto/300.css';
|
||||
@import '~fontsource-roboto/400.css';
|
||||
@import '~fontsource-roboto/700.css';
|
||||
@import '~fontsource-montserrat/800.css';
|
||||
@import '~@fontsource/roboto/300.css';
|
||||
@import '~@fontsource/roboto/400.css';
|
||||
@import '~@fontsource/roboto/700.css';
|
||||
@import '~@fontsource/montserrat/800.css';
|
||||
|
||||
@import 'mixins';
|
||||
@import 'themes';
|
||||
|
@ -84,6 +84,7 @@
|
|||
@import 'components/datepicker';
|
||||
@import 'components/remote-timeline';
|
||||
@import 'components/federation-restrictions';
|
||||
@import 'components/aliases';
|
||||
|
||||
// Holiday
|
||||
@import 'holiday/halloween';
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
.aliases {
|
||||
&__accounts {
|
||||
overflow-y: auto;
|
||||
|
||||
.account__display-name {
|
||||
&:hover strong {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.empty-column-indicator {
|
||||
min-height: unset;
|
||||
overflow-y: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&_search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 10px;
|
||||
|
||||
.search__input {
|
||||
padding: 7px 30px 6px 10px;
|
||||
}
|
||||
|
||||
> label {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
> .search__icon .fa {
|
||||
top: 8px;
|
||||
right: 102px !important;
|
||||
}
|
||||
|
||||
> .button {
|
||||
width: 80px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aliases-settings-panel {
|
||||
flex: 1;
|
||||
|
||||
.item-list article {
|
||||
border-bottom: 1px solid var(--primary-text-color--faint);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.alias__container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
|
||||
span.alias__list-label {
|
||||
padding-right: 5px;
|
||||
color: var(--primary-text-color--faint);
|
||||
}
|
||||
|
||||
span.alias__list-value span {
|
||||
padding-right: 5px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&::after {
|
||||
content: ',';
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
&::after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alias__delete {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
cursor: pointer;
|
||||
|
||||
span.alias__delete-label {
|
||||
color: var(--primary-text-color--faint);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.alias__delete-icon {
|
||||
background: none;
|
||||
color: var(--primary-text-color--faint);
|
||||
padding: 0 5px;
|
||||
margin: 0 auto;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slist--flex {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -297,6 +297,14 @@
|
|||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
> i.fa {
|
||||
width: 100%;
|
||||
color: hsla(var(--primary-text-color_hsl), 0.4);
|
||||
font-size: 64px;
|
||||
text-align: center;
|
||||
line-height: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,3 +27,44 @@
|
|||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pinned-hosts-picker {
|
||||
margin-left: 10px;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.pinned-host {
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pinned-host {
|
||||
background: var(--background-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--brand-color--faint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pinned-host {
|
||||
background: var(--background-color);
|
||||
border-radius: 999px;
|
||||
transition: 0.2s;
|
||||
|
||||
&.active {
|
||||
background: var(--brand-color--faint);
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: var(--primary-text-color);
|
||||
text-decoration: none;
|
||||
padding: 5px 11px;
|
||||
max-width: 115px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "soapbox-fe",
|
||||
"displayName": "Soapbox FE",
|
||||
"version": "1.3.0",
|
||||
"description": "Soapbox frontend for Pleroma.",
|
||||
"homepage": "https://soapbox.pub/",
|
||||
|
@ -45,6 +46,8 @@
|
|||
"@babel/preset-env": "^7.14.7",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/runtime": "^7.14.6",
|
||||
"@fontsource/montserrat": "^4.5.1",
|
||||
"@fontsource/roboto": "^4.5.0",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@welldone-software/why-did-you-render": "^6.2.0",
|
||||
"array-includes": "^3.0.3",
|
||||
|
@ -74,8 +77,6 @@
|
|||
"escape-html": "^1.0.3",
|
||||
"exif-js": "^2.3.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"fontsource-montserrat": "^3.0.9",
|
||||
"fontsource-roboto": "^3.0.3",
|
||||
"fork-awesome": "https://github.com/alexgleason/Fork-Awesome#c23fd34246a9f33c4bf24ea095a4cf26e7abe265",
|
||||
"html-webpack-harddisk-plugin": "^1.0.1",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -1695,6 +1695,16 @@
|
|||
minimatch "^3.0.4"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@fontsource/montserrat@^4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/montserrat/-/montserrat-4.5.1.tgz#64a33ffdb77bbc63484c0321710bed272cc5b16f"
|
||||
integrity sha512-xdMkzsnFlzDt5Vj9+AWuUp4Vd9F3hYN2I0GTZZYscWN/bQ20wVOGtdWLO8Okfv7SZ9t/z39/EayAAt+f/8tAvw==
|
||||
|
||||
"@fontsource/roboto@^4.5.0":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/roboto/-/roboto-4.5.0.tgz#d6f925668ba6af46707f1040c43aff498ba204bb"
|
||||
integrity sha512-ja4XYw/9kNRFM5Ndk9vwzHWsdBMXczyBazFkTXJQ74QQBnT0BbSsHn0pF60AU0Iznig1Wt9x3rADfG8LANvMpw==
|
||||
|
||||
"@formatjs/ecma402-abstract@1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.4.0.tgz#ac6c17a8fffac43c6d68c849a7b732626d32654c"
|
||||
|
@ -5760,16 +5770,6 @@ follow-redirects@^1.10.0:
|
|||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
|
||||
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
|
||||
|
||||
fontsource-montserrat@^3.0.9:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/fontsource-montserrat/-/fontsource-montserrat-3.0.9.tgz#ff614725c420839c4aefbca6f2b49c6224050991"
|
||||
integrity sha512-kSE4GwWoEKJlIp0UyuoYtBvjKv5kwOTK7EvCwdQ7ukvKvgIK1LR5+GpZC5c/juonasNtFTKav67Zd7/C7m+a5w==
|
||||
|
||||
fontsource-roboto@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fontsource-roboto/-/fontsource-roboto-3.0.3.tgz#99c312babeabce22b3e933b3edf2951d4508f4f7"
|
||||
integrity sha512-kfsC9qAP6XhwnSDAhg2lhWeaUJfLGXZh7GcLxFiz/4lXdkV2pVhWv2Xp9ES3b3BHdc9UuPrWXXLOphzHIStcOw==
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
|
|
Ładowanie…
Reference in New Issue