diff --git a/README.md b/README.md
index 30fa61a1d..4538075ee 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ yarn
Finally, run the dev server:
```sh
-yarn start
+yarn dev
```
**That's it!** :tada:
@@ -140,7 +140,7 @@ NODE_ENV=development
```
#### Local dev server
-- `yarn dev` - Exact same as above, aliased to `yarn start` for convenience.
+- `yarn dev` - Run the local dev server.
#### Building
- `yarn build` - Compile without a dev server, into `/static` directory.
diff --git a/app/soapbox/__fixtures__/intlMessages.json b/app/soapbox/__fixtures__/intlMessages.json
index 6e873f504..328cd4180 100644
--- a/app/soapbox/__fixtures__/intlMessages.json
+++ b/app/soapbox/__fixtures__/intlMessages.json
@@ -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.sign_up": "Sign up",
"registration.tos": "Terms of Service",
+ "registration.reason": "Reason for Joining",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js
index c241b801f..a8d6d5f0f 100644
--- a/app/soapbox/actions/auth.js
+++ b/app/soapbox/actions/auth.js
@@ -133,15 +133,20 @@ export function logOut() {
export function register(params) {
return (dispatch, getState) => {
const needsConfirmation = getState().getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']);
+ const needsApproval = getState().getIn(['instance', 'approval_required']);
dispatch({ type: AUTH_REGISTER_REQUEST });
return dispatch(createAppAndToken()).then(() => {
return api(getState, 'app').post('/api/v1/accounts', params);
}).then(response => {
dispatch({ type: AUTH_REGISTER_SUCCESS, token: response.data });
dispatch(authLoggedIn(response.data));
- return needsConfirmation
- ? dispatch(showAlert('', 'Check your email for further instructions.'))
- : dispatch(fetchMe());
+ if (needsConfirmation) {
+ return dispatch(showAlert('', 'Check your email for further instructions.'));
+ } else if (needsApproval) {
+ return dispatch(showAlert('', 'Your account has been submitted for approval.'));
+ } else {
+ return dispatch(fetchMe());
+ }
}).catch(error => {
dispatch({ type: AUTH_REGISTER_FAIL, error });
throw error;
diff --git a/app/soapbox/actions/bookmarks.js b/app/soapbox/actions/bookmarks.js
new file mode 100644
index 000000000..544ed2ff2
--- /dev/null
+++ b/app/soapbox/actions/bookmarks.js
@@ -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,
+ };
+};
diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js
index 28442f994..a2b5c0f85 100644
--- a/app/soapbox/actions/compose.js
+++ b/app/soapbox/actions/compose.js
@@ -43,6 +43,7 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_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_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
@@ -175,6 +176,7 @@ export function submitCompose(routerHistory, group) {
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
visibility: getState().getIn(['compose', 'privacy']),
+ content_type: getState().getIn(['compose', 'content_type']),
poll: getState().getIn(['compose', 'poll'], null),
group_id: group ? group.get('id') : null,
}, {
@@ -226,11 +228,6 @@ export function uploadCompose(files) {
return;
}
- if (getState().getIn(['compose', 'poll'])) {
- dispatch(showAlert(undefined, messages.uploadErrorPoll));
- return;
- }
-
dispatch(uploadComposeRequest());
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) {
return {
type: COMPOSE_SPOILER_TEXT_CHANGE,
diff --git a/app/soapbox/actions/interactions.js b/app/soapbox/actions/interactions.js
index 7451c14f9..1acfa9c57 100644
--- a/app/soapbox/actions/interactions.js
+++ b/app/soapbox/actions/interactions.js
@@ -1,5 +1,6 @@
import api from '../api';
import { importFetchedAccounts, importFetchedStatus } from './importer';
+import { showAlert } from 'soapbox/actions/alerts';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
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_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) {
return function(dispatch, getState) {
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) {
return (dispatch, getState) => {
if (!getState().get('me')) return;
diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js
index 815479b96..0c2f35a8b 100644
--- a/app/soapbox/actions/settings.js
+++ b/app/soapbox/actions/settings.js
@@ -22,6 +22,7 @@ const defaultSettings = ImmutableMap({
defaultPrivacy: 'public',
themeMode: 'light',
locale: navigator.language.split(/[-_]/)[0] || 'en',
+ explanationBox: true,
systemFont: false,
dyslexicFont: false,
diff --git a/app/soapbox/components/display_name.js b/app/soapbox/components/display_name.js
index 1016c9f5a..30dbce9a2 100644
--- a/app/soapbox/components/display_name.js
+++ b/app/soapbox/components/display_name.js
@@ -1,4 +1,5 @@
import React from 'react';
+import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import VerificationBadge from './verification_badge';
import { acctFull } from '../utils/accounts';
@@ -8,10 +9,11 @@ export default class DisplayName extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
others: ImmutablePropTypes.list,
+ children: PropTypes.node,
};
render() {
- const { account, others } = this.props;
+ const { account, others, children } = this.props;
let displayName, suffix;
@@ -40,6 +42,7 @@ export default class DisplayName extends React.PureComponent {
{displayName}
{suffix}
+ {children}
);
}
diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js
index 67d67d904..fa589dc7b 100644
--- a/app/soapbox/components/sidebar_menu.js
+++ b/app/soapbox/components/sidebar_menu.js
@@ -33,6 +33,7 @@ const messages = defineMessages({
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
lists: { id: 'column.lists', defaultMessage: 'Lists' },
+ bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
donate: { id: 'donate', defaultMessage: 'Donate' },
@@ -145,6 +146,10 @@ class SidebarMenu extends ImmutablePureComponent {