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 { {intl.formatMessage(messages.lists)} + + + {intl.formatMessage(messages.bookmarks)} +
diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 889afc01e..b7f21a2fd 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -18,6 +18,9 @@ import classNames from 'classnames'; import Icon from 'soapbox/components/icon'; import PollContainer from 'soapbox/containers/poll_container'; 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 // to use the progress bar to show download progress @@ -104,6 +107,7 @@ class Status extends ImmutablePureComponent { state = { showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia), statusId: undefined, + profileCardVisible: false, }; // Track height changes we know about to compensate scrolling @@ -249,6 +253,19 @@ class Status extends ImmutablePureComponent { 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() { const { status } = this.props; @@ -265,6 +282,7 @@ class Status extends ImmutablePureComponent { render() { let media = null; + let poll = null; let statusAvatar, prepend, rebloggedByText, reblogContent; const { intl, hidden, featured, otherAccounts, unread, showThread, group } = this.props; @@ -332,8 +350,9 @@ class Status extends ImmutablePureComponent { } if (status.get('poll')) { - media = ; - } else if (status.get('media_attachments').size > 0) { + poll = ; + } + if (status.get('media_attachments').size > 0) { if (this.props.muted) { media = ( @@ -448,13 +468,16 @@ class Status extends ImmutablePureComponent { - +
+ {statusAvatar}
- - - + + + + +
{!group && status.get('group') && ( @@ -473,6 +496,7 @@ class Status extends ImmutablePureComponent { /> {media} + {poll} {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (