Resolve merge conflicts
Merge branch 'develop' into 'admin_cfg' # Conflicts: # app/soapbox/features/compose/components/action_bar.js # app/soapbox/features/edit_profile/index.js
This commit is contained in:
commit
ea94b05608
|
@ -54,7 +54,7 @@ yarn
|
||||||
Finally, run the dev server:
|
Finally, run the dev server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn start
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**That's it!** :tada:
|
**That's it!** :tada:
|
||||||
|
@ -140,7 +140,7 @@ NODE_ENV=development
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Local dev server
|
#### Local dev server
|
||||||
- `yarn dev` - Exact same as above, aliased to `yarn start` for convenience.
|
- `yarn dev` - Run the local dev server.
|
||||||
|
|
||||||
#### Building
|
#### Building
|
||||||
- `yarn build` - Compile without a dev server, into `/static` directory.
|
- `yarn build` - Compile without a dev server, into `/static` directory.
|
||||||
|
|
|
@ -836,6 +836,7 @@
|
||||||
"registration.lead": "With an account on {instance} you\"ll be able to follow people on any server in the fediverse.",
|
"registration.lead": "With an account on {instance} you\"ll be able to follow people on any server in the fediverse.",
|
||||||
"registration.sign_up": "Sign up",
|
"registration.sign_up": "Sign up",
|
||||||
"registration.tos": "Terms of Service",
|
"registration.tos": "Terms of Service",
|
||||||
|
"registration.reason": "Reason for Joining",
|
||||||
"relative_time.days": "{number}d",
|
"relative_time.days": "{number}d",
|
||||||
"relative_time.hours": "{number}h",
|
"relative_time.hours": "{number}h",
|
||||||
"relative_time.just_now": "now",
|
"relative_time.just_now": "now",
|
||||||
|
|
|
@ -112,12 +112,32 @@ export function refreshUserToken() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function otpVerify(code, mfa_token) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const app = getState().getIn(['auth', 'app']);
|
||||||
|
return api(getState, 'app').post('/oauth/mfa/challenge', {
|
||||||
|
client_id: app.get('client_id'),
|
||||||
|
client_secret: app.get('client_secret'),
|
||||||
|
mfa_token: mfa_token,
|
||||||
|
code: code,
|
||||||
|
challenge_type: 'totp',
|
||||||
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
|
}).then(response => {
|
||||||
|
dispatch(authLoggedIn(response.data));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function logIn(username, password) {
|
export function logIn(username, password) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
return dispatch(createAppAndToken()).then(() => {
|
return dispatch(createAppAndToken()).then(() => {
|
||||||
return dispatch(createUserToken(username, password));
|
return dispatch(createUserToken(username, password));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(showAlert('Login failed.', 'Invalid username or password.'));
|
if (error.response.data.error === 'mfa_required') {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
dispatch(showAlert('Login failed.', 'Invalid username or password.'));
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -133,15 +153,20 @@ export function logOut() {
|
||||||
export function register(params) {
|
export function register(params) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const needsConfirmation = getState().getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']);
|
const needsConfirmation = getState().getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']);
|
||||||
|
const needsApproval = getState().getIn(['instance', 'approval_required']);
|
||||||
dispatch({ type: AUTH_REGISTER_REQUEST });
|
dispatch({ type: AUTH_REGISTER_REQUEST });
|
||||||
return dispatch(createAppAndToken()).then(() => {
|
return dispatch(createAppAndToken()).then(() => {
|
||||||
return api(getState, 'app').post('/api/v1/accounts', params);
|
return api(getState, 'app').post('/api/v1/accounts', params);
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
dispatch({ type: AUTH_REGISTER_SUCCESS, token: response.data });
|
dispatch({ type: AUTH_REGISTER_SUCCESS, token: response.data });
|
||||||
dispatch(authLoggedIn(response.data));
|
dispatch(authLoggedIn(response.data));
|
||||||
return needsConfirmation
|
if (needsConfirmation) {
|
||||||
? dispatch(showAlert('', 'Check your email for further instructions.'))
|
return dispatch(showAlert('', 'Check your email for further instructions.'));
|
||||||
: dispatch(fetchMe());
|
} else if (needsApproval) {
|
||||||
|
return dispatch(showAlert('', 'Your account has been submitted for approval.'));
|
||||||
|
} else {
|
||||||
|
return dispatch(fetchMe());
|
||||||
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: AUTH_REGISTER_FAIL, error });
|
dispatch({ type: AUTH_REGISTER_FAIL, error });
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import api, { getLinks } from '../api';
|
||||||
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
|
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
|
||||||
|
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
|
||||||
|
export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST';
|
||||||
|
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
|
||||||
|
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatuses() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchBookmarkedStatusesRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/bookmarks').then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchBookmarkedStatusesFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatusesRequest() {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatusesSuccess(statuses, next) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBookmarkedStatusesFail(error) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandBookmarkedStatuses() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);
|
||||||
|
|
||||||
|
if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandBookmarkedStatusesRequest());
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(expandBookmarkedStatusesFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandBookmarkedStatusesRequest() {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandBookmarkedStatusesSuccess(statuses, next) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandBookmarkedStatusesFail(error) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
|
@ -43,6 +43,7 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
||||||
|
|
||||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||||
|
export const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
||||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||||
|
@ -175,6 +176,7 @@ export function submitCompose(routerHistory, group) {
|
||||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
|
content_type: getState().getIn(['compose', 'content_type']),
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
group_id: group ? group.get('id') : null,
|
group_id: group ? group.get('id') : null,
|
||||||
}, {
|
}, {
|
||||||
|
@ -226,11 +228,6 @@ export function uploadCompose(files) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getState().getIn(['compose', 'poll'])) {
|
|
||||||
dispatch(showAlert(undefined, messages.uploadErrorPoll));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(uploadComposeRequest());
|
dispatch(uploadComposeRequest());
|
||||||
|
|
||||||
for (const [i, f] of Array.from(files).entries()) {
|
for (const [i, f] of Array.from(files).entries()) {
|
||||||
|
@ -495,6 +492,13 @@ export function changeComposeSpoilerness() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function changeComposeContentType(value) {
|
||||||
|
return {
|
||||||
|
type: COMPOSE_TYPE_CHANGE,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function changeComposeSpoilerText(text) {
|
export function changeComposeSpoilerText(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_SPOILER_TEXT_CHANGE,
|
type: COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { showAlert } from 'soapbox/actions/alerts';
|
||||||
|
|
||||||
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
||||||
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
export const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
||||||
|
@ -8,6 +9,10 @@ export const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
|
||||||
export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
||||||
export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
||||||
|
|
||||||
|
export const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
||||||
|
export const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
||||||
|
export const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
||||||
|
|
||||||
export const fetchFilters = () => (dispatch, getState) => {
|
export const fetchFilters = () => (dispatch, getState) => {
|
||||||
if (!getState().get('me')) return;
|
if (!getState().get('me')) return;
|
||||||
|
|
||||||
|
@ -31,13 +36,33 @@ export const fetchFilters = () => (dispatch, getState) => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createFilter(params) {
|
export function createFilter(phrase, expires_at, context, whole_word, irreversible) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||||
return api(getState).post('/api/v1/filters', params).then(response => {
|
return api(getState).post('/api/v1/filters', {
|
||||||
|
phrase,
|
||||||
|
context,
|
||||||
|
irreversible,
|
||||||
|
whole_word,
|
||||||
|
expires_at,
|
||||||
|
}).then(response => {
|
||||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||||
|
dispatch(showAlert('', 'Filter added'));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function deleteFilter(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||||
|
return api(getState).delete('/api/v1/filters/'+id).then(response => {
|
||||||
|
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||||
|
dispatch(showAlert('', 'Filter deleted'));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
|
import { showAlert } from 'soapbox/actions/alerts';
|
||||||
|
|
||||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||||
|
@ -33,6 +34,14 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST';
|
||||||
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
|
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
|
||||||
export const UNPIN_FAIL = 'UNPIN_FAIL';
|
export const UNPIN_FAIL = 'UNPIN_FAIL';
|
||||||
|
|
||||||
|
export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST';
|
||||||
|
export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS';
|
||||||
|
export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL';
|
||||||
|
|
||||||
|
export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
||||||
|
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||||
|
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||||
|
|
||||||
export function reblog(status) {
|
export function reblog(status) {
|
||||||
return function(dispatch, getState) {
|
return function(dispatch, getState) {
|
||||||
if (!getState().get('me')) return;
|
if (!getState().get('me')) return;
|
||||||
|
@ -195,6 +204,80 @@ export function unfavouriteFail(status, error) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function bookmark(status) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
dispatch(bookmarkRequest(status));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
|
||||||
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(bookmarkSuccess(status, response.data));
|
||||||
|
dispatch(showAlert('', 'Bookmark added'));
|
||||||
|
}).catch(function(error) {
|
||||||
|
dispatch(bookmarkFail(status, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unbookmark(status) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(unbookmarkRequest(status));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
||||||
|
dispatch(importFetchedStatus(response.data));
|
||||||
|
dispatch(unbookmarkSuccess(status, response.data));
|
||||||
|
dispatch(showAlert('', 'Bookmark removed'));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(unbookmarkFail(status, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function bookmarkRequest(status) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARK_REQUEST,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function bookmarkSuccess(status, response) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARK_SUCCESS,
|
||||||
|
status: status,
|
||||||
|
response: response,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function bookmarkFail(status, error) {
|
||||||
|
return {
|
||||||
|
type: BOOKMARK_FAIL,
|
||||||
|
status: status,
|
||||||
|
error: error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unbookmarkRequest(status) {
|
||||||
|
return {
|
||||||
|
type: UNBOOKMARK_REQUEST,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unbookmarkSuccess(status, response) {
|
||||||
|
return {
|
||||||
|
type: UNBOOKMARK_SUCCESS,
|
||||||
|
status: status,
|
||||||
|
response: response,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unbookmarkFail(status, error) {
|
||||||
|
return {
|
||||||
|
type: UNBOOKMARK_FAIL,
|
||||||
|
status: status,
|
||||||
|
error: error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function fetchReblogs(id) {
|
export function fetchReblogs(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (!getState().get('me')) return;
|
if (!getState().get('me')) return;
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const TOTP_SETTINGS_FETCH_REQUEST = 'TOTP_SETTINGS_FETCH_REQUEST';
|
||||||
|
export const TOTP_SETTINGS_FETCH_SUCCESS = 'TOTP_SETTINGS_FETCH_SUCCESS';
|
||||||
|
export const TOTP_SETTINGS_FETCH_FAIL = 'TOTP_SETTINGS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const BACKUP_CODES_FETCH_REQUEST = 'BACKUP_CODES_FETCH_REQUEST';
|
||||||
|
export const BACKUP_CODES_FETCH_SUCCESS = 'BACKUP_CODES_FETCH_SUCCESS';
|
||||||
|
export const BACKUP_CODES_FETCH_FAIL = 'BACKUP_CODES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const TOTP_SETUP_FETCH_REQUEST = 'TOTP_SETUP_FETCH_REQUEST';
|
||||||
|
export const TOTP_SETUP_FETCH_SUCCESS = 'TOTP_SETUP_FETCH_SUCCESS';
|
||||||
|
export const TOTP_SETUP_FETCH_FAIL = 'TOTP_SETUP_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const CONFIRM_TOTP_REQUEST = 'CONFIRM_TOTP_REQUEST';
|
||||||
|
export const CONFIRM_TOTP_SUCCESS = 'CONFIRM_TOTP_SUCCESS';
|
||||||
|
export const CONFIRM_TOTP_FAIL = 'CONFIRM_TOTP_FAIL';
|
||||||
|
|
||||||
|
export const DISABLE_TOTP_REQUEST = 'DISABLE_TOTP_REQUEST';
|
||||||
|
export const DISABLE_TOTP_SUCCESS = 'DISABLE_TOTP_SUCCESS';
|
||||||
|
export const DISABLE_TOTP_FAIL = 'DISABLE_TOTP_FAIL';
|
||||||
|
|
||||||
|
export function fetchUserMfaSettings() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: TOTP_SETTINGS_FETCH_REQUEST });
|
||||||
|
return api(getState).get('/api/pleroma/accounts/mfa').then(response => {
|
||||||
|
dispatch({ type: TOTP_SETTINGS_FETCH_SUCCESS, totpEnabled: response.data.totp });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: TOTP_SETTINGS_FETCH_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUserMfaSettingsRequest() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETTINGS_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchUserMfaSettingsSuccess() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETTINGS_FETCH_SUCCESS,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchUserMfaSettingsFail() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETTINGS_FETCH_FAIL,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBackupCodes() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: BACKUP_CODES_FETCH_REQUEST });
|
||||||
|
return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(response => {
|
||||||
|
dispatch({ type: BACKUP_CODES_FETCH_SUCCESS, backup_codes: response.data });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: BACKUP_CODES_FETCH_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBackupCodesRequest() {
|
||||||
|
return {
|
||||||
|
type: BACKUP_CODES_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBackupCodesSuccess(backup_codes, response) {
|
||||||
|
return {
|
||||||
|
type: BACKUP_CODES_FETCH_SUCCESS,
|
||||||
|
backup_codes: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchBackupCodesFail(error) {
|
||||||
|
return {
|
||||||
|
type: BACKUP_CODES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchToptSetup() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: TOTP_SETUP_FETCH_REQUEST });
|
||||||
|
return api(getState).get('/api/pleroma/accounts/mfa/setup/totp').then(response => {
|
||||||
|
dispatch({ type: TOTP_SETUP_FETCH_SUCCESS, totp_setup: response.data });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: TOTP_SETUP_FETCH_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchToptSetupRequest() {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETUP_FETCH_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchToptSetupSuccess(totp_setup, response) {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETUP_FETCH_SUCCESS,
|
||||||
|
totp_setup: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchToptSetupFail(error) {
|
||||||
|
return {
|
||||||
|
type: TOTP_SETUP_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function confirmToptSetup(code, password) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: CONFIRM_TOTP_REQUEST, code });
|
||||||
|
return api(getState).post('/api/pleroma/accounts/mfa/confirm/totp', {
|
||||||
|
code,
|
||||||
|
password,
|
||||||
|
}).then(response => {
|
||||||
|
dispatch({ type: CONFIRM_TOTP_SUCCESS });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: CONFIRM_TOTP_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmToptRequest() {
|
||||||
|
return {
|
||||||
|
type: CONFIRM_TOTP_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function confirmToptSuccess(backup_codes, response) {
|
||||||
|
return {
|
||||||
|
type: CONFIRM_TOTP_SUCCESS,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function confirmToptFail(error) {
|
||||||
|
return {
|
||||||
|
type: CONFIRM_TOTP_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableToptSetup(password) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({ type: DISABLE_TOTP_REQUEST });
|
||||||
|
return api(getState).delete('/api/pleroma/accounts/mfa/totp', { data: { password } }).then(response => {
|
||||||
|
dispatch({ type: DISABLE_TOTP_SUCCESS });
|
||||||
|
return response;
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch({ type: DISABLE_TOTP_FAIL });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableToptRequest() {
|
||||||
|
return {
|
||||||
|
type: DISABLE_TOTP_REQUEST,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableToptSuccess(backup_codes, response) {
|
||||||
|
return {
|
||||||
|
type: DISABLE_TOTP_SUCCESS,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableToptFail(error) {
|
||||||
|
return {
|
||||||
|
type: DISABLE_TOTP_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
|
@ -22,6 +22,8 @@ const defaultSettings = ImmutableMap({
|
||||||
defaultPrivacy: 'public',
|
defaultPrivacy: 'public',
|
||||||
themeMode: 'light',
|
themeMode: 'light',
|
||||||
locale: navigator.language.split(/[-_]/)[0] || 'en',
|
locale: navigator.language.split(/[-_]/)[0] || 'en',
|
||||||
|
explanationBox: true,
|
||||||
|
otpEnabled: false,
|
||||||
|
|
||||||
systemFont: false,
|
systemFont: false,
|
||||||
dyslexicFont: false,
|
dyslexicFont: false,
|
||||||
|
@ -31,6 +33,7 @@ const defaultSettings = ImmutableMap({
|
||||||
shows: ImmutableMap({
|
shows: ImmutableMap({
|
||||||
reblog: true,
|
reblog: true,
|
||||||
reply: true,
|
reply: true,
|
||||||
|
direct: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
regex: ImmutableMap({
|
regex: ImmutableMap({
|
||||||
|
@ -71,6 +74,10 @@ const defaultSettings = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
community: ImmutableMap({
|
community: ImmutableMap({
|
||||||
|
shows: ImmutableMap({
|
||||||
|
reblog: true,
|
||||||
|
reply: true,
|
||||||
|
}),
|
||||||
other: ImmutableMap({
|
other: ImmutableMap({
|
||||||
onlyMedia: false,
|
onlyMedia: false,
|
||||||
}),
|
}),
|
||||||
|
@ -80,6 +87,10 @@ const defaultSettings = ImmutableMap({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
public: ImmutableMap({
|
public: ImmutableMap({
|
||||||
|
shows: ImmutableMap({
|
||||||
|
reblog: true,
|
||||||
|
reply: true,
|
||||||
|
}),
|
||||||
other: ImmutableMap({
|
other: ImmutableMap({
|
||||||
onlyMedia: false,
|
onlyMedia: false,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -148,13 +148,21 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||||
|
|
||||||
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
|
|
||||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
|
|
||||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||||
|
|
||||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||||
|
|
||||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||||
|
|
||||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||||
|
|
||||||
export const expandGroupTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
|
export const expandGroupTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done);
|
||||||
|
|
||||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
|
|
|
@ -1,26 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import VerificationBadge from './verification_badge';
|
import VerificationBadge from './verification_badge';
|
||||||
import { acctFull } from '../utils/accounts';
|
import { acctFull } from '../utils/accounts';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
export default class DisplayName extends React.PureComponent {
|
export default class DisplayName extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
others: ImmutablePropTypes.list,
|
others: ImmutablePropTypes.list,
|
||||||
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { account, others } = this.props;
|
const { account, others, children } = this.props;
|
||||||
|
|
||||||
let displayName, suffix;
|
let displayName, suffix;
|
||||||
|
const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified');
|
||||||
|
|
||||||
if (others && others.size > 1) {
|
if (others && others.size > 1) {
|
||||||
displayName = others.take(2).map(a => [
|
displayName = others.take(2).map(a => [
|
||||||
<bdi key={a.get('id')}>
|
<bdi key={a.get('id')}>
|
||||||
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} />
|
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} />
|
||||||
</bdi>,
|
</bdi>,
|
||||||
a.get('is_verified') && <VerificationBadge />,
|
verified && <VerificationBadge />,
|
||||||
]).reduce((prev, cur) => [prev, ', ', cur]);
|
]).reduce((prev, cur) => [prev, ', ', cur]);
|
||||||
|
|
||||||
if (others.size - 2 > 0) {
|
if (others.size - 2 > 0) {
|
||||||
|
@ -30,7 +34,7 @@ export default class DisplayName extends React.PureComponent {
|
||||||
displayName = (
|
displayName = (
|
||||||
<>
|
<>
|
||||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
||||||
{account.get('is_verified') && <VerificationBadge />}
|
{verified && <VerificationBadge />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
suffix = <span className='display-name__account'>@{acctFull(account)}</span>;
|
suffix = <span className='display-name__account'>@{acctFull(account)}</span>;
|
||||||
|
@ -40,6 +44,7 @@ export default class DisplayName extends React.PureComponent {
|
||||||
<span className='display-name'>
|
<span className='display-name'>
|
||||||
{displayName}
|
{displayName}
|
||||||
{suffix}
|
{suffix}
|
||||||
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ const messages = defineMessages({
|
||||||
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
|
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||||
|
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
||||||
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
|
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
|
||||||
donate: { id: 'donate', defaultMessage: 'Donate' },
|
donate: { id: 'donate', defaultMessage: 'Donate' },
|
||||||
|
@ -145,6 +146,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
<Icon id='list' />
|
<Icon id='list' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink className='sidebar-menu-item' to='/bookmarks' onClick={onClose}>
|
||||||
|
<Icon id='bookmark' />
|
||||||
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.bookmarks)}</span>
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='sidebar-menu__section'>
|
<div className='sidebar-menu__section'>
|
||||||
|
@ -164,11 +169,11 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
<Icon id='times-circle' />
|
<Icon id='times-circle' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.mutes)}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{/* <NavLink className='sidebar-menu-item' to='/filters' onClick={onClose}>
|
<NavLink className='sidebar-menu-item' to='/filters' onClick={onClose}>
|
||||||
<Icon id='filter' />
|
<Icon id='filter' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.filters)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.filters)}</span>
|
||||||
</NavLink> */}
|
</NavLink>
|
||||||
{ isStaff && <a className='sidebar-menu-item' href={'/pleroma/admin/'} onClick={onClose}>
|
{ isStaff && <a className='sidebar-menu-item' href={'/pleroma/admin/'} target='_blank' onClick={onClose}>
|
||||||
<Icon id='shield' />
|
<Icon id='shield' />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.admin_settings)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.admin_settings)}</span>
|
||||||
</a> }
|
</a> }
|
||||||
|
|
|
@ -18,6 +18,9 @@ import classNames from 'classnames';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import PollContainer from 'soapbox/containers/poll_container';
|
import PollContainer from 'soapbox/containers/poll_container';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import ProfileHoverCardContainer from '../features/profile_hover_card/profile_hover_card_container';
|
||||||
|
import { isMobile } from '../../../app/soapbox/is_mobile';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
// We use the component (and not the container) since we do not want
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
|
@ -104,6 +107,7 @@ class Status extends ImmutablePureComponent {
|
||||||
state = {
|
state = {
|
||||||
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
||||||
statusId: undefined,
|
statusId: undefined,
|
||||||
|
profileCardVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track height changes we know about to compensate scrolling
|
// Track height changes we know about to compensate scrolling
|
||||||
|
@ -249,6 +253,19 @@ class Status extends ImmutablePureComponent {
|
||||||
this.handleToggleMediaVisibility();
|
this.handleToggleMediaVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showProfileCard = debounce(() => {
|
||||||
|
this.setState({ profileCardVisible: true });
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
|
handleProfileHover = e => {
|
||||||
|
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProfileLeave = e => {
|
||||||
|
this.showProfileCard.cancel();
|
||||||
|
this.setState({ profileCardVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
_properStatus() {
|
_properStatus() {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
|
|
||||||
|
@ -265,6 +282,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let media = null;
|
let media = null;
|
||||||
|
let poll = null;
|
||||||
let statusAvatar, prepend, rebloggedByText, reblogContent;
|
let statusAvatar, prepend, rebloggedByText, reblogContent;
|
||||||
|
|
||||||
const { intl, hidden, featured, otherAccounts, unread, showThread, group } = this.props;
|
const { intl, hidden, featured, otherAccounts, unread, showThread, group } = this.props;
|
||||||
|
@ -332,8 +350,9 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('poll')) {
|
if (status.get('poll')) {
|
||||||
media = <PollContainer pollId={status.get('poll')} />;
|
poll = <PollContainer pollId={status.get('poll')} />;
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
}
|
||||||
|
if (status.get('media_attachments').size > 0) {
|
||||||
if (this.props.muted) {
|
if (this.props.muted) {
|
||||||
media = (
|
media = (
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
|
@ -435,6 +454,7 @@ class Status extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
||||||
|
const { profileCardVisible } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
|
@ -448,13 +468,18 @@ class Status extends ImmutablePureComponent {
|
||||||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
<div className='status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
||||||
<div className='status__avatar'>
|
<div className='status__avatar'>
|
||||||
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
||||||
{statusAvatar}
|
{statusAvatar}
|
||||||
</div>
|
</div>
|
||||||
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
{ profileCardVisible &&
|
||||||
|
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth)} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!group && status.get('group') && (
|
{!group && status.get('group') && (
|
||||||
|
@ -473,6 +498,7 @@ class Status extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
{poll}
|
||||||
|
|
||||||
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||||
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||||
|
|
|
@ -31,6 +31,8 @@ const messages = defineMessages({
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
||||||
open: { id: 'status.open', defaultMessage: 'Expand this post' },
|
open: { id: 'status.open', defaultMessage: 'Expand this post' },
|
||||||
|
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||||
|
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||||
|
@ -55,6 +57,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onOpenUnauthorizedModal: PropTypes.func.isRequired,
|
onOpenUnauthorizedModal: PropTypes.func.isRequired,
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
|
onBookmark: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onDirect: PropTypes.func,
|
onDirect: PropTypes.func,
|
||||||
|
@ -149,6 +152,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleBookmarkClick = () => {
|
||||||
|
this.props.onBookmark(this.props.status);
|
||||||
|
}
|
||||||
|
|
||||||
handleReblogClick = e => {
|
handleReblogClick = e => {
|
||||||
const { me } = this.props;
|
const { me } = this.props;
|
||||||
if (me) {
|
if (me) {
|
||||||
|
@ -246,6 +253,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
// menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
// menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||||
|
|
||||||
if (!me) {
|
if (!me) {
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
@ -358,7 +367,9 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onMouseLeave={this.handleLikeButtonLeave}
|
onMouseLeave={this.handleLikeButtonLeave}
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
>
|
>
|
||||||
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
{ emojiSelectorVisible &&
|
||||||
|
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
||||||
|
}
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__action-bar-button star-icon'
|
className='status__action-bar-button star-icon'
|
||||||
animate
|
animate
|
||||||
|
|
|
@ -66,7 +66,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
onMuteNotifications(account, notifications) {
|
onMuteNotifications(account, notifications) {
|
||||||
dispatch(muteAccount(account.get('id'), notifications));
|
dispatch(muteAccount(account.get('id'), notifications));
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
favourite,
|
favourite,
|
||||||
unreblog,
|
unreblog,
|
||||||
unfavourite,
|
unfavourite,
|
||||||
|
bookmark,
|
||||||
|
unbookmark,
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
|
@ -100,6 +102,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onBookmark(status) {
|
||||||
|
if (status.get('bookmarked')) {
|
||||||
|
dispatch(unbookmark(status));
|
||||||
|
} else {
|
||||||
|
dispatch(bookmark(status));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onPin(status) {
|
onPin(status) {
|
||||||
if (status.get('pinned')) {
|
if (status.get('pinned')) {
|
||||||
dispatch(unpin(status));
|
dispatch(unpin(status));
|
||||||
|
|
|
@ -17,12 +17,9 @@ import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||||
import ProfileInfoPanel from '../../ui/components/profile_info_panel';
|
import ProfileInfoPanel from '../../ui/components/profile_info_panel';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import StillImage from 'soapbox/components/still_image';
|
import StillImage from 'soapbox/components/still_image';
|
||||||
|
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||||
|
@ -65,8 +62,6 @@ class Header extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
identity_props: ImmutablePropTypes.list,
|
identity_props: ImmutablePropTypes.list,
|
||||||
onFollow: PropTypes.func.isRequired,
|
|
||||||
onBlock: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
isStaff: PropTypes.bool.isRequired,
|
isStaff: PropTypes.bool.isRequired,
|
||||||
|
@ -171,7 +166,7 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (account.get('id') !== me && isStaff) {
|
if (account.get('id') !== me && isStaff) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/` });
|
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return menu;
|
return menu;
|
||||||
|
@ -199,30 +194,6 @@ class Header extends ImmutablePureComponent {
|
||||||
return info;
|
return info;
|
||||||
};
|
};
|
||||||
|
|
||||||
getActionBtn() {
|
|
||||||
const { account, intl, me } = this.props;
|
|
||||||
|
|
||||||
let actionBtn = null;
|
|
||||||
|
|
||||||
if (!account || !me) return actionBtn;
|
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
|
||||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
|
||||||
//
|
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
|
||||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
|
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
|
||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} to='/settings/profile' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return actionBtn;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { account, intl, username, me } = this.props;
|
const { account, intl, username, me } = this.props;
|
||||||
const { isSmallScreen } = this.state;
|
const { isSmallScreen } = this.state;
|
||||||
|
@ -247,7 +218,6 @@ class Header extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = this.makeInfo();
|
const info = this.makeInfo();
|
||||||
const actionBtn = this.getActionBtn();
|
|
||||||
const menu = this.makeMenu();
|
const menu = this.makeMenu();
|
||||||
|
|
||||||
const headerMissing = (account.get('header').indexOf('/headers/original/missing.png') > -1);
|
const headerMissing = (account.get('header').indexOf('/headers/original/missing.png') > -1);
|
||||||
|
@ -319,7 +289,7 @@ class Header extends ImmutablePureComponent {
|
||||||
{
|
{
|
||||||
me &&
|
me &&
|
||||||
<div className='account__header__extra__buttons'>
|
<div className='account__header__extra__buttons'>
|
||||||
{actionBtn}
|
<ActionButton account={account} />
|
||||||
{account.get('id') !== me &&
|
{account.get('id') !== me &&
|
||||||
<Button className='button button-alternative-2' onClick={this.props.onDirect}>
|
<Button className='button button-alternative-2' onClick={this.props.onDirect}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|
|
@ -3,11 +3,8 @@
|
||||||
exports[`<LoginForm /> renders correctly 1`] = `
|
exports[`<LoginForm /> renders correctly 1`] = `
|
||||||
<form
|
<form
|
||||||
className="simple_form new_user"
|
className="simple_form new_user"
|
||||||
onSubmit={[Function]}
|
|
||||||
>
|
>
|
||||||
<fieldset
|
<fieldset>
|
||||||
disabled={false}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="fields-group"
|
className="fields-group"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<LoginPage /> renders correctly 1`] = `
|
exports[`<LoginPage /> renders correctly on load 1`] = `
|
||||||
<form
|
<form
|
||||||
className="simple_form new_user"
|
className="simple_form new_user"
|
||||||
onSubmit={[Function]}
|
onSubmit={[Function]}
|
||||||
|
@ -59,4 +59,4 @@ exports[`<LoginPage /> renders correctly 1`] = `
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<LoginPage /> renders correctly 2`] = `null`;
|
exports[`<LoginPage /> renders correctly on load 2`] = `null`;
|
||||||
|
|
|
@ -2,9 +2,16 @@ import React from 'react';
|
||||||
import LoginPage from '../login_page';
|
import LoginPage from '../login_page';
|
||||||
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
// import { __stub as stubApi } from 'soapbox/api';
|
||||||
|
// import { logIn } from 'soapbox/actions/auth';
|
||||||
|
|
||||||
describe('<LoginPage />', () => {
|
describe('<LoginPage />', () => {
|
||||||
it('renders correctly', () => {
|
beforeEach(() => {
|
||||||
|
const store = mockStore(ImmutableMap({}));
|
||||||
|
return store;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly on load', () => {
|
||||||
expect(createComponent(
|
expect(createComponent(
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
).toJSON()).toMatchSnapshot();
|
).toJSON()).toMatchSnapshot();
|
||||||
|
@ -12,7 +19,38 @@ describe('<LoginPage />', () => {
|
||||||
const store = mockStore(ImmutableMap({ me: '1234' }));
|
const store = mockStore(ImmutableMap({ me: '1234' }));
|
||||||
expect(createComponent(
|
expect(createComponent(
|
||||||
<LoginPage />,
|
<LoginPage />,
|
||||||
{ store },
|
{ store }
|
||||||
).toJSON()).toMatchSnapshot();
|
).toJSON()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// it('renders the OTP form when logIn returns with mfa_required', () => {
|
||||||
|
//
|
||||||
|
// stubApi(mock => {
|
||||||
|
// mock.onPost('/api/v1/apps').reply(200, {
|
||||||
|
// data: {
|
||||||
|
// client_id:'12345', client_secret:'12345', id:'111', name:'SoapboxFE', redirect_uri:'urn:ietf:wg:oauth:2.0:oob', website:null, vapid_key:'12345',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// mock.onPost('/oauth/token').reply(403, {
|
||||||
|
// error:'mfa_required', mfa_token:'12345', supported_challenge_types:'totp',
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const app = new Map();
|
||||||
|
// app.set('app', { client_id: '12345', client_secret:'12345' });
|
||||||
|
// const store = mockStore(ImmutableMap({
|
||||||
|
// auth: { app },
|
||||||
|
// }));
|
||||||
|
// const loginPage = createComponent(<LoginPage />, { store });
|
||||||
|
//
|
||||||
|
// return loginPage.handleSubmit().then(() => {
|
||||||
|
// const wrapper = loginPage.toJSON();
|
||||||
|
// expect(wrapper.children[0].children[0].children[0].children[0]).toEqual(expect.objectContaining({
|
||||||
|
// type: 'h1',
|
||||||
|
// props: { className: 'otp-login' },
|
||||||
|
// children: [ 'OTP Login' ],
|
||||||
|
// }));
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import OtpAuthForm from '../otp_auth_form';
|
||||||
|
import { createComponent, mockStore } from 'soapbox/test_helpers';
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
describe('<OtpAuthForm />', () => {
|
||||||
|
it('renders correctly', () => {
|
||||||
|
|
||||||
|
const store = mockStore(ImmutableMap({ mfa_auth_needed: true }));
|
||||||
|
|
||||||
|
const wrapper = createComponent(
|
||||||
|
<OtpAuthForm
|
||||||
|
mfa_token={'12345'}
|
||||||
|
/>,
|
||||||
|
{ store }
|
||||||
|
).toJSON();
|
||||||
|
|
||||||
|
expect(wrapper).toEqual(expect.objectContaining({
|
||||||
|
type: 'form',
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(wrapper.children[0].children[0].children[0].children[0]).toEqual(expect.objectContaining({
|
||||||
|
type: 'h1',
|
||||||
|
props: { className: 'otp-login' },
|
||||||
|
children: [ 'OTP Login' ],
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,8 +3,6 @@ import { connect } from 'react-redux';
|
||||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { logIn } from 'soapbox/actions/auth';
|
|
||||||
import { fetchMe } from 'soapbox/actions/me';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
username: { id: 'login.fields.username_placeholder', defaultMessage: 'Username' },
|
username: { id: 'login.fields.username_placeholder', defaultMessage: 'Username' },
|
||||||
|
@ -15,34 +13,12 @@ export default @connect()
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class LoginForm extends ImmutablePureComponent {
|
class LoginForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
state = {
|
|
||||||
isLoading: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormData = (form) => {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Array.from(form).map(i => [i.name, i.value])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit = (event) => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const { username, password } = this.getFormData(event.target);
|
|
||||||
dispatch(logIn(username, password)).then(() => {
|
|
||||||
return dispatch(fetchMe());
|
|
||||||
}).catch(error => {
|
|
||||||
this.setState({ isLoading: false });
|
|
||||||
});
|
|
||||||
this.setState({ isLoading: true });
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl } = this.props;
|
const { intl, isLoading, handleSubmit } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='simple_form new_user' onSubmit={this.handleSubmit}>
|
<form className='simple_form new_user' onSubmit={handleSubmit}>
|
||||||
<fieldset disabled={this.state.isLoading}>
|
<fieldset disabled={isLoading}>
|
||||||
<div className='fields-group'>
|
<div className='fields-group'>
|
||||||
<div className='input email optional user_email'>
|
<div className='input email optional user_email'>
|
||||||
<input aria-label={intl.formatMessage(messages.username)} className='string email optional' placeholder={intl.formatMessage(messages.username)} type='text' name='username' />
|
<input aria-label={intl.formatMessage(messages.username)} className='string email optional' placeholder={intl.formatMessage(messages.username)} type='text' name='username' />
|
||||||
|
|
|
@ -3,19 +3,57 @@ import { connect } from 'react-redux';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import LoginForm from './login_form';
|
import LoginForm from './login_form';
|
||||||
|
import OtpAuthForm from './otp_auth_form';
|
||||||
|
import { logIn } from 'soapbox/actions/auth';
|
||||||
|
import { fetchMe } from 'soapbox/actions/me';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
me: state.get('me'),
|
me: state.get('me'),
|
||||||
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
class LoginPage extends ImmutablePureComponent {
|
class LoginPage extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormData = (form) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Array.from(form).map(i => [i.name, i.value])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
mfa_auth_needed: false,
|
||||||
|
mfa_token: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = (event) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { username, password } = this.getFormData(event.target);
|
||||||
|
dispatch(logIn(username, password)).then(() => {
|
||||||
|
return dispatch(fetchMe());
|
||||||
|
}).catch(error => {
|
||||||
|
if (error.response.data.error === 'mfa_required') {
|
||||||
|
this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token });
|
||||||
|
}
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
});
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { me } = this.props;
|
const { me, isLoading } = this.props;
|
||||||
|
const { mfa_auth_needed, mfa_token } = this.state;
|
||||||
if (me) return <Redirect to='/' />;
|
if (me) return <Redirect to='/' />;
|
||||||
|
|
||||||
return <LoginForm />;
|
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;
|
||||||
|
|
||||||
|
return <LoginForm handleSubmit={this.handleSubmit} isLoading={isLoading} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { otpVerify } from 'soapbox/actions/auth';
|
||||||
|
import { fetchMe } from 'soapbox/actions/me';
|
||||||
|
import { SimpleInput } from 'soapbox/features/forms';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' },
|
||||||
|
otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect()
|
||||||
|
@injectIntl
|
||||||
|
class OtpAuthForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isLoading: false,
|
||||||
|
code_error: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
mfa_token: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
getFormData = (form) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Array.from(form).map(i => [i.name, i.value])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = (event) => {
|
||||||
|
const { dispatch, mfa_token } = this.props;
|
||||||
|
const { code } = this.getFormData(event.target);
|
||||||
|
dispatch(otpVerify(code, mfa_token)).then(() => {
|
||||||
|
this.setState({ code_error: false });
|
||||||
|
return dispatch(fetchMe());
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
if (error.response.data.error === 'Invalid code') {
|
||||||
|
this.setState({ code_error: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const { code_error } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className='simple_form new_user otp-auth' onSubmit={this.handleSubmit}>
|
||||||
|
<fieldset disabled={this.state.isLoading}>
|
||||||
|
<div className='fields-group'>
|
||||||
|
<div className='input email optional user_email'>
|
||||||
|
<h1 className='otp-login'>
|
||||||
|
<FormattedMessage id='login.otp_log_in' defaultMessage='OTP Login' />
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className='input code optional otp_code'>
|
||||||
|
<SimpleInput
|
||||||
|
label={intl.formatMessage(messages.otpCodeLabel)}
|
||||||
|
hint={intl.formatMessage(messages.otpCodeHint)}
|
||||||
|
name='code'
|
||||||
|
type='text'
|
||||||
|
autoComplete='off'
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{ code_error &&
|
||||||
|
<div className='error-box'>
|
||||||
|
<FormattedMessage id='login.otp_log_in.fail' defaultMessage='Invalid code, please try again.' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className='actions'>
|
||||||
|
<button name='button' type='submit' className='btn button button-primary'>
|
||||||
|
<FormattedMessage id='login.log_in' defaultMessage='Log in' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import StatusList from '../../components/status_list';
|
||||||
|
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
|
||||||
|
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
||||||
|
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Bookmarks extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
statusIds: ImmutablePropTypes.list.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
columnId: PropTypes.string,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchBookmarkedStatuses());
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.dispatch(expandBookmarkedStatuses());
|
||||||
|
}, 300, { leading: true })
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||||
|
const pinned = !!columnId;
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column icon='bookmark' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||||
|
<StatusList
|
||||||
|
trackScroll={!pinned}
|
||||||
|
statusIds={statusIds}
|
||||||
|
scrollKey={`bookmarked_statuses-${columnId}`}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,6 +18,14 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,7 @@ const messages = defineMessages({
|
||||||
profile: { id: 'account.profile', defaultMessage: 'Profile' },
|
profile: { id: 'account.profile', defaultMessage: 'Profile' },
|
||||||
messages: { id: 'navigation_bar.messages', defaultMessage: 'Messages' },
|
messages: { id: 'navigation_bar.messages', defaultMessage: 'Messages' },
|
||||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||||
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
|
@ -70,17 +71,18 @@ class ActionBar extends React.PureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.profile), to: `/@${meUsername}` });
|
menu.push({ text: intl.formatMessage(messages.profile), to: `/@${meUsername}` });
|
||||||
menu.push({ text: intl.formatMessage(messages.messages), to: '/messages' });
|
menu.push({ text: intl.formatMessage(messages.messages), to: '/messages' });
|
||||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||||
// menu.push({ text: intl.formatMessage(messages.filters), to: '/filters' });
|
menu.push({ text: intl.formatMessage(messages.filters), to: '/filters' });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
|
menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
|
||||||
if (isStaff) {
|
if (isStaff) {
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_settings), href: '/pleroma/admin/' });
|
menu.push({ text: intl.formatMessage(messages.admin_settings), href: '/pleroma/admin/' });
|
||||||
menu.push({ text: intl.formatMessage(messages.soapbox_settings), href: '/admin/' });
|
menu.push({ text: intl.formatMessage(messages.admin_settings), href: '/pleroma/admin/', newTab: true });
|
||||||
}
|
}
|
||||||
menu.push({ text: intl.formatMessage(messages.preferences), to: '/settings/preferences' });
|
menu.push({ text: intl.formatMessage(messages.preferences), to: '/settings/preferences' });
|
||||||
menu.push({ text: intl.formatMessage(messages.security), to: '/auth/edit' });
|
menu.push({ text: intl.formatMessage(messages.security), to: '/auth/edit' });
|
||||||
|
|
|
@ -11,6 +11,7 @@ import PollButtonContainer from '../containers/poll_button_container';
|
||||||
import UploadButtonContainer from '../containers/upload_button_container';
|
import UploadButtonContainer from '../containers/upload_button_container';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||||
|
import MarkdownButtonContainer from '../containers/markdown_button_container';
|
||||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||||
import PollFormContainer from '../containers/poll_form_container';
|
import PollFormContainer from '../containers/poll_form_container';
|
||||||
|
@ -303,6 +304,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
<PollButtonContainer />
|
<PollButtonContainer />
|
||||||
<PrivacyDropdownContainer />
|
<PrivacyDropdownContainer />
|
||||||
<SpoilerButtonContainer />
|
<SpoilerButtonContainer />
|
||||||
|
<MarkdownButtonContainer />
|
||||||
</div>
|
</div>
|
||||||
{maxTootChars && <div className='character-counter__wrapper'><CharacterCounter max={maxTootChars} text={text} /></div>}
|
{maxTootChars && <div className='character-counter__wrapper'><CharacterCounter max={maxTootChars} text={text} /></div>}
|
||||||
<div className='compose-form__publish'>
|
<div className='compose-form__publish'>
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TextIconButton from '../components/text_icon_button';
|
||||||
|
import { changeComposeContentType } from '../../../actions/compose';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Post markdown enabled' },
|
||||||
|
unmarked: { id: 'compose_form.markdown.unmarked', defaultMessage: 'Post markdown disabled' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { intl }) => ({
|
||||||
|
label: 'MD',
|
||||||
|
title: intl.formatMessage(state.getIn(['compose', 'content_type']) === 'text/markdown' ? messages.marked : messages.unmarked),
|
||||||
|
active: state.getIn(['compose', 'content_type']) === 'text/markdown',
|
||||||
|
ariaControls: 'markdown-input',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onClick() {
|
||||||
|
dispatch(changeComposeContentType(this.active ? 'text/plain' : 'text/markdown'));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
|
|
@ -3,7 +3,7 @@ import PollButton from '../components/poll_button';
|
||||||
import { addPoll, removePoll } from '../../../actions/compose';
|
import { addPoll, removePoll } from '../../../actions/compose';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
unavailable: state.getIn(['compose', 'is_uploading']),
|
||||||
active: state.getIn(['compose', 'poll']) !== null,
|
active: state.getIn(['compose', 'poll']) !== null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
active: state.getIn(['compose', 'sensitive']),
|
active: state.getIn(['compose', 'sensitive']),
|
||||||
|
disabled: state.getIn(['compose', 'spoiler']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
@ -26,12 +27,13 @@ class SensitiveButton extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { active, onClick, intl } = this.props;
|
const { active, disabled, onClick, intl } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='compose-form__sensitive-button'>
|
<div className='compose-form__sensitive-button'>
|
||||||
|
@ -41,6 +43,7 @@ class SensitiveButton extends React.PureComponent {
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={active}
|
checked={active}
|
||||||
onChange={onClick}
|
onChange={onClick}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={classNames('checkbox', { active })} />
|
<span className={classNames('checkbox', { active })} />
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { uploadCompose } from '../../../actions/compose';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
|
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
|
||||||
unavailable: state.getIn(['compose', 'poll']) !== null,
|
|
||||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { acctFull } from 'soapbox/utils/accounts';
|
import { acctFull } from 'soapbox/utils/accounts';
|
||||||
import StillImage from 'soapbox/components/still_image';
|
import StillImage from 'soapbox/components/still_image';
|
||||||
|
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
const ProfilePreview = ({ account }) => (
|
const ProfilePreview = ({ account }) => (
|
||||||
<div className='card h-card'>
|
<div className='card h-card'>
|
||||||
|
@ -16,7 +18,10 @@ const ProfilePreview = ({ account }) => (
|
||||||
<div className='display-name'>
|
<div className='display-name'>
|
||||||
<span style={{ display: 'none' }}>{account.get('username')}</span>
|
<span style={{ display: 'none' }}>{account.get('username')}</span>
|
||||||
<bdi>
|
<bdi>
|
||||||
<strong className='emojify p-name'>{account.get('display_name')}</strong>
|
<strong className='emojify p-name'>
|
||||||
|
{account.get('display_name')}
|
||||||
|
{account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified') && <VerificationBadge />}
|
||||||
|
</strong>
|
||||||
</bdi>
|
</bdi>
|
||||||
<span>{acctFull(account)}</span>
|
<span>{acctFull(account)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { showAlert } from 'soapbox/actions/alerts';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import {
|
import {
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
|
@ -11,6 +12,7 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
FileChooser,
|
FileChooser,
|
||||||
|
SimpleTextarea,
|
||||||
} from 'soapbox/features/forms';
|
} from 'soapbox/features/forms';
|
||||||
import ProfilePreview from './components/profile_preview';
|
import ProfilePreview from './components/profile_preview';
|
||||||
import {
|
import {
|
||||||
|
@ -20,24 +22,24 @@ import {
|
||||||
import { patchMe } from 'soapbox/actions/me';
|
import { patchMe } from 'soapbox/actions/me';
|
||||||
import { unescape } from 'lodash';
|
import { unescape } from 'lodash';
|
||||||
|
|
||||||
const MAX_FIELDS = 4; // TODO: Make this dynamic by the instance
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.edit_profile', defaultMessage: 'Edit profile' },
|
heading: { id: 'column.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
metaFieldLabel: { id: 'edit_profile.fields.meta_fields.label_placeholder', defaultMessage: 'Label' },
|
metaFieldLabel: { id: 'edit_profile.fields.meta_fields.label_placeholder', defaultMessage: 'Label' },
|
||||||
metaFieldContent: { id: 'edit_profile.fields.meta_fields.content_placeholder', defaultMessage: 'Content' },
|
metaFieldContent: { id: 'edit_profile.fields.meta_fields.content_placeholder', defaultMessage: 'Content' },
|
||||||
|
verified: { id: 'edit_profile.fields.verified_display_name', defaultMessage: 'Verified users may not update their display name' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const me = state.get('me');
|
const me = state.get('me');
|
||||||
return {
|
return {
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
|
maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forces fields to be MAX_SIZE, filling empty values
|
// Forces fields to be maxFields size, filling empty values
|
||||||
const normalizeFields = fields => (
|
const normalizeFields = (fields, maxFields) => (
|
||||||
ImmutableList(fields).setSize(MAX_FIELDS).map(field =>
|
ImmutableList(fields).setSize(maxFields).map(field =>
|
||||||
field ? field : ImmutableMap({ name: '', value: '' })
|
field ? field : ImmutableMap({ name: '', value: '' })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -57,11 +59,11 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
|
maxFields: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
fields: normalizeFields(Array.from({ length: MAX_FIELDS })),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -69,8 +71,8 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
const initialState = props.account.withMutations(map => {
|
const initialState = props.account.withMutations(map => {
|
||||||
map.merge(map.get('source'));
|
map.merge(map.get('source'));
|
||||||
map.delete('source');
|
map.delete('source');
|
||||||
map.set('fields', normalizeFields(map.get('fields')));
|
map.set('fields', normalizeFields(map.get('fields'), props.maxFields));
|
||||||
unescapeParams(map, ['display_name', 'note']);
|
unescapeParams(map, ['display_name', 'bio']);
|
||||||
});
|
});
|
||||||
this.state = initialState.toObject();
|
this.state = initialState.toObject();
|
||||||
}
|
}
|
||||||
|
@ -111,8 +113,8 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
const data = this.getParams();
|
const data = this.getParams();
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
for (let key in data) {
|
for (let key in data) {
|
||||||
const shouldAppend = Boolean(data[key]
|
// Compact the submission. This should probably be done better.
|
||||||
|| key.startsWith('fields_attributes'));
|
const shouldAppend = Boolean(data[key] || key.startsWith('fields_attributes'));
|
||||||
if (shouldAppend) formData.append(key, data[key] || '');
|
if (shouldAppend) formData.append(key, data[key] || '');
|
||||||
}
|
}
|
||||||
return formData;
|
return formData;
|
||||||
|
@ -122,6 +124,7 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
dispatch(patchMe(this.getFormdata())).then(() => {
|
dispatch(patchMe(this.getFormdata())).then(() => {
|
||||||
this.setState({ isLoading: false });
|
this.setState({ isLoading: false });
|
||||||
|
dispatch(showAlert('', 'Profile saved!'));
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.setState({ isLoading: false });
|
this.setState({ isLoading: false });
|
||||||
});
|
});
|
||||||
|
@ -157,7 +160,8 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl } = this.props;
|
const { intl, maxFields, account } = this.props;
|
||||||
|
const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column icon='user' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
<Column icon='user' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||||
|
@ -165,16 +169,22 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
<fieldset disabled={this.state.isLoading}>
|
<fieldset disabled={this.state.isLoading}>
|
||||||
<FieldsGroup>
|
<FieldsGroup>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
className={verified ? 'disabled' : ''}
|
||||||
label={<FormattedMessage id='edit_profile.fields.display_name_label' defaultMessage='Display name' />}
|
label={<FormattedMessage id='edit_profile.fields.display_name_label' defaultMessage='Display name' />}
|
||||||
name='display_name'
|
name='display_name'
|
||||||
value={this.state.display_name}
|
value={this.state.display_name}
|
||||||
onChange={this.handleTextChange}
|
onChange={this.handleTextChange}
|
||||||
|
disabled={verified}
|
||||||
|
hint={verified && intl.formatMessage(messages.verified)}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<SimpleTextarea
|
||||||
label={<FormattedMessage id='edit_profile.fields.bio_label' defaultMessage='Bio' />}
|
label={<FormattedMessage id='edit_profile.fields.bio_label' defaultMessage='Bio' />}
|
||||||
name='note'
|
name='note'
|
||||||
|
autoComplete='off'
|
||||||
value={this.state.note}
|
value={this.state.note}
|
||||||
|
wrap='hard'
|
||||||
onChange={this.handleTextChange}
|
onChange={this.handleTextChange}
|
||||||
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className='fields-row'>
|
<div className='fields-row'>
|
||||||
<div className='fields-row__column fields-row__column-6'>
|
<div className='fields-row__column fields-row__column-6'>
|
||||||
|
@ -215,7 +225,7 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
<div className='input with_block_label'>
|
<div className='input with_block_label'>
|
||||||
<label><FormattedMessage id='edit_profile.fields.meta_fields_label' defaultMessage='Profile metadata' /></label>
|
<label><FormattedMessage id='edit_profile.fields.meta_fields_label' defaultMessage='Profile metadata' /></label>
|
||||||
<span className='hint'>
|
<span className='hint'>
|
||||||
<FormattedMessage id='edit_profile.hints.meta_fields' defaultMessage='You can have up to {count, plural, one {# item} other {# items}} displayed as a table on your profile' values={{ count: MAX_FIELDS }} />
|
<FormattedMessage id='edit_profile.hints.meta_fields' defaultMessage='You can have up to {count, plural, one {# item} other {# items}} displayed as a table on your profile' values={{ count: maxFields }} />
|
||||||
</span>
|
</span>
|
||||||
{
|
{
|
||||||
this.state.fields.map((field, i) => (
|
this.state.fields.map((field, i) => (
|
||||||
|
|
|
@ -4,16 +4,55 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import { fetchFilters } from '../../actions/filters';
|
import { fetchFilters, createFilter, deleteFilter } from '../../actions/filters';
|
||||||
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
|
import Button from 'soapbox/components/button';
|
||||||
|
import {
|
||||||
|
SimpleForm,
|
||||||
|
SimpleInput,
|
||||||
|
FieldsGroup,
|
||||||
|
SelectDropdown,
|
||||||
|
Checkbox,
|
||||||
|
} from 'soapbox/features/forms';
|
||||||
|
import { showAlert } from 'soapbox/actions/alerts';
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import ColumnSubheading from '../ui/components/column_subheading';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
|
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
|
||||||
|
subheading_add_new: { id: 'column.filters.subheading_add_new', defaultMessage: 'Add New Filter' },
|
||||||
|
keyword: { id: 'column.filters.keyword', defaultMessage: 'Keyword or phrase' },
|
||||||
|
expires: { id: 'column.filters.expires', defaultMessage: 'Expire after' },
|
||||||
|
expires_hint: { id: 'column.filters.expires_hint', defaultMessage: 'Expiration dates are not currently supported' },
|
||||||
|
home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
|
||||||
|
public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
|
||||||
|
notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
|
||||||
|
conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
|
||||||
|
drop_header: { id: 'column.filters.drop_header', defaultMessage: 'Drop instead of hide' },
|
||||||
|
drop_hint: { id: 'column.filters.drop_hint', defaultMessage: 'Filtered posts will disappear irreversibly, even if filter is later removed' },
|
||||||
|
whole_word_header: { id: 'column.filters.whole_word_header', defaultMessage: 'Whole word' },
|
||||||
|
whole_word_hint: { id: 'column.filters.whole_word_hint', defaultMessage: 'When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word' },
|
||||||
|
add_new: { id: 'column.filters.add_new', defaultMessage: 'Add New Filter' },
|
||||||
|
create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
|
||||||
|
delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' },
|
||||||
|
subheading_filters: { id: 'column.filters.subheading_filters', defaultMessage: 'Current Filters' },
|
||||||
|
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expirations = {
|
||||||
|
null: 'Never',
|
||||||
|
// 3600: '30 minutes',
|
||||||
|
// 21600: '1 hour',
|
||||||
|
// 43200: '12 hours',
|
||||||
|
// 86400 : '1 day',
|
||||||
|
// 604800: '1 week',
|
||||||
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
filters: state.get('filters'),
|
filters: state.get('filters'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class Filters extends ImmutablePureComponent {
|
class Filters extends ImmutablePureComponent {
|
||||||
|
@ -24,17 +63,206 @@ class Filters extends ImmutablePureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
phrase: '',
|
||||||
|
expires_at: '',
|
||||||
|
home_timeline: true,
|
||||||
|
public_timeline: false,
|
||||||
|
notifications: false,
|
||||||
|
conversations: false,
|
||||||
|
irreversible: false,
|
||||||
|
whole_word: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.dispatch(fetchFilters());
|
this.props.dispatch(fetchFilters());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInputChange = e => {
|
||||||
|
this.setState({ [e.target.name]: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectChange = e => {
|
||||||
|
this.setState({ [e.target.name]: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckboxChange = e => {
|
||||||
|
this.setState({ [e.target.name]: e.target.checked });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddNew = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { intl, dispatch } = this.props;
|
||||||
|
const { phrase, whole_word, expires_at, irreversible } = this.state;
|
||||||
|
const { home_timeline, public_timeline, notifications, conversations } = this.state;
|
||||||
|
let context = [];
|
||||||
|
|
||||||
|
if (home_timeline) {
|
||||||
|
context.push('home');
|
||||||
|
};
|
||||||
|
if (public_timeline) {
|
||||||
|
context.push('public');
|
||||||
|
};
|
||||||
|
if (notifications) {
|
||||||
|
context.push('notifications');
|
||||||
|
};
|
||||||
|
if (conversations) {
|
||||||
|
context.push('thread');
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(createFilter(phrase, expires_at, context, whole_word, irreversible)).then(response => {
|
||||||
|
return dispatch(fetchFilters());
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.create_error)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterDelete = e => {
|
||||||
|
const { intl, dispatch } = this.props;
|
||||||
|
dispatch(deleteFilter(e.currentTarget.dataset.value)).then(response => {
|
||||||
|
return dispatch(fetchFilters());
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.delete_error)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl } = this.props;
|
const { intl, filters } = this.props;
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
|
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column icon='filter' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
<Column className='filter-settings-panel' icon='filter' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||||
{emptyMessage}
|
<ColumnSubheading text={intl.formatMessage(messages.subheading_add_new)} />
|
||||||
|
<SimpleForm>
|
||||||
|
<div className='filter-settings-panel'>
|
||||||
|
<fieldset disabled={false}>
|
||||||
|
<FieldsGroup>
|
||||||
|
<div className='two-col'>
|
||||||
|
<SimpleInput
|
||||||
|
label={intl.formatMessage(messages.keyword)}
|
||||||
|
required
|
||||||
|
type='text'
|
||||||
|
name='phrase'
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
|
<div className='input with_label required'>
|
||||||
|
<SelectDropdown
|
||||||
|
label={intl.formatMessage(messages.expires)}
|
||||||
|
hint={intl.formatMessage(messages.expires_hint)}
|
||||||
|
items={expirations}
|
||||||
|
defaultValue={expirations.never}
|
||||||
|
onChange={this.handleSelectChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FieldsGroup>
|
||||||
|
|
||||||
|
<FieldsGroup>
|
||||||
|
<label className='checkboxes required'>
|
||||||
|
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
|
||||||
|
</label>
|
||||||
|
<span className='hint'>
|
||||||
|
<FormattedMessage id='filters.context_hint' defaultMessage='One or multiple contexts where the filter should apply' />
|
||||||
|
</span>
|
||||||
|
<div className='two-col'>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.home_timeline)}
|
||||||
|
name='home_timeline'
|
||||||
|
checked={this.state.home_timeline}
|
||||||
|
onChange={this.handleCheckboxChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.public_timeline)}
|
||||||
|
name='public_timeline'
|
||||||
|
checked={this.state.public_timeline}
|
||||||
|
onChange={this.handleCheckboxChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.notifications)}
|
||||||
|
name='notifications'
|
||||||
|
checked={this.state.notifications}
|
||||||
|
onChange={this.handleCheckboxChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.conversations)}
|
||||||
|
name='conversations'
|
||||||
|
checked={this.state.conversations}
|
||||||
|
onChange={this.handleCheckboxChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</FieldsGroup>
|
||||||
|
|
||||||
|
<FieldsGroup>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.drop_header)}
|
||||||
|
hint={intl.formatMessage(messages.drop_hint)}
|
||||||
|
name='irreversible'
|
||||||
|
checked={this.state.irreversible}
|
||||||
|
onChange={this.handleCheckboxChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={intl.formatMessage(messages.whole_word_header)}
|
||||||
|
hint={intl.formatMessage(messages.whole_word_hint)}
|
||||||
|
name='whole_word'
|
||||||
|
checked={this.state.whole_word}
|
||||||
|
onChange={this.handleCheckboxChange}
|
||||||
|
/>
|
||||||
|
</FieldsGroup>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<Button className='button button-primary setup' text={intl.formatMessage(messages.add_new)} onClick={this.handleAddNew} />
|
||||||
|
|
||||||
|
<ColumnSubheading text={intl.formatMessage(messages.subheading_filters)} />
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='filters'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
>
|
||||||
|
{filters.map((filter, i) => (
|
||||||
|
<div key={i} className='filter__container'>
|
||||||
|
<div className='filter__details'>
|
||||||
|
<div className='filter__phrase'>
|
||||||
|
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' /></span>
|
||||||
|
<span className='filter__list-value'>{filter.get('phrase')}</span>
|
||||||
|
</div>
|
||||||
|
<div className='filter__contexts'>
|
||||||
|
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' /></span>
|
||||||
|
<span className='filter__list-value'>
|
||||||
|
{filter.get('context').map((context, i) => (
|
||||||
|
<span key={i} className='context'>{context}</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='filter__details'>
|
||||||
|
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_details_label' defaultMessage='Filter settings:' /></span>
|
||||||
|
<span className='filter__list-value'>
|
||||||
|
{filter.get('irreversible') ?
|
||||||
|
<span><FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /></span> :
|
||||||
|
<span><FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' /></span>
|
||||||
|
}
|
||||||
|
{filter.get('whole_word') &&
|
||||||
|
<span><FormattedMessage id='filters.filters_list_whole-word' defaultMessage='Whole word' /></span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='filter__delete' role='button' tabIndex='0' onClick={this.handleFilterDelete} data-value={filter.get('id')} aria-label={intl.formatMessage(messages.delete)}>
|
||||||
|
<Icon className='filter__delete-icon' id='times' size={40} />
|
||||||
|
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ InputContainer.propTypes = {
|
||||||
extraClass: PropTypes.string,
|
extraClass: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LabelInputContainer = ({ label, children, ...props }) => {
|
export const LabelInputContainer = ({ label, hint, children, ...props }) => {
|
||||||
const [id] = useState(uuidv4());
|
const [id] = useState(uuidv4());
|
||||||
const childrenWithProps = React.Children.map(children, child => (
|
const childrenWithProps = React.Children.map(children, child => (
|
||||||
React.cloneElement(child, { id: id, key: id })
|
React.cloneElement(child, { id: id, key: id })
|
||||||
|
@ -54,12 +54,14 @@ export const LabelInputContainer = ({ label, children, ...props }) => {
|
||||||
<div className='label_input__wrapper'>
|
<div className='label_input__wrapper'>
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
</div>
|
</div>
|
||||||
|
{hint && <span className='hint'>{hint}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
LabelInputContainer.propTypes = {
|
LabelInputContainer.propTypes = {
|
||||||
label: FormPropTypes.label.isRequired,
|
label: FormPropTypes.label.isRequired,
|
||||||
|
hint: PropTypes.node,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -74,6 +76,17 @@ LabelInput.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LabelTextarea = ({ label, dispatch, ...props }) => (
|
||||||
|
<LabelInputContainer label={label}>
|
||||||
|
<textarea {...props} />
|
||||||
|
</LabelInputContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
LabelTextarea.propTypes = {
|
||||||
|
label: FormPropTypes.label.isRequired,
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
export class SimpleInput extends ImmutablePureComponent {
|
export class SimpleInput extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -94,6 +107,26 @@ export class SimpleInput extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SimpleTextarea extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
label: FormPropTypes.label,
|
||||||
|
hint: PropTypes.node,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { hint, ...props } = this.props;
|
||||||
|
const Input = this.props.label ? LabelTextarea : 'textarea';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputContainer {...this.props}>
|
||||||
|
<Input {...props} />
|
||||||
|
</InputContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export class SimpleForm extends ImmutablePureComponent {
|
export class SimpleForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -290,11 +323,12 @@ export class SelectDropdown extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
label: FormPropTypes.label,
|
label: FormPropTypes.label,
|
||||||
|
hint: PropTypes.node,
|
||||||
items: PropTypes.object.isRequired,
|
items: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { label, items, ...props } = this.props;
|
const { label, hint, items, ...props } = this.props;
|
||||||
|
|
||||||
const optionElems = Object.keys(items).map(item => (
|
const optionElems = Object.keys(items).map(item => (
|
||||||
<option key={item} value={item}>{items[item]}</option>
|
<option key={item} value={item}>{items[item]}</option>
|
||||||
|
@ -303,7 +337,7 @@ export class SelectDropdown extends ImmutablePureComponent {
|
||||||
const selectElem = <select {...props}>{optionElems}</select>;
|
const selectElem = <select {...props}>{optionElems}</select>;
|
||||||
|
|
||||||
return label ? (
|
return label ? (
|
||||||
<LabelInputContainer label={label}>{selectElem}</LabelInputContainer>
|
<LabelInputContainer label={label} hint={hint}>{selectElem}</LabelInputContainer>
|
||||||
) : selectElem;
|
) : selectElem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,6 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />} />
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,6 +25,10 @@ class ColumnSettings extends React.PureComponent {
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'direct']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_direct' defaultMessage='Show direct messages' />} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
SimpleInput,
|
SimpleInput,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
SimpleTextarea,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
} from 'soapbox/features/forms';
|
} from 'soapbox/features/forms';
|
||||||
import { register } from 'soapbox/actions/auth';
|
import { register } from 'soapbox/actions/auth';
|
||||||
|
@ -136,6 +137,16 @@ class RegistrationForm extends ImmutablePureComponent {
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
{instance.get('approval_required') &&
|
||||||
|
<SimpleTextarea
|
||||||
|
label={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
|
||||||
|
hint={<FormattedMessage id='registration.reason_hint' defaultMessage='This will help us review your application' />}
|
||||||
|
name='reason'
|
||||||
|
maxLength={500}
|
||||||
|
autoComplete='off'
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
required
|
||||||
|
/>}
|
||||||
</div>
|
</div>
|
||||||
<CaptchaField
|
<CaptchaField
|
||||||
onFetch={this.onFetchCaptcha}
|
onFetch={this.onFetchCaptcha}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ClearColumnButton from './clear_column_button';
|
import ClearColumnButton from './clear_column_button';
|
||||||
import SettingToggle from './setting_toggle';
|
import SettingToggle from './setting_toggle';
|
||||||
|
import MultiSettingToggle from './multi_setting_toggle';
|
||||||
|
|
||||||
export default class ColumnSettings extends React.PureComponent {
|
export default class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -18,15 +19,24 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
this.props.onChange(['push', ...path], checked);
|
this.props.onChange(['push', ...path], checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAllSoundsChange = (path, checked) => {
|
||||||
|
const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll']];
|
||||||
|
|
||||||
|
for (var i = 0; i < soundSettings.length; i++) {
|
||||||
|
this.props.onChange(soundSettings[i], checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { settings, pushSettings, onChange, onClear } = this.props;
|
const { settings, pushSettings, onChange, onClear } = this.props;
|
||||||
|
|
||||||
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
||||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||||
|
const allSoundsStr = <FormattedMessage id='notifications.column_settings.sounds.all_sounds' defaultMessage='Play sound for all notifications' />;
|
||||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||||
|
const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll']];
|
||||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||||
|
|
||||||
|
@ -36,11 +46,19 @@ export default class ColumnSettings extends React.PureComponent {
|
||||||
<ClearColumnButton onClick={onClear} />
|
<ClearColumnButton onClick={onClear} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role='group' aria-labelledby='notifications-all_sounds'>
|
||||||
|
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||||
|
<FormattedMessage id='notifications.column_settings.sounds' defaultMessage='Sounds' />
|
||||||
|
</span>
|
||||||
|
<MultiSettingToggle prefix='notifications_all_sounds' settings={settings} settingPaths={soundSettings} onChange={this.onAllSoundsChange} label={allSoundsStr} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
<div role='group' aria-labelledby='notifications-filter-bar'>
|
||||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||||
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
|
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
|
||||||
</span>
|
</span>
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
export default class MultiSettingToggle extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
prefix: PropTypes.string,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
settingPaths: PropTypes.array.isRequired,
|
||||||
|
label: PropTypes.node,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
icons: PropTypes.oneOfType([
|
||||||
|
PropTypes.bool,
|
||||||
|
PropTypes.object,
|
||||||
|
]),
|
||||||
|
ariaLabel: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = ({ target }) => {
|
||||||
|
for (var i = 0; i < this.props.settingPaths.length; i++) {
|
||||||
|
this.props.onChange(this.props.settingPaths[i], target.checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
areTrue = (settingPath) => {
|
||||||
|
return this.props.settings.getIn(settingPath) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { prefix, settingPaths, label, icons, ariaLabel } = this.props;
|
||||||
|
const id = ['setting-toggle', prefix].filter(Boolean).join('-');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='setting-toggle' aria-label={ariaLabel}>
|
||||||
|
<Toggle id={id} checked={settingPaths.every(this.areTrue)} onChange={this.onChange} icons={icons} onKeyDown={this.onKeyDown} />
|
||||||
|
{label && (<label htmlFor={id} className='setting-toggle__label'>{label}</label>)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetAccount } from '../../selectors';
|
||||||
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import UserPanel from '../ui/components/user_panel';
|
||||||
|
import ActionButton from '../ui/components/action_button';
|
||||||
|
import { isAdmin, isModerator } from 'soapbox/utils/accounts';
|
||||||
|
import Badge from 'soapbox/components/badge';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { accountId }) => {
|
||||||
|
return {
|
||||||
|
account: getAccount(state, accountId),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class ProfileHoverCardContainer extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
visible: PropTypes.bool,
|
||||||
|
accountId: PropTypes.string,
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
visible: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
getBadges = () => {
|
||||||
|
const { account } = this.props;
|
||||||
|
let badges = [];
|
||||||
|
if (isAdmin(account)) badges.push(<Badge key='admin' slug='admin' title='Admin' />);
|
||||||
|
if (isModerator(account)) badges.push(<Badge key='moderator' slug='moderator' title='Moderator' />);
|
||||||
|
if (account.getIn(['patron', 'is_patron'])) badges.push(<Badge key='patron' slug='patron' title='Patron' />);
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { visible, accountId, account } = this.props;
|
||||||
|
if (!accountId) return null;
|
||||||
|
const accountBio = { __html: account.get('note_emojified') };
|
||||||
|
const followedBy = account.getIn(['relationship', 'followed_by']);
|
||||||
|
const badges = this.getBadges();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('profile-hover-card', { 'profile-hover-card--visible': visible })}>
|
||||||
|
<div className='profile-hover-card__container'>
|
||||||
|
{followedBy &&
|
||||||
|
<span className='relationship-tag'>
|
||||||
|
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||||
|
</span>}
|
||||||
|
<div className='profile-hover-card__action-button'><ActionButton account={account} /></div>
|
||||||
|
<UserPanel className='profile-hover-card__user' accountId={accountId} />
|
||||||
|
{badges.length > 0 &&
|
||||||
|
<div className='profile-hover-card__badges'>
|
||||||
|
{badges}
|
||||||
|
</div>}
|
||||||
|
{account.getIn(['source', 'note'], '').length > 0 &&
|
||||||
|
<div className='profile-hover-card__bio' dangerouslySetInnerHTML={accountBio} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -6,25 +6,85 @@ import { Link } from 'react-router-dom';
|
||||||
import LoginForm from 'soapbox/features/auth_login/components/login_form';
|
import LoginForm from 'soapbox/features/auth_login/components/login_form';
|
||||||
import SiteLogo from './site_logo';
|
import SiteLogo from './site_logo';
|
||||||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||||
|
import { logIn } from 'soapbox/actions/auth';
|
||||||
|
import { fetchMe } from 'soapbox/actions/me';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import OtpAuthForm from 'soapbox/features/auth_login/components/otp_auth_form';
|
||||||
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
me: state.get('me'),
|
me: state.get('me'),
|
||||||
instance: state.get('instance'),
|
instance: state.get('instance'),
|
||||||
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
class Header extends ImmutablePureComponent {
|
class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormData = (form) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Array.from(form).map(i => [i.name, i.value])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = (event) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const { username, password } = this.getFormData(event.target);
|
||||||
|
dispatch(logIn(username, password)).then(() => {
|
||||||
|
return dispatch(fetchMe());
|
||||||
|
}).catch(error => {
|
||||||
|
if (error.response.data.error === 'mfa_required') {
|
||||||
|
this.setState({ mfa_auth_needed: true, mfa_token: error.response.data.mfa_token });
|
||||||
|
}
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
});
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickClose = (event) => {
|
||||||
|
this.setState({ mfa_auth_needed: false, mfa_token: '' });
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
me: SoapboxPropTypes.me,
|
me: SoapboxPropTypes.me,
|
||||||
instance: ImmutablePropTypes.map,
|
instance: ImmutablePropTypes.map,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
mfa_auth_needed: false,
|
||||||
|
mfa_token: '',
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { me, instance } = this.props;
|
const { me, instance, isLoading, intl } = this.props;
|
||||||
|
const { mfa_auth_needed, mfa_token } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='header'>
|
<nav className='header'>
|
||||||
|
{ mfa_auth_needed &&
|
||||||
|
<div className='otp-form-overlay__container'>
|
||||||
|
<div className='otp-form-overlay__form'>
|
||||||
|
<IconButton className='otp-form-overlay__close' title={intl.formatMessage(messages.close)} icon='times' onClick={this.onClickClose} size={20} />
|
||||||
|
<OtpAuthForm mfa_token={mfa_token} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div className='header-container'>
|
<div className='header-container'>
|
||||||
<div className='nav-left'>
|
<div className='nav-left'>
|
||||||
<Link className='brand' to='/'>
|
<Link className='brand' to='/'>
|
||||||
|
@ -38,7 +98,7 @@ class Header extends ImmutablePureComponent {
|
||||||
<div className='hidden-sm'>
|
<div className='hidden-sm'>
|
||||||
{me
|
{me
|
||||||
? <Link className='nav-link nav-button webapp-btn' to='/'>Back to {instance.get('title')}</Link>
|
? <Link className='nav-link nav-button webapp-btn' to='/'>Back to {instance.get('title')}</Link>
|
||||||
: <LoginForm />
|
: <LoginForm handleSubmit={this.handleSubmit} isLoading={isLoading} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className='visible-sm'>
|
<div className='visible-sm'>
|
||||||
|
|
|
@ -18,6 +18,14 @@ class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle prefix='public_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='public_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
import Button from 'soapbox/components/button';
|
||||||
import {
|
import {
|
||||||
SimpleForm,
|
SimpleForm,
|
||||||
SimpleInput,
|
SimpleInput,
|
||||||
|
@ -18,7 +19,9 @@ import {
|
||||||
revokeOAuthToken,
|
revokeOAuthToken,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
} from 'soapbox/actions/auth';
|
} from 'soapbox/actions/auth';
|
||||||
|
import { fetchUserMfaSettings } from '../../actions/mfa';
|
||||||
import { showAlert } from 'soapbox/actions/alerts';
|
import { showAlert } from 'soapbox/actions/alerts';
|
||||||
|
import { changeSetting, getSettings } from 'soapbox/actions/settings';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Security settings page for user account
|
Security settings page for user account
|
||||||
|
@ -51,9 +54,22 @@ const messages = defineMessages({
|
||||||
deleteSubmit: { id: 'security.submit.delete', defaultMessage: 'Delete Account' },
|
deleteSubmit: { id: 'security.submit.delete', defaultMessage: 'Delete Account' },
|
||||||
deleteAccountSuccess: { id: 'security.delete_account.success', defaultMessage: 'Account successfully deleted.' },
|
deleteAccountSuccess: { id: 'security.delete_account.success', defaultMessage: 'Account successfully deleted.' },
|
||||||
deleteAccountFail: { id: 'security.delete_account.fail', defaultMessage: 'Account deletion failed.' },
|
deleteAccountFail: { id: 'security.delete_account.fail', defaultMessage: 'Account deletion failed.' },
|
||||||
|
mfa: { id: 'security.mfa', defaultMessage: 'Set up 2-Factor Auth' },
|
||||||
|
mfa_setup_hint: { id: 'security.mfa_setup_hint', defaultMessage: 'Configure multi-factor authentication with OTP' },
|
||||||
|
mfa_enabled: { id: 'security.mfa_enabled', defaultMessage: 'You have multi-factor authentication set up with OTP.' },
|
||||||
|
disable_mfa: { id: 'security.disable_mfa', defaultMessage: 'Disable' },
|
||||||
|
mfaHeader: { id: 'security.mfa_header', defaultMessage: 'Authorization Methods' },
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
const mapStateToProps = state => ({
|
||||||
|
backup_codes: state.getIn(['auth', 'backup_codes', 'codes']),
|
||||||
|
settings: getSettings(state),
|
||||||
|
tokens: state.getIn(['auth', 'tokens']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
class SecurityForm extends ImmutablePureComponent {
|
class SecurityForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -68,6 +84,7 @@ class SecurityForm extends ImmutablePureComponent {
|
||||||
<Column icon='lock' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
<Column icon='lock' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||||
<ChangeEmailForm />
|
<ChangeEmailForm />
|
||||||
<ChangePasswordForm />
|
<ChangePasswordForm />
|
||||||
|
<SetUpMfa />
|
||||||
<AuthTokenList />
|
<AuthTokenList />
|
||||||
<DeactivateAccount />
|
<DeactivateAccount />
|
||||||
</Column>
|
</Column>
|
||||||
|
@ -227,9 +244,56 @@ class ChangePasswordForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
@connect(mapStateToProps)
|
||||||
tokens: state.getIn(['auth', 'tokens']),
|
@injectIntl
|
||||||
});
|
class SetUpMfa extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.props.dispatch(fetchUserMfaSettings()).then(response => {
|
||||||
|
this.props.dispatch(changeSetting(['otpEnabled'], response.data.settings.enabled));
|
||||||
|
}).catch(e => e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMfaClick = e => {
|
||||||
|
this.context.router.history.push('../auth/mfa');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, settings } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<h2>{intl.formatMessage(messages.mfaHeader)}</h2>
|
||||||
|
{ settings.get('otpEnabled') === false ?
|
||||||
|
<div>
|
||||||
|
<p className='hint'>
|
||||||
|
{intl.formatMessage(messages.mfa_setup_hint)}
|
||||||
|
</p>
|
||||||
|
<Button className='button button-secondary set-up-mfa' text={intl.formatMessage(messages.mfa)} onClick={this.handleMfaClick} />
|
||||||
|
</div> :
|
||||||
|
<div>
|
||||||
|
<p className='hint'>
|
||||||
|
{intl.formatMessage(messages.mfa_enabled)}
|
||||||
|
</p>
|
||||||
|
<Button className='button button--destructive disable-mfa' text={intl.formatMessage(messages.disable_mfa)} onClick={this.handleMfaClick} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
|
|
|
@ -0,0 +1,333 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import QRCode from 'qrcode.react';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
import ColumnSubheading from '../ui/components/column_subheading';
|
||||||
|
import LoadingIndicator from 'soapbox/components/loading_indicator';
|
||||||
|
import Button from 'soapbox/components/button';
|
||||||
|
import { changeSetting, getSettings } from 'soapbox/actions/settings';
|
||||||
|
import { showAlert } from 'soapbox/actions/alerts';
|
||||||
|
import {
|
||||||
|
SimpleForm,
|
||||||
|
SimpleInput,
|
||||||
|
FieldsGroup,
|
||||||
|
TextInput,
|
||||||
|
} from 'soapbox/features/forms';
|
||||||
|
import {
|
||||||
|
fetchBackupCodes,
|
||||||
|
fetchToptSetup,
|
||||||
|
confirmToptSetup,
|
||||||
|
fetchUserMfaSettings,
|
||||||
|
disableToptSetup,
|
||||||
|
} from '../../actions/mfa';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Security settings page for user account
|
||||||
|
Routed to /auth/mfa
|
||||||
|
Includes following features:
|
||||||
|
- Set up Multi-factor Auth
|
||||||
|
*/
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.security', defaultMessage: 'Security' },
|
||||||
|
subheading: { id: 'column.mfa', defaultMessage: 'Multi-Factor Authentication' },
|
||||||
|
mfa_cancel_button: { id: 'column.mfa_cancel', defaultMessage: 'Cancel' },
|
||||||
|
mfa_setup_button: { id: 'column.mfa_setup', defaultMessage: 'Proceed to Setup' },
|
||||||
|
mfa_setup_confirm_button: { id: 'column.mfa_confirm_button', defaultMessage: 'Confirm' },
|
||||||
|
mfa_setup_disable_button: { id: 'column.mfa_disable_button', defaultMessage: 'Disable' },
|
||||||
|
passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' },
|
||||||
|
confirmFail: { id: 'security.confirm.fail', defaultMessage: 'Incorrect code or password. Try again.' },
|
||||||
|
qrFail: { id: 'security.qr.fail', defaultMessage: 'Failed to fetch setup key' },
|
||||||
|
codesFail: { id: 'security.codes.fail', defaultMessage: 'Failed to fetch backup codes' },
|
||||||
|
disableFail: { id: 'security.disable.fail', defaultMessage: 'Incorrect password. Try again.' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
backup_codes: state.getIn(['auth', 'backup_codes', 'codes']),
|
||||||
|
settings: getSettings(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class MfaForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.props.dispatch(fetchUserMfaSettings()).then(response => {
|
||||||
|
this.props.dispatch(changeSetting(['otpEnabled'], response.data.settings.enabled));
|
||||||
|
// this.setState({ otpEnabled: response.data.settings.enabled });
|
||||||
|
}).catch(e => e);
|
||||||
|
this.handleSetupProceedClick = this.handleSetupProceedClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
displayOtpForm: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSetupProceedClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({ displayOtpForm: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl, settings } = this.props;
|
||||||
|
const { displayOtpForm } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column icon='lock' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||||
|
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||||
|
{ settings.get('otpEnabled') === true && <DisableOtpForm />}
|
||||||
|
{ settings.get('otpEnabled') === false && <EnableOtpForm handleSetupProceedClick={this.handleSetupProceedClick} />}
|
||||||
|
{ settings.get('otpEnabled') === false && displayOtpForm && <OtpConfirmForm /> }
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
class DisableOtpForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = e => {
|
||||||
|
this.setState({ [e.target.name]: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOtpDisableClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { password } = this.state;
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(disableToptSetup(password)).then(response => {
|
||||||
|
this.context.router.history.push('../auth/edit');
|
||||||
|
dispatch(changeSetting(['otpEnabled'], false));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.disableFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<div className='security-settings-panel'>
|
||||||
|
<h1 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.otp_enabled_title' defaultMessage='OTP Enabled' />
|
||||||
|
</h1>
|
||||||
|
<div><FormattedMessage id='mfa.otp_enabled_description' defaultMessage='You have enabled two-factor authentication via OTP.' /></div>
|
||||||
|
<div><FormattedMessage id='mfa.mfa_disable_enter_password' defaultMessage='Enter your current password to disable two-factor auth:' /></div>
|
||||||
|
<SimpleInput
|
||||||
|
type='password'
|
||||||
|
name='password'
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
|
<Button className='button button-primary disable' text={intl.formatMessage(messages.mfa_setup_disable_button)} onClick={this.handleOtpDisableClick} />
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
class EnableOtpForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
backupCodes: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(fetchBackupCodes()).then(response => {
|
||||||
|
this.setState({ backupCodes: response.data.codes });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.codesFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelClick = e => {
|
||||||
|
this.context.router.history.push('../auth/edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const { backupCodes, displayOtpForm } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<div className='security-settings-panel'>
|
||||||
|
<h1 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.setup_otp_title' defaultMessage='OTP Disabled' />
|
||||||
|
</h1>
|
||||||
|
<h2 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.setup_hint' defaultMessage='Follow these steps to set up multi-factor authentication on your account with OTP' />
|
||||||
|
</h2>
|
||||||
|
<div className='security-warning'>
|
||||||
|
<FormattedMessage id='mfa.setup_warning' defaultMessage="Write these codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account." />
|
||||||
|
</div>
|
||||||
|
<h2 className='security-settings-panel__setup-otp'>
|
||||||
|
<FormattedMessage id='mfa.setup_recoverycodes' defaultMessage='Recovery codes' />
|
||||||
|
</h2>
|
||||||
|
<div className='backup_codes'>
|
||||||
|
{ backupCodes.length ?
|
||||||
|
<div>
|
||||||
|
{backupCodes.map((code, i) => (
|
||||||
|
<div key={i} className='backup_code'>
|
||||||
|
<div className='backup_code'>{code}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div> :
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{ !displayOtpForm &&
|
||||||
|
<div className='security-settings-panel__setup-otp__buttons'>
|
||||||
|
<Button className='button button-secondary cancel' text={intl.formatMessage(messages.mfa_cancel_button)} onClick={this.handleCancelClick} />
|
||||||
|
{ backupCodes.length ?
|
||||||
|
<Button className='button button-primary setup' text={intl.formatMessage(messages.mfa_setup_button)} onClick={this.props.handleSetupProceedClick} /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
class OtpConfirmForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
password: '',
|
||||||
|
done: false,
|
||||||
|
code: '',
|
||||||
|
qrCodeURI: '',
|
||||||
|
confirm_key: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(fetchToptSetup()).then(response => {
|
||||||
|
this.setState({ qrCodeURI: response.data.provisioning_uri, confirm_key: response.data.key });
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.qrFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = e => {
|
||||||
|
this.setState({ [e.target.name]: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOtpConfirmClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { code, password } = this.state;
|
||||||
|
const { dispatch, intl } = this.props;
|
||||||
|
dispatch(confirmToptSetup(code, password)).then(response => {
|
||||||
|
dispatch(changeSetting(['otpEnabled'], true));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(showAlert('', intl.formatMessage(messages.confirmFail)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { intl } = this.props;
|
||||||
|
const { qrCodeURI, confirm_key } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleForm>
|
||||||
|
<div className='security-settings-panel'>
|
||||||
|
|
||||||
|
<fieldset disabled={false}>
|
||||||
|
<FieldsGroup>
|
||||||
|
<div className='security-settings-panel__section-container'>
|
||||||
|
<h2><FormattedMessage id='mfa.mfa_setup_scan_title' defaultMessage='Scan' /></h2>
|
||||||
|
|
||||||
|
<div><FormattedMessage id='mfa.mfa_setup_scan_description' defaultMessage='Using your two-factor app, scan this QR code or enter text key:' /></div>
|
||||||
|
|
||||||
|
<span className='security-settings-panel qr-code'>
|
||||||
|
<QRCode value={qrCodeURI} />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className='security-settings-panel confirm-key'><FormattedMessage id='mfa.mfa_setup_scan_key' defaultMessage='Key:' /> {confirm_key}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='security-settings-panel__section-container'>
|
||||||
|
<h2><FormattedMessage id='mfa.mfa_setup_verify_title' defaultMessage='Verify' /></h2>
|
||||||
|
|
||||||
|
<div><FormattedMessage id='mfa.mfa_setup_verify_description' defaultMessage='To enable two-factor authentication, enter the code from your two-factor app:' /></div>
|
||||||
|
<TextInput
|
||||||
|
name='code'
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div><FormattedMessage id='mfa.mfa_setup_enter_password' defaultMessage='Enter your current password to confirm your identity:' /></div>
|
||||||
|
<SimpleInput
|
||||||
|
type='password'
|
||||||
|
name='password'
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FieldsGroup>
|
||||||
|
</fieldset>
|
||||||
|
<div className='security-settings-panel__setup-otp__buttons'>
|
||||||
|
<Button className='button button-secondary cancel' text={intl.formatMessage(messages.mfa_cancel_button)} onClick={this.handleCancelClick} />
|
||||||
|
<Button className='button button-primary setup' text={intl.formatMessage(messages.mfa_setup_confirm_button)} onClick={this.handleOtpConfirmClick} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SimpleForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -311,7 +311,9 @@ class ActionBar extends React.PureComponent {
|
||||||
onMouseLeave={this.handleLikeButtonLeave}
|
onMouseLeave={this.handleLikeButtonLeave}
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
>
|
>
|
||||||
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
{ emojiSelectorVisible &&
|
||||||
|
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
||||||
|
}
|
||||||
<IconButton
|
<IconButton
|
||||||
className='star-icon'
|
className='star-icon'
|
||||||
animate
|
animate
|
||||||
|
|
|
@ -16,6 +16,9 @@ import classNames from 'classnames';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import PollContainer from 'soapbox/containers/poll_container';
|
import PollContainer from 'soapbox/containers/poll_container';
|
||||||
import { StatusInteractionBar } from './status_interaction_bar';
|
import { StatusInteractionBar } from './status_interaction_bar';
|
||||||
|
import ProfileHoverCardContainer from 'soapbox/features/profile_hover_card/profile_hover_card_container';
|
||||||
|
import { isMobile } from 'soapbox/is_mobile';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
export default class DetailedStatus extends ImmutablePureComponent {
|
export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -38,6 +41,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
height: null,
|
height: null,
|
||||||
|
profileCardVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOpenVideo = (media, startTime) => {
|
handleOpenVideo = (media, startTime) => {
|
||||||
|
@ -81,16 +85,31 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showProfileCard = debounce(() => {
|
||||||
|
this.setState({ profileCardVisible: true });
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
|
handleProfileHover = e => {
|
||||||
|
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProfileLeave = e => {
|
||||||
|
this.showProfileCard.cancel();
|
||||||
|
this.setState({ profileCardVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { compact } = this.props;
|
const { compact } = this.props;
|
||||||
|
const { profileCardVisible } = this.state;
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let media = '';
|
let media = '';
|
||||||
|
let poll = '';
|
||||||
let statusTypeIcon = '';
|
let statusTypeIcon = '';
|
||||||
|
|
||||||
if (this.props.measureHeight) {
|
if (this.props.measureHeight) {
|
||||||
|
@ -98,8 +117,9 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('poll')) {
|
if (status.get('poll')) {
|
||||||
media = <PollContainer pollId={status.get('poll')} />;
|
poll = <PollContainer pollId={status.get('poll')} />;
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
}
|
||||||
|
if (status.get('media_attachments').size > 0) {
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
const video = status.getIn(['media_attachments', 0]);
|
const video = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
|
@ -158,10 +178,19 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
||||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} className='detailed-status__display-name'>
|
<div className='detailed-status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
||||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
|
<div className='detailed-status__display-name'>
|
||||||
<DisplayName account={status.get('account')} />
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`}>
|
||||||
</NavLink>
|
<div className='detailed-status__display-avatar'>
|
||||||
|
<Avatar account={status.get('account')} size={48} />
|
||||||
|
</div>
|
||||||
|
</NavLink>
|
||||||
|
<DisplayName account={status.get('account')}>
|
||||||
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
||||||
|
</DisplayName>
|
||||||
|
</div>
|
||||||
|
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth) && profileCardVisible} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{status.get('group') && (
|
{status.get('group') && (
|
||||||
<div className='status__meta'>
|
<div className='status__meta'>
|
||||||
|
@ -172,6 +201,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
|
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
{poll}
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<StatusInteractionBar status={status} />
|
<StatusInteractionBar status={status} />
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import Button from 'soapbox/components/button';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
followAccount,
|
||||||
|
unfollowAccount,
|
||||||
|
blockAccount,
|
||||||
|
unblockAccount,
|
||||||
|
} from 'soapbox/actions/accounts';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||||
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const me = state.get('me');
|
||||||
|
return {
|
||||||
|
me,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
onFollow(account) {
|
||||||
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(followAccount(account.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlock(account) {
|
||||||
|
if (account.getIn(['relationship', 'blocking'])) {
|
||||||
|
dispatch(unblockAccount(account.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(blockAccount(account.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class ActionButton extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
onFollow: PropTypes.func.isRequired,
|
||||||
|
onBlock: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFollow = () => {
|
||||||
|
this.props.onFollow(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlock = () => {
|
||||||
|
this.props.onBlock(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { account, intl, me } = this.props;
|
||||||
|
let actionBtn = null;
|
||||||
|
|
||||||
|
if (!account || !me) return actionBtn;
|
||||||
|
|
||||||
|
if (me !== account.get('id')) {
|
||||||
|
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||||
|
//
|
||||||
|
} else if (account.getIn(['relationship', 'requested'])) {
|
||||||
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
|
||||||
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
|
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||||
|
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||||
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} to='/settings/profile' />;
|
||||||
|
}
|
||||||
|
return actionBtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,25 +1,65 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
|
import { changeSetting, getSettings } from 'soapbox/actions/settings';
|
||||||
|
|
||||||
export default
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
collapse: { id: 'explanation_box.collapse', defaultMessage: 'Collapse explanation box' },
|
||||||
|
expand: { id: 'explanation_box.expand', defaultMessage: 'Expand explanation box' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
settings: getSettings(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
toggleExplanationBox(setting) {
|
||||||
|
dispatch(changeSetting(['explanationBox'], setting));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
class ExplanationBox extends React.PureComponent {
|
class ExplanationBox extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||||
explanation: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
explanation: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||||
dismissable: PropTypes.bool,
|
dismissable: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
toggleExplanationBox: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleToggleExplanationBox = () => {
|
||||||
|
this.props.toggleExplanationBox(this.props.settings.get('explanationBox') === true ? false : true);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { title, explanation, dismissable } = this.props;
|
const { title, explanation, dismissable, settings, intl } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='explanation-box'>
|
<div className='explanation-box'>
|
||||||
{title && <div className='explanation-box__title'>{title}</div>}
|
{title && <div className='explanation-box__title'>{title}
|
||||||
<div className='explanation-box__explanation'>
|
<IconButton
|
||||||
{explanation}
|
className='explanation_box__toggle' size={20}
|
||||||
{dismissable && <span className='explanation-box__dismiss'>Dismiss</span>}
|
title={settings.get('explanationBox') ? intl.formatMessage(messages.collapse) : intl.formatMessage(messages.expand)}
|
||||||
</div>
|
icon={settings.get('explanationBox') ? 'angle-down' : 'angle-up'}
|
||||||
|
onClick={this.handleToggleExplanationBox}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
{settings.get('explanationBox') &&
|
||||||
|
<div className='explanation-box__explanation'>
|
||||||
|
{explanation}
|
||||||
|
{dismissable && <span className='explanation-box__dismiss'>Dismiss</span>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default class FeaturesPanel extends React.PureComponent {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='wtf-panel promo-panel'>
|
||||||
|
<div className='promo-panel__container'>
|
||||||
|
|
||||||
|
<div className='promo-panel-item'>
|
||||||
|
<NavLink className='promo-panel-item__btn' to='/settings/profile'>
|
||||||
|
<Icon id='user' className='promo-panel-item__icon' fixedWidth />
|
||||||
|
Edit Profile
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='promo-panel-item'>
|
||||||
|
<NavLink className='promo-panel-item__btn' to='/messages'>
|
||||||
|
<Icon id='envelope' className='promo-panel-item__icon' fixedWidth />
|
||||||
|
Messages
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='promo-panel-item'>
|
||||||
|
<NavLink className='promo-panel-item__btn' to='/bookmarks'>
|
||||||
|
<Icon id='bookmark' className='promo-panel-item__icon' fixedWidth />
|
||||||
|
Bookmarks
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='promo-panel-item'>
|
||||||
|
<NavLink className='promo-panel-item__btn' to='/lists'>
|
||||||
|
<Icon id='list' className='promo-panel-item__icon' fixedWidth />
|
||||||
|
Lists
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='promo-panel-item'>
|
||||||
|
<NavLink className='promo-panel-item__btn' to='/auth/edit'>
|
||||||
|
<Icon id='lock' className='promo-panel-item__icon' fixedWidth />
|
||||||
|
Security
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='promo-panel-item'>
|
||||||
|
<NavLink className='promo-panel-item__btn' to='/settings/preferences'>
|
||||||
|
<Icon id='cog' className='promo-panel-item__icon' fixedWidth />
|
||||||
|
Preferences
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -59,6 +59,7 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
||||||
const fields = account.get('fields');
|
const fields = account.get('fields');
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const memberSinceDate = intl.formatDate(account.get('created_at'), { month: 'long', year: 'numeric' });
|
const memberSinceDate = intl.formatDate(account.get('created_at'), { month: 'long', year: 'numeric' });
|
||||||
|
const verified = account.get('pleroma').get('tags').includes('verified');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='profile-info-panel'>
|
<div className='profile-info-panel'>
|
||||||
|
@ -67,7 +68,7 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
||||||
<div className='profile-info-panel-content__name'>
|
<div className='profile-info-panel-content__name'>
|
||||||
<h1>
|
<h1>
|
||||||
<span dangerouslySetInnerHTML={displayNameHtml} />
|
<span dangerouslySetInnerHTML={displayNameHtml} />
|
||||||
{account.get('is_verified') && <VerificationBadge />}
|
{verified && <VerificationBadge />}
|
||||||
{badge}
|
{badge}
|
||||||
<small>@{acctFull(account)} {lockedIcon}</small>
|
<small>@{acctFull(account)} {lockedIcon}</small>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -10,6 +10,8 @@ import Avatar from 'soapbox/components/avatar';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
import { acctFull } from 'soapbox/utils/accounts';
|
import { acctFull } from 'soapbox/utils/accounts';
|
||||||
import StillImage from 'soapbox/components/still_image';
|
import StillImage from 'soapbox/components/still_image';
|
||||||
|
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
class UserPanel extends ImmutablePureComponent {
|
class UserPanel extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -24,6 +26,7 @@ class UserPanel extends ImmutablePureComponent {
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||||
|
const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='user-panel'>
|
<div className='user-panel'>
|
||||||
|
@ -45,6 +48,7 @@ class UserPanel extends ImmutablePureComponent {
|
||||||
<h1>
|
<h1>
|
||||||
<Link to={`/@${account.get('acct')}`}>
|
<Link to={`/@${account.get('acct')}`}>
|
||||||
<span className='user-panel__account__name' dangerouslySetInnerHTML={displayNameHtml} />
|
<span className='user-panel__account__name' dangerouslySetInnerHTML={displayNameHtml} />
|
||||||
|
{verified && <VerificationBadge />}
|
||||||
<small className='user-panel__account__username'>@{acctFull(account)}</small>
|
<small className='user-panel__account__username'>@{acctFull(account)}</small>
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -84,17 +88,17 @@ class UserPanel extends ImmutablePureComponent {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
const mapStateToProps = state => {
|
|
||||||
const me = state.get('me');
|
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
return {
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
account: getAccount(state, me),
|
account: getAccount(state, accountId),
|
||||||
};
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(
|
export default injectIntl(
|
||||||
connect(mapStateToProps, null, null, {
|
connect(makeMapStateToProps, null, null, {
|
||||||
forwardRef: true,
|
forwardRef: true,
|
||||||
})(UserPanel));
|
})(UserPanel));
|
||||||
|
|
|
@ -5,9 +5,10 @@ import { createSelector } from 'reselect';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { dequeueTimeline } from 'soapbox/actions/timelines';
|
import { dequeueTimeline } from 'soapbox/actions/timelines';
|
||||||
import { scrollTopTimeline } from '../../../actions/timelines';
|
import { scrollTopTimeline } from '../../../actions/timelines';
|
||||||
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
|
|
||||||
const makeGetStatusIds = () => createSelector([
|
const makeGetStatusIds = () => createSelector([
|
||||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
(state, { type }) => getSettings(state).get(type, ImmutableMap()),
|
||||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
||||||
(state) => state.get('statuses'),
|
(state) => state.get('statuses'),
|
||||||
(state) => state.get('me'),
|
(state) => state.get('me'),
|
||||||
|
@ -23,7 +24,11 @@ const makeGetStatusIds = () => createSelector([
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columnSettings.getIn(['shows', 'reply']) === false) {
|
if (columnSettings.getIn(['shows', 'reply']) === false) {
|
||||||
showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
|
showStatus = showStatus && (statusForId.get('in_reply_to_id') === null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columnSettings.getIn(['shows', 'direct']) === false) {
|
||||||
|
showStatus = showStatus && (statusForId.get('visibility') !== 'direct');
|
||||||
}
|
}
|
||||||
|
|
||||||
return showStatus;
|
return showStatus;
|
||||||
|
|
|
@ -22,8 +22,8 @@ import { openModal } from '../../actions/modal';
|
||||||
import { WrappedRoute } from './util/react_router_helpers';
|
import { WrappedRoute } from './util/react_router_helpers';
|
||||||
import UploadArea from './components/upload_area';
|
import UploadArea from './components/upload_area';
|
||||||
import TabsBar from './components/tabs_bar';
|
import TabsBar from './components/tabs_bar';
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel';
|
|
||||||
import LinkFooter from './components/link_footer';
|
import LinkFooter from './components/link_footer';
|
||||||
|
import FeaturesPanel from './components/features_panel';
|
||||||
import ProfilePage from 'soapbox/pages/profile_page';
|
import ProfilePage from 'soapbox/pages/profile_page';
|
||||||
// import GroupsPage from 'soapbox/pages/groups_page';
|
// import GroupsPage from 'soapbox/pages/groups_page';
|
||||||
// import GroupPage from 'soapbox/pages/group_page';
|
// import GroupPage from 'soapbox/pages/group_page';
|
||||||
|
@ -64,6 +64,7 @@ import {
|
||||||
// GroupTimeline,
|
// GroupTimeline,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
Lists,
|
Lists,
|
||||||
|
Bookmarks,
|
||||||
// GroupMembers,
|
// GroupMembers,
|
||||||
// GroupRemovedAccounts,
|
// GroupRemovedAccounts,
|
||||||
// GroupCreate,
|
// GroupCreate,
|
||||||
|
@ -74,6 +75,7 @@ import {
|
||||||
ConfigSoapbox,
|
ConfigSoapbox,
|
||||||
PasswordReset,
|
PasswordReset,
|
||||||
SecurityForm,
|
SecurityForm,
|
||||||
|
MfaForm,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
|
@ -136,19 +138,16 @@ const LAYOUT = {
|
||||||
},
|
},
|
||||||
DEFAULT: {
|
DEFAULT: {
|
||||||
LEFT: [
|
LEFT: [
|
||||||
<WhoToFollowPanel key='0' />,
|
|
||||||
<LinkFooter key='1' />,
|
<LinkFooter key='1' />,
|
||||||
],
|
],
|
||||||
RIGHT: [
|
RIGHT: [
|
||||||
// <GroupSidebarPanel key='0' />
|
<FeaturesPanel key='0' />,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
STATUS: {
|
STATUS: {
|
||||||
TOP: null,
|
TOP: null,
|
||||||
LEFT: null,
|
LEFT: null,
|
||||||
RIGHT: [
|
RIGHT: [
|
||||||
// <GroupSidebarPanel key='0' />,
|
|
||||||
<WhoToFollowPanel key='1' />,
|
|
||||||
<LinkFooter key='2' />,
|
<LinkFooter key='2' />,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -196,12 +195,13 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
<Switch>
|
<Switch>
|
||||||
<WrappedRoute path='/auth/sign_in' component={LoginPage} publicRoute exact />
|
<WrappedRoute path='/auth/sign_in' component={LoginPage} publicRoute exact />
|
||||||
<WrappedRoute path='/auth/reset_password' component={PasswordReset} publicRoute exact />
|
<WrappedRoute path='/auth/reset_password' component={PasswordReset} publicRoute exact />
|
||||||
<WrappedRoute path='/auth/edit' component={SecurityForm} exact />
|
<WrappedRoute path='/auth/edit' layout={LAYOUT.DEFAULT} component={SecurityForm} exact />
|
||||||
|
<WrappedRoute path='/auth/mfa' layout={LAYOUT.DEFAULT} component={MfaForm} exact />
|
||||||
|
|
||||||
<WrappedRoute path='/' exact page={HomePage} component={HomeTimeline} content={children} />
|
<WrappedRoute path='/' exact page={HomePage} component={HomeTimeline} content={children} />
|
||||||
<WrappedRoute path='/timeline/local' exact page={HomePage} component={CommunityTimeline} content={children} />
|
<WrappedRoute path='/timeline/local' exact page={HomePage} component={CommunityTimeline} content={children} />
|
||||||
<WrappedRoute path='/timeline/fediverse' exact page={HomePage} component={PublicTimeline} content={children} />
|
<WrappedRoute path='/timeline/fediverse' exact page={HomePage} component={PublicTimeline} content={children} />
|
||||||
<WrappedRoute path='/messages' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
<WrappedRoute path='/messages' layout={LAYOUT.DEFAULT} component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'featured' }} />
|
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'featured' }} />
|
||||||
|
@ -228,6 +228,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
|
|
||||||
<WrappedRoute path='/lists' layout={LAYOUT.DEFAULT} component={Lists} content={children} />
|
<WrappedRoute path='/lists' layout={LAYOUT.DEFAULT} component={Lists} content={children} />
|
||||||
<WrappedRoute path='/list/:id' page={HomePage} component={ListTimeline} content={children} />
|
<WrappedRoute path='/list/:id' page={HomePage} component={ListTimeline} content={children} />
|
||||||
|
<WrappedRoute path='/bookmarks' layout={LAYOUT.DEFAULT} component={Bookmarks} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/notifications' layout={LAYOUT.DEFAULT} component={Notifications} content={children} />
|
<WrappedRoute path='/notifications' layout={LAYOUT.DEFAULT} component={Notifications} content={children} />
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,10 @@ export function Lists() {
|
||||||
return import(/* webpackChunkName: "features/lists" */'../../lists');
|
return import(/* webpackChunkName: "features/lists" */'../../lists');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Bookmarks() {
|
||||||
|
return import(/* webpackChunkName: "features/bookmarks" */'../../bookmarks');
|
||||||
|
}
|
||||||
|
|
||||||
export function Status() {
|
export function Status() {
|
||||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||||
}
|
}
|
||||||
|
@ -189,3 +193,7 @@ export function PasswordReset() {
|
||||||
export function SecurityForm() {
|
export function SecurityForm() {
|
||||||
return import(/* webpackChunkName: "features/security" */'../../security');
|
return import(/* webpackChunkName: "features/security" */'../../security');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MfaForm() {
|
||||||
|
return import(/* webpackChunkName: "features/security/mfa_form" */'../../security/mfa_form');
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import WhoToFollowPanel from '../features/ui/components/who_to_follow_panel';
|
import WhoToFollowPanel from '../features/ui/components/who_to_follow_panel';
|
||||||
import TrendsPanel from '../features/ui/components/trends_panel';
|
import TrendsPanel from '../features/ui/components/trends_panel';
|
||||||
import LinkFooter from '../features/ui/components/link_footer';
|
import LinkFooter from '../features/ui/components/link_footer';
|
||||||
|
import FeaturesPanel from '../features/ui/components/features_panel';
|
||||||
import PromoPanel from '../features/ui/components/promo_panel';
|
import PromoPanel from '../features/ui/components/promo_panel';
|
||||||
import UserPanel from '../features/ui/components/user_panel';
|
import UserPanel from '../features/ui/components/user_panel';
|
||||||
import FundingPanel from '../features/ui/components/funding_panel';
|
import FundingPanel from '../features/ui/components/funding_panel';
|
||||||
|
@ -15,6 +16,7 @@ import { getFeatures } from 'soapbox/utils/features';
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const me = state.get('me');
|
const me = state.get('me');
|
||||||
return {
|
return {
|
||||||
|
me,
|
||||||
account: state.getIn(['accounts', me]),
|
account: state.getIn(['accounts', me]),
|
||||||
hasPatron: state.getIn(['soapbox', 'extensions', 'patron', 'enabled']),
|
hasPatron: state.getIn(['soapbox', 'extensions', 'patron', 'enabled']),
|
||||||
features: getFeatures(state.get('instance')),
|
features: getFeatures(state.get('instance')),
|
||||||
|
@ -30,7 +32,7 @@ class HomePage extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, account, hasPatron, features } = this.props;
|
const { me, children, account, hasPatron, features } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
|
@ -39,10 +41,8 @@ class HomePage extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
||||||
<div className='columns-area__panels__pane__inner'>
|
<div className='columns-area__panels__pane__inner'>
|
||||||
<UserPanel />
|
<UserPanel accountId={me} />
|
||||||
{hasPatron && <FundingPanel />}
|
{hasPatron && <FundingPanel />}
|
||||||
<PromoPanel />
|
|
||||||
<LinkFooter />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -68,6 +68,9 @@ class HomePage extends ImmutablePureComponent {
|
||||||
{/* <GroupSidebarPanel /> */}
|
{/* <GroupSidebarPanel /> */}
|
||||||
{features.trends && <TrendsPanel limit={3} />}
|
{features.trends && <TrendsPanel limit={3} />}
|
||||||
{features.suggestions && <WhoToFollowPanel limit={5} />}
|
{features.suggestions && <WhoToFollowPanel limit={5} />}
|
||||||
|
<FeaturesPanel />
|
||||||
|
<PromoPanel />
|
||||||
|
<LinkFooter />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,18 +50,18 @@ describe('alerts reducer', () => {
|
||||||
// });
|
// });
|
||||||
|
|
||||||
it('should handle ALERT_CLEAR', () => {
|
it('should handle ALERT_CLEAR', () => {
|
||||||
const state = ImmutableList([
|
const state = ImmutableList([
|
||||||
{
|
{
|
||||||
key: 0,
|
key: 0,
|
||||||
message: 'message_1',
|
message: 'message_1',
|
||||||
title: 'title_1',
|
title: 'title_1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 1,
|
key: 1,
|
||||||
message: 'message_2',
|
message: 'message_2',
|
||||||
title: 'title_2',
|
title: 'title_2',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.ALERT_CLEAR,
|
type: actions.ALERT_CLEAR,
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,6 +32,7 @@ describe('compose reducer', () => {
|
||||||
default_sensitive: false,
|
default_sensitive: false,
|
||||||
idempotencyKey: null,
|
idempotencyKey: null,
|
||||||
tagHistory: [],
|
tagHistory: [],
|
||||||
|
content_type: 'text/markdown',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -168,8 +169,18 @@ describe('compose reducer', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => {
|
||||||
|
const state = ImmutableMap({ spoiler: true, sensitive: true, idempotencyKey: null });
|
||||||
|
const action = {
|
||||||
|
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||||
|
};
|
||||||
|
expect(reducer(state, action).toJS()).toMatchObject({
|
||||||
|
sensitive: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => {
|
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => {
|
||||||
const state = ImmutableMap({ sensitive: true });
|
const state = ImmutableMap({ spoiler: false, sensitive: true });
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||||
};
|
};
|
||||||
|
@ -777,4 +788,11 @@ describe('compose reducer', () => {
|
||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
it('sets the post content-type', () => {
|
||||||
|
const action = {
|
||||||
|
type: actions.COMPOSE_TYPE_CHANGE,
|
||||||
|
value: 'text/plain',
|
||||||
|
};
|
||||||
|
expect(reducer(undefined, action).toJS()).toMatchObject({ content_type: 'text/plain' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -206,16 +206,16 @@ describe('notifications reducer', () => {
|
||||||
};
|
};
|
||||||
expect(reducer(state, action)).toEqual(ImmutableMap({
|
expect(reducer(state, action)).toEqual(ImmutableMap({
|
||||||
items: ImmutableList([
|
items: ImmutableList([
|
||||||
ImmutableMap({
|
ImmutableMap({
|
||||||
id: '10743',
|
id: '10743',
|
||||||
type: 'favourite',
|
type: 'favourite',
|
||||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||||
created_at: '2020-06-10T02:51:05.000Z',
|
created_at: '2020-06-10T02:51:05.000Z',
|
||||||
status: '9vvNxoo5EFbbnfdXQu',
|
status: '9vvNxoo5EFbbnfdXQu',
|
||||||
emoji: undefined,
|
emoji: undefined,
|
||||||
is_seen: true,
|
is_seen: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
top: false,
|
top: false,
|
||||||
unread: 2,
|
unread: 2,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -9,6 +9,11 @@ describe('status_lists reducer', () => {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
}),
|
}),
|
||||||
|
bookmarks: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
loaded: false,
|
||||||
|
items: ImmutableList(),
|
||||||
|
}),
|
||||||
pins: ImmutableMap({
|
pins: ImmutableMap({
|
||||||
next: null,
|
next: null,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
COMPOSE_TAG_HISTORY_UPDATE,
|
COMPOSE_TAG_HISTORY_UPDATE,
|
||||||
COMPOSE_SENSITIVITY_CHANGE,
|
COMPOSE_SENSITIVITY_CHANGE,
|
||||||
COMPOSE_SPOILERNESS_CHANGE,
|
COMPOSE_SPOILERNESS_CHANGE,
|
||||||
|
COMPOSE_TYPE_CHANGE,
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
COMPOSE_VISIBILITY_CHANGE,
|
COMPOSE_VISIBILITY_CHANGE,
|
||||||
COMPOSE_COMPOSING_CHANGE,
|
COMPOSE_COMPOSING_CHANGE,
|
||||||
|
@ -50,6 +51,7 @@ const initialState = ImmutableMap({
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
spoiler_text: '',
|
spoiler_text: '',
|
||||||
|
content_type: 'text/markdown',
|
||||||
privacy: null,
|
privacy: null,
|
||||||
text: '',
|
text: '',
|
||||||
focusDate: null,
|
focusDate: null,
|
||||||
|
@ -94,6 +96,7 @@ function clearAll(state) {
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
|
map.set('content_type', 'text/markdown');
|
||||||
map.set('is_submitting', false);
|
map.set('is_submitting', false);
|
||||||
map.set('is_changing_upload', false);
|
map.set('is_changing_upload', false);
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
|
@ -114,7 +117,7 @@ function appendMedia(state, media) {
|
||||||
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
|
||||||
if (prevSize === 0 && state.get('default_sensitive')) {
|
if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) {
|
||||||
map.set('sensitive', true);
|
map.set('sensitive', true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -211,8 +214,15 @@ export default function compose(state = initialState, action) {
|
||||||
.set('is_composing', false);
|
.set('is_composing', false);
|
||||||
case COMPOSE_SENSITIVITY_CHANGE:
|
case COMPOSE_SENSITIVITY_CHANGE:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('sensitive', !state.get('sensitive'));
|
if (!state.get('spoiler')) {
|
||||||
|
map.set('sensitive', !state.get('sensitive'));
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set('idempotencyKey', uuid());
|
||||||
|
});
|
||||||
|
case COMPOSE_TYPE_CHANGE:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('content_type', action.value);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
});
|
});
|
||||||
case COMPOSE_SPOILERNESS_CHANGE:
|
case COMPOSE_SPOILERNESS_CHANGE:
|
||||||
|
@ -220,6 +230,10 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
map.set('spoiler', !state.get('spoiler'));
|
map.set('spoiler', !state.get('spoiler'));
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
|
||||||
|
if (!state.get('sensitive') && state.get('media_attachments').size >= 1) {
|
||||||
|
map.set('sensitive', true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
case COMPOSE_SPOILER_TEXT_CHANGE:
|
case COMPOSE_SPOILER_TEXT_CHANGE:
|
||||||
return state
|
return state
|
||||||
|
@ -243,6 +257,7 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('focusDate', new Date());
|
map.set('focusDate', new Date());
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
map.set('content_type', 'text/markdown');
|
||||||
|
|
||||||
if (action.status.get('spoiler_text', '').length > 0) {
|
if (action.status.get('spoiler_text', '').length > 0) {
|
||||||
map.set('spoiler', true);
|
map.set('spoiler', true);
|
||||||
|
@ -326,6 +341,7 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('focusDate', new Date());
|
map.set('focusDate', new Date());
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
map.set('idempotencyKey', uuid());
|
map.set('idempotencyKey', uuid());
|
||||||
|
map.set('content_type', 'text/markdown');
|
||||||
|
|
||||||
if (action.status.get('spoiler_text').length > 0) {
|
if (action.status.get('spoiler_text').length > 0) {
|
||||||
map.set('spoiler', true);
|
map.set('spoiler', true);
|
||||||
|
|
|
@ -12,6 +12,9 @@ const nodeinfoToInstance = nodeinfo => {
|
||||||
account_activation_required: nodeinfo.getIn(['metadata', 'accountActivationRequired']),
|
account_activation_required: nodeinfo.getIn(['metadata', 'accountActivationRequired']),
|
||||||
features: nodeinfo.getIn(['metadata', 'features']),
|
features: nodeinfo.getIn(['metadata', 'features']),
|
||||||
federation: nodeinfo.getIn(['metadata', 'federation']),
|
federation: nodeinfo.getIn(['metadata', 'federation']),
|
||||||
|
fields_limits: ImmutableMap({
|
||||||
|
max_fields: nodeinfo.getIn(['metadata', 'fieldsLimits', 'maxFields']),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -31,9 +34,9 @@ const initialState = ImmutableMap({
|
||||||
export default function instance(state = initialState, action) {
|
export default function instance(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case INSTANCE_FETCH_SUCCESS:
|
case INSTANCE_FETCH_SUCCESS:
|
||||||
return initialState.merge(fromJS(action.instance));
|
return initialState.mergeDeep(fromJS(action.instance));
|
||||||
case NODEINFO_FETCH_SUCCESS:
|
case NODEINFO_FETCH_SUCCESS:
|
||||||
return nodeinfoToInstance(fromJS(action.nodeinfo)).merge(state);
|
return nodeinfoToInstance(fromJS(action.nodeinfo)).mergeDeep(state);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
DOMAIN_UNBLOCK_SUCCESS,
|
DOMAIN_UNBLOCK_SUCCESS,
|
||||||
} from '../actions/domain_blocks';
|
} from '../actions/domain_blocks';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
|
||||||
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
|
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
|
||||||
|
|
||||||
|
@ -42,8 +43,10 @@ const setDomainBlocking = (state, accounts, blocking) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const importPleromaAccount = (state, account) => {
|
const importPleromaAccount = (state, account) => {
|
||||||
if (!account.pleroma) return state;
|
const relationship = get(account, ['pleroma', 'relationship'], {});
|
||||||
return normalizeRelationship(state, account.pleroma.relationship);
|
if (relationship.id && relationship !== {})
|
||||||
|
return normalizeRelationship(state, relationship);
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const importPleromaAccounts = (state, accounts) => {
|
const importPleromaAccounts = (state, accounts) => {
|
||||||
|
|
|
@ -6,6 +6,14 @@ import {
|
||||||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
||||||
FAVOURITED_STATUSES_EXPAND_FAIL,
|
FAVOURITED_STATUSES_EXPAND_FAIL,
|
||||||
} from '../actions/favourites';
|
} from '../actions/favourites';
|
||||||
|
import {
|
||||||
|
BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||||
|
BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
||||||
|
BOOKMARKED_STATUSES_FETCH_FAIL,
|
||||||
|
BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
||||||
|
BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
||||||
|
BOOKMARKED_STATUSES_EXPAND_FAIL,
|
||||||
|
} from '../actions/bookmarks';
|
||||||
import {
|
import {
|
||||||
PINNED_STATUSES_FETCH_SUCCESS,
|
PINNED_STATUSES_FETCH_SUCCESS,
|
||||||
} from '../actions/pin_statuses';
|
} from '../actions/pin_statuses';
|
||||||
|
@ -13,6 +21,8 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import {
|
import {
|
||||||
FAVOURITE_SUCCESS,
|
FAVOURITE_SUCCESS,
|
||||||
UNFAVOURITE_SUCCESS,
|
UNFAVOURITE_SUCCESS,
|
||||||
|
BOOKMARK_SUCCESS,
|
||||||
|
UNBOOKMARK_SUCCESS,
|
||||||
PIN_SUCCESS,
|
PIN_SUCCESS,
|
||||||
UNPIN_SUCCESS,
|
UNPIN_SUCCESS,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
|
@ -23,6 +33,11 @@ const initialState = ImmutableMap({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
}),
|
}),
|
||||||
|
bookmarks: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
loaded: false,
|
||||||
|
items: ImmutableList(),
|
||||||
|
}),
|
||||||
pins: ImmutableMap({
|
pins: ImmutableMap({
|
||||||
next: null,
|
next: null,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
@ -71,10 +86,24 @@ export default function statusLists(state = initialState, action) {
|
||||||
return normalizeList(state, 'favourites', action.statuses, action.next);
|
return normalizeList(state, 'favourites', action.statuses, action.next);
|
||||||
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
|
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
|
||||||
return appendToList(state, 'favourites', action.statuses, action.next);
|
return appendToList(state, 'favourites', action.statuses, action.next);
|
||||||
|
case BOOKMARKED_STATUSES_FETCH_REQUEST:
|
||||||
|
case BOOKMARKED_STATUSES_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['bookmarks', 'isLoading'], true);
|
||||||
|
case BOOKMARKED_STATUSES_FETCH_FAIL:
|
||||||
|
case BOOKMARKED_STATUSES_EXPAND_FAIL:
|
||||||
|
return state.setIn(['bookmarks', 'isLoading'], false);
|
||||||
|
case BOOKMARKED_STATUSES_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, 'bookmarks', action.statuses, action.next);
|
||||||
|
case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, 'bookmarks', action.statuses, action.next);
|
||||||
case FAVOURITE_SUCCESS:
|
case FAVOURITE_SUCCESS:
|
||||||
return prependOneToList(state, 'favourites', action.status);
|
return prependOneToList(state, 'favourites', action.status);
|
||||||
case UNFAVOURITE_SUCCESS:
|
case UNFAVOURITE_SUCCESS:
|
||||||
return removeOneFromList(state, 'favourites', action.status);
|
return removeOneFromList(state, 'favourites', action.status);
|
||||||
|
case BOOKMARK_SUCCESS:
|
||||||
|
return prependOneToList(state, 'bookmarks', action.status);
|
||||||
|
case UNBOOKMARK_SUCCESS:
|
||||||
|
return removeOneFromList(state, 'bookmarks', action.status);
|
||||||
case PINNED_STATUSES_FETCH_SUCCESS:
|
case PINNED_STATUSES_FETCH_SUCCESS:
|
||||||
return normalizeList(state, 'pins', action.statuses, action.next);
|
return normalizeList(state, 'pins', action.statuses, action.next);
|
||||||
case PIN_SUCCESS:
|
case PIN_SUCCESS:
|
||||||
|
|
|
@ -34,6 +34,7 @@ $small-breakpoint: 960px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
padding: 14px 0;
|
padding: 14px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (max-width: 1024px) {
|
||||||
padding: 14px 20px;
|
padding: 14px 20px;
|
||||||
|
@ -1712,7 +1713,40 @@ $small-breakpoint: 960px;
|
||||||
.header,
|
.header,
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
}
|
||||||
|
|
||||||
|
.otp-form-overlay__container {
|
||||||
|
z-index: 9998;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba($base-overlay-background, 0.7);
|
||||||
|
|
||||||
|
.otp-form-overlay__form {
|
||||||
|
@include standard-panel-shadow;
|
||||||
|
border-radius: 10px;
|
||||||
|
z-index: 9999;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 800px;
|
||||||
|
position: relative;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.simple_form {
|
||||||
|
padding: 30px 50px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-form-overlay__close {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1725,3 +1759,10 @@ $small-breakpoint: 960px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1.otp-login {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
.card__bar {
|
.card__bar {
|
||||||
background: var(--brand-color--med);
|
background: var(--foreground-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,11 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bdi,
|
||||||
|
span.verified-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -584,7 +589,7 @@ a .account__avatar {
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__section-headline {
|
.account__section-headline {
|
||||||
background: var(--accent-color--faint);
|
background: var(--foreground-color);
|
||||||
|
|
||||||
button,
|
button,
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -71,3 +71,6 @@
|
||||||
@import 'components/error-boundary';
|
@import 'components/error-boundary';
|
||||||
@import 'components/video-player';
|
@import 'components/video-player';
|
||||||
@import 'components/audio-player';
|
@import 'components/audio-player';
|
||||||
|
@import 'components/profile_hover_card';
|
||||||
|
@import 'components/filters';
|
||||||
|
@import 'components/mfa_form';
|
||||||
|
|
|
@ -170,7 +170,10 @@ body {
|
||||||
&__title {
|
&__title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-bottom: 1em;
|
}
|
||||||
|
|
||||||
|
&__explanation {
|
||||||
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__dismiss {
|
&__dismiss {
|
||||||
|
@ -215,3 +218,14 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floating-link {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
|
@ -19,9 +19,10 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--accent-color--med);
|
background: var(--accent-color--faint);
|
||||||
@media screen and (max-width: 895px) {height: 225px;}
|
@media screen and (max-width: 895px) {height: 225px;}
|
||||||
&--none {height: 125px;}
|
&--none {height: 125px;}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -30,6 +31,10 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.still-image {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.still-image--play-on-hover::before {
|
.still-image--play-on-hover::before {
|
||||||
content: 'GIF';
|
content: 'GIF';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -53,7 +58,7 @@
|
||||||
min-height: 74px;
|
min-height: 74px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--accent-color--med);
|
background: var(--background-color);
|
||||||
@media (min-width: 895px) {height: 74px;}
|
@media (min-width: 895px) {height: 74px;}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,11 +173,11 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overlow: hidden;
|
overlow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
background: hsl( var(--brand-color_h), var(--brand-color_s), 20% );
|
background: hsl(var(--brand-color_h), var(--brand-color_s), 20%);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
color: white;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
|
@ -102,7 +102,7 @@ button {
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--brand-color);
|
border-color: var(--brand-color);
|
||||||
color: var(--primary-text-color);
|
color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
|
|
@ -205,13 +205,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-back-button {
|
.column-back-button {
|
||||||
background: var(--accent-color--med);
|
background: var(--accent-color--faint);
|
||||||
color: var(--highlight-text-color);
|
color: var(--highlight-text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
text-align: unset;
|
text-align: unset;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -298,6 +298,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
|
||||||
.compose-form__buttons {
|
.compose-form__buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
border-bottom: 1px solid var(--brand-color--faint);
|
border-bottom: 1px solid var(--brand-color--faint);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status__link {
|
.detailed-status__link {
|
||||||
|
|
|
@ -41,6 +41,7 @@ a.account__display-name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-name__html {
|
.display-name__html {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
.column,
|
.column,
|
||||||
.drawer {
|
.drawer {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer__pager {
|
.drawer__pager {
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
.filter-settings-panel {
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list article {
|
||||||
|
border-bottom: 1px solid var(--primary-text-color--faint);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields-group .two-col {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
div.input {
|
||||||
|
width: 45%;
|
||||||
|
margin-right: 20px;
|
||||||
|
|
||||||
|
.label_input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 485px) {
|
||||||
|
div.input {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
.label_input {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__container {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.filter__phrase,
|
||||||
|
.filter__contexts,
|
||||||
|
.filter__details {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.filter__list-label {
|
||||||
|
padding-right: 5px;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.filter__list-value span {
|
||||||
|
padding-right: 5px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__delete {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px;
|
||||||
|
align-items: baseline;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
span.filter__delete-label {
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__delete-icon {
|
||||||
|
background: none;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
padding: 0 5px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
.security-settings-panel {
|
||||||
|
margin: 20px;
|
||||||
|
|
||||||
|
h1.security-settings-panel__setup-otp {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.security-settings-panel__setup-otp {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-warning {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--warning-color--faint);
|
||||||
|
margin: 5px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup_codes {
|
||||||
|
margin: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--brand-color--faint);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
min-height: 125px;
|
||||||
|
|
||||||
|
.backup_code {
|
||||||
|
margin: 5px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-settings-panel__setup-otp__buttons {
|
||||||
|
margin: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
min-width: 182px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.confirm-key {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0 0 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.otp-auth {
|
||||||
|
.error-box {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: $error-red;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
.display-name__account {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name .profile-hover-card {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hover-card {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
width: 320px;
|
||||||
|
z-index: 998;
|
||||||
|
left: -10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&--visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(min-width: 750px) {
|
||||||
|
left: -100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hover-card__container {
|
||||||
|
@include standard-panel;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hover-card__action-button {
|
||||||
|
z-index: 999;
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel {
|
||||||
|
box-shadow: none;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
.user-panel-stats-item a strong {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel-stats-item {
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
&__label,
|
||||||
|
&__value {
|
||||||
|
display: inline;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hover-card__badges {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 110px;
|
||||||
|
left: 120px;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-hover-card__bio {
|
||||||
|
margin: 0 20px 20px;
|
||||||
|
max-height: 4em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--highlight-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(0deg, var(--foreground-color) 0%, var(--foreground-color), 80%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailed-status {
|
||||||
|
.profile-hover-card {
|
||||||
|
top: 0;
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent floating avatars from intercepting with current card */
|
||||||
|
.status,
|
||||||
|
.detailed-status {
|
||||||
|
.floating-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .floating-link {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
background-color: var(--foreground-color);
|
background-color: hsla(var(--brand-color_h), var(--brand-color_s), var(--brand-color_l), 0.35);
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,61 @@
|
||||||
|
.status__content {
|
||||||
|
p,
|
||||||
|
li {
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
li {
|
||||||
|
em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
blockquote {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: disc inside none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style: decimal inside none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote p {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
p > code {
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code block */
|
||||||
|
pre {
|
||||||
|
line-height: 1.6em;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
word-break: break-all;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status__content--with-action {
|
.status__content--with-action {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -152,7 +210,6 @@
|
||||||
.status__info .status__display-name {
|
.status__info .status__display-name {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding-right: 25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__info {
|
.status__info {
|
||||||
|
@ -160,6 +217,16 @@
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status__profile,
|
||||||
|
.detailed-status__profile {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__profile {
|
||||||
|
padding-right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-check-box {
|
.status-check-box {
|
||||||
border-bottom: 1px solid var(--background-color);
|
border-bottom: 1px solid var(--background-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -255,9 +255,9 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
border: 0;
|
border: 0;
|
||||||
height: 50px;
|
height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 10px 0;
|
padding: 13px 0 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
filter: brightness(0%) grayscale(100%) invert(100%);
|
filter: brightness(0%) grayscale(100%) invert(100%);
|
||||||
& span {display: none !important;}
|
& span {display: none !important;}
|
||||||
|
|
|
@ -2,15 +2,21 @@
|
||||||
.setting-toggle {
|
.setting-toggle {
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle {
|
.react-toggle {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&-track {
|
||||||
|
background-color: var(--foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
&-track-check,
|
&-track-check,
|
||||||
&-track-x {
|
&-track-x {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,23 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 265px;
|
width: 265px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: hidden;
|
|
||||||
|
.user-panel__account__name {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&,
|
||||||
|
.user-panel__account__name,
|
||||||
|
.user-panel__account__username {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -42,13 +58,13 @@
|
||||||
&__meta {
|
&__meta {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 6px 20px 17px;
|
padding: 6px 20px 17px;
|
||||||
opacity: 0.6;
|
// opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__account {
|
&__account {
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color--faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
|
@ -56,7 +72,7 @@
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color--faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover & {
|
&:hover & {
|
||||||
|
@ -89,7 +105,7 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color--faint);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
@ -99,7 +115,7 @@
|
||||||
&__value {
|
&__value {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color--faint);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
|
|
@ -8,6 +8,10 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--foreground-color);
|
background: var(--foreground-color);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@ body.admin {
|
||||||
}
|
}
|
||||||
|
|
||||||
.funding-panel {
|
.funding-panel {
|
||||||
margin: 20px 0;
|
margin-top: 15px;
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
input {
|
input {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
color: var(--primary-text-color);
|
||||||
padding: 7px 9px;
|
padding: 7px 9px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -342,6 +342,15 @@ code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=text][disabled],
|
||||||
|
input[type=number][disabled],
|
||||||
|
input[type=email][disabled],
|
||||||
|
input[type=password][disabled],
|
||||||
|
textarea[disabled] {
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
border-color: var(--primary-text-color--faint);
|
||||||
|
}
|
||||||
|
|
||||||
.input.field_with_errors {
|
.input.field_with_errors {
|
||||||
label {
|
label {
|
||||||
color: lighten($error-red, 12%);
|
color: lighten($error-red, 12%);
|
||||||
|
|
|
@ -188,6 +188,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -30,12 +30,14 @@ body {
|
||||||
--accent-color: hsl(var(--accent-color_hsl));
|
--accent-color: hsl(var(--accent-color_hsl));
|
||||||
--primary-text-color: hsl(var(--primary-text-color_hsl));
|
--primary-text-color: hsl(var(--primary-text-color_hsl));
|
||||||
--background-color: hsl(var(--background-color_hsl));
|
--background-color: hsl(var(--background-color_hsl));
|
||||||
|
--warning-color: hsla(var(--warning-color_hsl));
|
||||||
|
|
||||||
// Meta-variables
|
// Meta-variables
|
||||||
--brand-color_hsl: var(--brand-color_h), var(--brand-color_s), var(--brand-color_l);
|
--brand-color_hsl: var(--brand-color_h), var(--brand-color_s), var(--brand-color_l);
|
||||||
--accent-color_hsl: var(--accent-color_h), var(--accent-color_s), var(--accent-color_l);
|
--accent-color_hsl: var(--accent-color_h), var(--accent-color_s), var(--accent-color_l);
|
||||||
--primary-text-color_hsl: var(--primary-text-color_h), var(--primary-text-color_s), var(--primary-text-color_l);
|
--primary-text-color_hsl: var(--primary-text-color_h), var(--primary-text-color_s), var(--primary-text-color_l);
|
||||||
--background-color_hsl: var(--background-color_h), var(--background-color_s), var(--background-color_l);
|
--background-color_hsl: var(--background-color_h), var(--background-color_s), var(--background-color_l);
|
||||||
|
--warning-color_hsl: var(--warning-color_h), var(--warning-color_s), var(--warning-color_l);
|
||||||
--accent-color_h: calc(var(--brand-color_h) - 15);
|
--accent-color_h: calc(var(--brand-color_h) - 15);
|
||||||
--accent-color_s: 86%;
|
--accent-color_s: 86%;
|
||||||
--accent-color_l: 44%;
|
--accent-color_l: 44%;
|
||||||
|
@ -51,6 +53,7 @@ body {
|
||||||
calc(var(--accent-color_l) + 3%)
|
calc(var(--accent-color_l) + 3%)
|
||||||
);
|
);
|
||||||
--primary-text-color--faint: hsla(var(--primary-text-color_hsl), 0.6);
|
--primary-text-color--faint: hsla(var(--primary-text-color_hsl), 0.6);
|
||||||
|
--warning-color--faint: hsla(var(--warning-color_hsl), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-mode-light {
|
body.theme-mode-light {
|
||||||
|
@ -69,6 +72,9 @@ body.theme-mode-light {
|
||||||
--background-color_h: 0;
|
--background-color_h: 0;
|
||||||
--background-color_s: 0%;
|
--background-color_s: 0%;
|
||||||
--background-color_l: 94.9%;
|
--background-color_l: 94.9%;
|
||||||
|
--warning-color_h: 0;
|
||||||
|
--warning-color_s: 100%;
|
||||||
|
--warning-color_l: 66%;
|
||||||
|
|
||||||
// Modifiers
|
// Modifiers
|
||||||
--brand-color--hicontrast: hsl(
|
--brand-color--hicontrast: hsl(
|
||||||
|
@ -94,6 +100,9 @@ body.theme-mode-dark {
|
||||||
--background-color_h: 0;
|
--background-color_h: 0;
|
||||||
--background-color_s: 0%;
|
--background-color_s: 0%;
|
||||||
--background-color_l: 20%;
|
--background-color_l: 20%;
|
||||||
|
--warning-color_h: 0;
|
||||||
|
--warning-color_s: 100%;
|
||||||
|
--warning-color_l: 66%;
|
||||||
|
|
||||||
// Modifiers
|
// Modifiers
|
||||||
--brand-color--hicontrast: hsl(
|
--brand-color--hicontrast: hsl(
|
||||||
|
|
|
@ -654,6 +654,7 @@
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
|
border-color: transparent transparent var(--foreground-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,7 @@
|
||||||
"postcss-object-fit-images": "^1.1.2",
|
"postcss-object-fit-images": "^1.1.2",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"punycode": "^2.1.0",
|
"punycode": "^2.1.0",
|
||||||
|
"qrcode.react": "^1.0.0",
|
||||||
"rails-ujs": "^5.2.3",
|
"rails-ujs": "^5.2.3",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-color": "^2.18.1",
|
"react-color": "^2.18.1",
|
||||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -9335,6 +9335,20 @@ q@^1.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||||
|
|
||||||
|
qr.js@0.0.0:
|
||||||
|
version "0.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
|
||||||
|
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
|
||||||
|
|
||||||
|
qrcode.react@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-1.0.0.tgz#7e8889db3b769e555e8eb463d4c6de221c36d5de"
|
||||||
|
integrity sha512-jBXleohRTwvGBe1ngV+62QvEZ/9IZqQivdwzo9pJM4LQMoCM2VnvNBnKdjvGnKyDZ/l0nCDgsPod19RzlPvm/Q==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
prop-types "^15.6.0"
|
||||||
|
qr.js "0.0.0"
|
||||||
|
|
||||||
qs@6.7.0:
|
qs@6.7.0:
|
||||||
version "6.7.0"
|
version "6.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
|
|
Loading…
Reference in New Issue