From 08daa19f2cef1ad3fb829c20884f76b7140df72a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 Jun 2022 13:46:21 -0500 Subject: [PATCH 01/15] Add back SensitiveButton, convert to TSX --- .../compose/components/sensitive-button.tsx | 44 ++++++++++++++ .../compose/components/upload_form.tsx | 4 +- .../containers/sensitive_button_container.js | 60 ------------------- 3 files changed, 46 insertions(+), 62 deletions(-) create mode 100644 app/soapbox/features/compose/components/sensitive-button.tsx delete mode 100644 app/soapbox/features/compose/containers/sensitive_button_container.js diff --git a/app/soapbox/features/compose/components/sensitive-button.tsx b/app/soapbox/features/compose/components/sensitive-button.tsx new file mode 100644 index 000000000..74a5176af --- /dev/null +++ b/app/soapbox/features/compose/components/sensitive-button.tsx @@ -0,0 +1,44 @@ +import classNames from 'classnames'; +import React from 'react'; +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { changeComposeSensitivity } from 'soapbox/actions/compose'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; + +const messages = defineMessages({ + marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, + unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' }, +}); + +/** Button to mark own media as sensitive. */ +const SensitiveButton: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const active = useAppSelector(state => state.compose.get('sensitive') === true); + const disabled = useAppSelector(state => state.compose.get('spoiler') === true); + + const onClick = () => { + dispatch(changeComposeSensitivity()); + }; + + return ( +
+ +
+ ); +}; + +export default SensitiveButton; diff --git a/app/soapbox/features/compose/components/upload_form.tsx b/app/soapbox/features/compose/components/upload_form.tsx index 00e66030c..ae44d2561 100644 --- a/app/soapbox/features/compose/components/upload_form.tsx +++ b/app/soapbox/features/compose/components/upload_form.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useAppSelector } from 'soapbox/hooks'; -// import SensitiveButtonContainer from '../containers/sensitive_button_container'; +import SensitiveButton from '../components/sensitive-button'; import UploadProgress from '../components/upload-progress'; import UploadContainer from '../containers/upload_container'; @@ -25,7 +25,7 @@ const UploadForm = () => { ))} - {/* {!mediaIds.isEmpty() && } */} + {!mediaIds.isEmpty() && } ); }; diff --git a/app/soapbox/features/compose/containers/sensitive_button_container.js b/app/soapbox/features/compose/containers/sensitive_button_container.js deleted file mode 100644 index b28d3c1f7..000000000 --- a/app/soapbox/features/compose/containers/sensitive_button_container.js +++ /dev/null @@ -1,60 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { changeComposeSensitivity } from 'soapbox/actions/compose'; - -const messages = defineMessages({ - marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, - unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' }, -}); - -const mapStateToProps = state => ({ - active: state.getIn(['compose', 'sensitive']), - disabled: state.getIn(['compose', 'spoiler']), -}); - -const mapDispatchToProps = dispatch => ({ - - onClick() { - dispatch(changeComposeSensitivity()); - }, - -}); - -class SensitiveButton extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - disabled: PropTypes.bool, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render() { - const { active, disabled, onClick, intl } = this.props; - - return ( -
- -
- ); - } - -} - -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); From edffe9a837641654fe5b141349b6cff26f5d4a34 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 Jun 2022 13:55:34 -0500 Subject: [PATCH 02/15] SensitiveButton: use UI components --- .../components/ui/form-group/form-group.tsx | 6 +++++- .../compose/components/sensitive-button.tsx | 18 ++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/soapbox/components/ui/form-group/form-group.tsx b/app/soapbox/components/ui/form-group/form-group.tsx index 75517fc79..ec0da73e4 100644 --- a/app/soapbox/components/ui/form-group/form-group.tsx +++ b/app/soapbox/components/ui/form-group/form-group.tsx @@ -8,6 +8,8 @@ import Stack from '../stack/stack'; interface IFormGroup { /** Input label message. */ labelText?: React.ReactNode, + /** Input label tooltip message. */ + labelTitle?: string, /** Input hint message. */ hintText?: React.ReactNode, /** Input errors. */ @@ -16,7 +18,7 @@ interface IFormGroup { /** Input container with label. Renders the child. */ const FormGroup: React.FC = (props) => { - const { children, errors = [], labelText, hintText } = props; + const { children, errors = [], labelText, labelTitle, hintText } = props; const formFieldId: string = useMemo(() => `field-${uuidv4()}`, []); const inputChildren = React.Children.toArray(children); const hasError = errors?.length > 0; @@ -41,6 +43,7 @@ const FormGroup: React.FC = (props) => { htmlFor={formFieldId} data-testid='form-group-label' className='-mt-0.5 block text-sm font-medium text-gray-700 dark:text-gray-400' + title={labelTitle} > {labelText} @@ -74,6 +77,7 @@ const FormGroup: React.FC = (props) => { htmlFor={formFieldId} data-testid='form-group-label' className='block text-sm font-medium text-gray-700 dark:text-gray-400' + title={labelTitle} > {labelText} diff --git a/app/soapbox/features/compose/components/sensitive-button.tsx b/app/soapbox/features/compose/components/sensitive-button.tsx index 74a5176af..e1c7b48ba 100644 --- a/app/soapbox/features/compose/components/sensitive-button.tsx +++ b/app/soapbox/features/compose/components/sensitive-button.tsx @@ -1,8 +1,8 @@ -import classNames from 'classnames'; import React from 'react'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { changeComposeSensitivity } from 'soapbox/actions/compose'; +import { FormGroup, Checkbox } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; const messages = defineMessages({ @@ -23,20 +23,18 @@ const SensitiveButton: React.FC = () => { }; return ( -
- +
); }; From 1e8e1fd071fe8001603bf9b75447db37de60d9e9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 Jun 2022 13:59:08 -0500 Subject: [PATCH 03/15] QuotedStatus: use thumbs in composer --- app/soapbox/features/status/components/quoted_status.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/status/components/quoted_status.tsx b/app/soapbox/features/status/components/quoted_status.tsx index bf8069aa8..8cb4db1f6 100644 --- a/app/soapbox/features/status/components/quoted_status.tsx +++ b/app/soapbox/features/status/components/quoted_status.tsx @@ -135,7 +135,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => dangerouslySetInnerHTML={{ __html: status.contentHtml }} /> - + ); }; From 735d04b0074ac245fd837d884a0192b42bf5c1b5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 Jun 2022 14:26:12 -0500 Subject: [PATCH 04/15] QuotedStatus: properly display sensitive media --- app/soapbox/components/status.tsx | 11 +--------- .../status/components/quoted_status.tsx | 20 +++++++++++++++++-- app/soapbox/features/status/index.tsx | 3 ++- app/soapbox/utils/status.ts | 11 ++++++++++ 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 2b1325a60..774ffed7c 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -8,6 +8,7 @@ import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import AccountContainer from 'soapbox/containers/account_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; +import { defaultMediaVisibility } from 'soapbox/utils/status'; import StatusMedia from './status-media'; import StatusReplyMentions from './status-reply-mentions'; @@ -50,16 +51,6 @@ export const textForScreenReader = (intl: IntlShape, status: StatusEntity, reblo return values.join(', '); }; -export const defaultMediaVisibility = (status: StatusEntity, displayMedia: string): boolean => { - if (!status) return false; - - if (status.reblog && typeof status.reblog === 'object') { - status = status.reblog; - } - - return (displayMedia !== 'hide_all' && !status.sensitive || displayMedia === 'show_all'); -}; - interface IStatus extends RouteComponentProps { id?: string, contextType?: string, diff --git a/app/soapbox/features/status/components/quoted_status.tsx b/app/soapbox/features/status/components/quoted_status.tsx index 8cb4db1f6..6c0fae410 100644 --- a/app/soapbox/features/status/components/quoted_status.tsx +++ b/app/soapbox/features/status/components/quoted_status.tsx @@ -1,11 +1,13 @@ import classNames from 'classnames'; -import React from 'react'; +import React, { useState } from 'react'; import { defineMessages, useIntl, FormattedMessage, FormattedList } from 'react-intl'; import { useHistory } from 'react-router-dom'; import StatusMedia from 'soapbox/components/status-media'; import { Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; +import { useSettings } from 'soapbox/hooks'; +import { defaultMediaVisibility } from 'soapbox/utils/status'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; @@ -27,6 +29,11 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => const intl = useIntl(); const history = useHistory(); + const settings = useSettings(); + const displayMedia = settings.get('displayMedia'); + + const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); + const handleExpandClick = (e: React.MouseEvent) => { if (!status) return; const account = status.account as AccountEntity; @@ -44,6 +51,10 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => } }; + const handleToggleMediaVisibility = () => { + setShowMedia(!showMedia); + }; + const renderReplyMentions = () => { if (!status?.in_reply_to_id) { return null; @@ -135,7 +146,12 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => dangerouslySetInnerHTML={{ __html: status.contentHtml }} /> - + ); }; diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 39d768e9f..735f1804a 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -49,13 +49,14 @@ import { import { fetchStatusWithContext, fetchNext } from 'soapbox/actions/statuses'; import MissingIndicator from 'soapbox/components/missing_indicator'; import ScrollableList from 'soapbox/components/scrollable_list'; -import { textForScreenReader, defaultMediaVisibility } from 'soapbox/components/status'; +import { textForScreenReader } from 'soapbox/components/status'; import SubNavigation from 'soapbox/components/sub_navigation'; import Tombstone from 'soapbox/components/tombstone'; import { Column, Stack } from 'soapbox/components/ui'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; import PendingStatus from 'soapbox/features/ui/components/pending_status'; import { makeGetStatus } from 'soapbox/selectors'; +import { defaultMediaVisibility } from 'soapbox/utils/status'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; diff --git a/app/soapbox/utils/status.ts b/app/soapbox/utils/status.ts index b735fb75d..439edfc02 100644 --- a/app/soapbox/utils/status.ts +++ b/app/soapbox/utils/status.ts @@ -2,6 +2,17 @@ import { isIntegerId } from 'soapbox/utils/numbers'; import type { Status as StatusEntity } from 'soapbox/types/entities'; +/** Get the initial visibility of media attachments from user settings. */ +export const defaultMediaVisibility = (status: StatusEntity | undefined, displayMedia: string): boolean => { + if (!status) return false; + + if (status.reblog && typeof status.reblog === 'object') { + status = status.reblog; + } + + return (displayMedia !== 'hide_all' && !status.sensitive || displayMedia === 'show_all'); +}; + /** Grab the first external link from a status. */ export const getFirstExternalLink = (status: StatusEntity): HTMLAnchorElement | null => { try { From da70097960e83f017ebfe1cb2ed4b3683c121a1a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 Jun 2022 14:48:24 -0500 Subject: [PATCH 05/15] Convert QuotedStatus containers to TSX --- app/soapbox/components/status.tsx | 2 +- .../containers/quoted_status_container.js | 26 --------------- .../containers/quoted_status_container.tsx | 32 +++++++++++++++++++ .../status/components/detailed-status.tsx | 2 +- .../containers/quoted_status_container.js | 17 ---------- .../containers/quoted_status_container.tsx | 29 +++++++++++++++++ .../features/ui/components/pending_status.tsx | 2 +- 7 files changed, 64 insertions(+), 46 deletions(-) delete mode 100644 app/soapbox/features/compose/containers/quoted_status_container.js create mode 100644 app/soapbox/features/compose/containers/quoted_status_container.tsx delete mode 100644 app/soapbox/features/status/containers/quoted_status_container.js create mode 100644 app/soapbox/features/status/containers/quoted_status_container.tsx diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 774ffed7c..4389b1fc1 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -422,7 +422,7 @@ class Status extends ImmutablePureComponent { ); } else { - quote = ; + quote = ; } } diff --git a/app/soapbox/features/compose/containers/quoted_status_container.js b/app/soapbox/features/compose/containers/quoted_status_container.js deleted file mode 100644 index c15fa1764..000000000 --- a/app/soapbox/features/compose/containers/quoted_status_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import { connect } from 'react-redux'; - -import { cancelQuoteCompose } from 'soapbox/actions/compose'; -import QuotedStatus from 'soapbox/features/status/components/quoted_status'; -import { makeGetStatus } from 'soapbox/selectors'; - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = state => ({ - status: getStatus(state, { id: state.getIn(['compose', 'quote']) }), - compose: true, - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = dispatch => ({ - - onCancel() { - dispatch(cancelQuoteCompose()); - }, - -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(QuotedStatus); \ No newline at end of file diff --git a/app/soapbox/features/compose/containers/quoted_status_container.tsx b/app/soapbox/features/compose/containers/quoted_status_container.tsx new file mode 100644 index 000000000..e9a968573 --- /dev/null +++ b/app/soapbox/features/compose/containers/quoted_status_container.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { cancelQuoteCompose } from 'soapbox/actions/compose'; +import QuotedStatus from 'soapbox/features/status/components/quoted_status'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; +import { makeGetStatus } from 'soapbox/selectors'; + +const getStatus = makeGetStatus(); + +/** QuotedStatus shown in post composer. */ +const QuotedStatusContainer: React.FC = () => { + const dispatch = useAppDispatch(); + const status = useAppSelector(state => getStatus(state, { id: state.compose.get('quote') })); + + const onCancel = () => { + dispatch(cancelQuoteCompose()); + }; + + if (!status) { + return null; + } + + return ( + + ); +}; + +export default QuotedStatusContainer; diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 1387fd2cf..4f9b584af 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -116,7 +116,7 @@ class DetailedStatus extends ImmutablePureComponent ); } else { - quote = ; + quote = ; } } diff --git a/app/soapbox/features/status/containers/quoted_status_container.js b/app/soapbox/features/status/containers/quoted_status_container.js deleted file mode 100644 index a375b2562..000000000 --- a/app/soapbox/features/status/containers/quoted_status_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; - -import { makeGetStatus } from 'soapbox/selectors'; - -import QuotedStatus from '../components/quoted_status'; - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, { statusId }) => ({ - status: getStatus(state, { id: statusId }), - }); - - return mapStateToProps; -}; - -export default connect(makeMapStateToProps)(QuotedStatus); diff --git a/app/soapbox/features/status/containers/quoted_status_container.tsx b/app/soapbox/features/status/containers/quoted_status_container.tsx new file mode 100644 index 000000000..e7e4ad151 --- /dev/null +++ b/app/soapbox/features/status/containers/quoted_status_container.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetStatus } from 'soapbox/selectors'; + +import QuotedStatus from '../components/quoted_status'; + +const getStatus = makeGetStatus(); + +interface IQuotedStatusContainer { + /** Status ID to the quoted status. */ + statusId: string, +} + +const QuotedStatusContainer: React.FC = ({ statusId }) => { + const status = useAppSelector(state => getStatus(state, { id: statusId })); + + if (!status) { + return null; + } + + return ( + + ); +}; + +export default QuotedStatusContainer; diff --git a/app/soapbox/features/ui/components/pending_status.tsx b/app/soapbox/features/ui/components/pending_status.tsx index f4b990fc6..03853008c 100644 --- a/app/soapbox/features/ui/components/pending_status.tsx +++ b/app/soapbox/features/ui/components/pending_status.tsx @@ -83,7 +83,7 @@ const PendingStatus: React.FC = ({ idempotencyKey, className, mu {status.poll && } - {status.quote && } + {status.quote && } {/* TODO */} From 5fe442b68455734d2bc6ea2448ecc5a78bc4e500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 7 Jun 2022 22:21:18 +0200 Subject: [PATCH 06/15] ESLint: add no-duplicate-imports rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .eslintrc.js | 1 + app/soapbox/actions/__tests__/statuses-test.js | 3 +-- app/soapbox/api.ts | 3 +-- app/soapbox/components/media_gallery.js | 3 +-- app/soapbox/components/sidebar_menu.tsx | 3 +-- app/soapbox/components/status_action_bar.tsx | 3 +-- app/soapbox/features/account_gallery/index.js | 3 +-- .../features/admin/components/latest_accounts_panel.tsx | 3 +-- app/soapbox/features/audio/index.js | 6 ++---- app/soapbox/features/chats/components/chat_panes.js | 3 +-- app/soapbox/features/compose/components/search_results.js | 3 +-- .../features/compose/containers/poll_form_container.js | 6 +++++- app/soapbox/features/compose/containers/upload_container.js | 3 +-- app/soapbox/features/filters/index.js | 3 +-- app/soapbox/features/groups/removed_accounts/index.js | 3 +-- app/soapbox/features/list_adder/index.tsx | 3 +-- app/soapbox/features/list_editor/index.tsx | 3 +-- app/soapbox/features/lists/index.tsx | 3 +-- .../features/notifications/components/notification.tsx | 3 +-- .../notifications/containers/column_settings_container.js | 3 +-- app/soapbox/features/status/components/detailed-status.tsx | 3 +-- app/soapbox/features/status/index.tsx | 3 ++- app/soapbox/features/ui/components/profile-dropdown.tsx | 3 +-- app/soapbox/features/ui/components/timeline.tsx | 3 +-- app/soapbox/features/ui/index.tsx | 6 +++--- app/soapbox/features/verification/registration.tsx | 3 +-- .../features/verification/steps/email-verification.js | 3 +-- app/soapbox/reducers/contexts.ts | 2 +- 28 files changed, 36 insertions(+), 54 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index bfe92311d..c140fa524 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -258,6 +258,7 @@ module.exports = { alphabetize: { order: 'asc' }, }, ], + '@typescript-eslint/no-duplicate-imports': 'error', 'promise/catch-or-return': 'error', diff --git a/app/soapbox/actions/__tests__/statuses-test.js b/app/soapbox/actions/__tests__/statuses-test.js index 463b87437..b00175401 100644 --- a/app/soapbox/actions/__tests__/statuses-test.js +++ b/app/soapbox/actions/__tests__/statuses-test.js @@ -6,8 +6,7 @@ import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { normalizeStatus } from 'soapbox/normalizers/status'; import rootReducer from 'soapbox/reducers'; -import { fetchContext } from '../statuses'; -import { deleteStatus } from '../statuses'; +import { deleteStatus, fetchContext } from '../statuses'; describe('fetchContext()', () => { it('handles Mitra context', done => { diff --git a/app/soapbox/api.ts b/app/soapbox/api.ts index 34ed699f8..05d08b768 100644 --- a/app/soapbox/api.ts +++ b/app/soapbox/api.ts @@ -11,8 +11,7 @@ import { createSelector } from 'reselect'; import * as BuildConfig from 'soapbox/build_config'; import { RootState } from 'soapbox/store'; -import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth'; -import { isURL } from 'soapbox/utils/auth'; +import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth'; /** Parse Link headers, mostly for pagination. diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js index 44b08a3df..5e528d4be 100644 --- a/app/soapbox/components/media_gallery.js +++ b/app/soapbox/components/media_gallery.js @@ -1,6 +1,5 @@ import classNames from 'classnames'; -import { is } from 'immutable'; -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, is } from 'immutable'; import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; diff --git a/app/soapbox/components/sidebar_menu.tsx b/app/soapbox/components/sidebar_menu.tsx index 7d47baa47..c53ec0b40 100644 --- a/app/soapbox/components/sidebar_menu.tsx +++ b/app/soapbox/components/sidebar_menu.tsx @@ -4,8 +4,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link, NavLink } from 'react-router-dom'; -import { logOut, switchAccount } from 'soapbox/actions/auth'; -import { fetchOwnAccounts } from 'soapbox/actions/auth'; +import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth'; import { getSettings } from 'soapbox/actions/settings'; import { closeSidebar } from 'soapbox/actions/sidebar'; import Account from 'soapbox/components/account'; diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index f50852698..dbb78b8d7 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; +import { openModal } from 'soapbox/actions/modals'; import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper'; import StatusActionButton from 'soapbox/components/status-action-button'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; @@ -13,8 +14,6 @@ import { isUserTouching } from 'soapbox/is_mobile'; import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts'; import { getFeatures } from 'soapbox/utils/features'; -import { openModal } from '../actions/modals'; - import type { History } from 'history'; import type { AnyAction, Dispatch } from 'redux'; import type { Menu } from 'soapbox/components/dropdown_menu'; diff --git a/app/soapbox/features/account_gallery/index.js b/app/soapbox/features/account_gallery/index.js index a17fa85e1..18d06c036 100644 --- a/app/soapbox/features/account_gallery/index.js +++ b/app/soapbox/features/account_gallery/index.js @@ -13,8 +13,7 @@ import { openModal } from 'soapbox/actions/modals'; import { expandAccountMediaTimeline } from 'soapbox/actions/timelines'; import LoadMore from 'soapbox/components/load_more'; import MissingIndicator from 'soapbox/components/missing_indicator'; -import { Column } from 'soapbox/components/ui'; -import { Spinner } from 'soapbox/components/ui'; +import { Column, Spinner } from 'soapbox/components/ui'; import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors'; import { getFeatures } from 'soapbox/utils/features'; diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.tsx b/app/soapbox/features/admin/components/latest_accounts_panel.tsx index 804744d5e..b9eabcde6 100644 --- a/app/soapbox/features/admin/components/latest_accounts_panel.tsx +++ b/app/soapbox/features/admin/components/latest_accounts_panel.tsx @@ -7,8 +7,7 @@ import { fetchUsers } from 'soapbox/actions/admin'; import compareId from 'soapbox/compare_id'; import { Widget } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; -import { useAppSelector } from 'soapbox/hooks'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' }, diff --git a/app/soapbox/features/audio/index.js b/app/soapbox/features/audio/index.js index d73b7208c..6fa5418bc 100644 --- a/app/soapbox/features/audio/index.js +++ b/app/soapbox/features/audio/index.js @@ -1,13 +1,11 @@ import classNames from 'classnames'; -import { throttle } from 'lodash'; -import { debounce } from 'lodash'; +import { debounce, throttle } from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import Icon from 'soapbox/components/icon'; -import { formatTime } from 'soapbox/features/video'; -import { getPointerPosition, fileNameFromURL } from 'soapbox/features/video'; +import { formatTime, getPointerPosition, fileNameFromURL } from 'soapbox/features/video'; import Visualizer from './visualizer'; diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js index 7232af6e5..74647c98c 100644 --- a/app/soapbox/features/chats/components/chat_panes.js +++ b/app/soapbox/features/chats/components/chat_panes.js @@ -3,8 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import { injectIntl, defineMessages } from 'react-intl'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { createSelector } from 'reselect'; diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js index e4dfa936b..bb816094e 100644 --- a/app/soapbox/features/compose/components/search_results.js +++ b/app/soapbox/features/compose/components/search_results.js @@ -3,8 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Hashtag from 'soapbox/components/hashtag'; import ScrollableList from 'soapbox/components/scrollable_list'; diff --git a/app/soapbox/features/compose/containers/poll_form_container.js b/app/soapbox/features/compose/containers/poll_form_container.js index a0b00709d..04c3b61ee 100644 --- a/app/soapbox/features/compose/containers/poll_form_container.js +++ b/app/soapbox/features/compose/containers/poll_form_container.js @@ -1,7 +1,11 @@ import { connect } from 'react-redux'; -import { addPollOption, removePollOption, changePollOption, changePollSettings, removePoll } from '../../../actions/compose'; import { + addPollOption, + removePollOption, + changePollOption, + changePollSettings, + removePoll, clearComposeSuggestions, fetchComposeSuggestions, selectComposeSuggestion, diff --git a/app/soapbox/features/compose/containers/upload_container.js b/app/soapbox/features/compose/containers/upload_container.js index 332f206ee..21b257fc8 100644 --- a/app/soapbox/features/compose/containers/upload_container.js +++ b/app/soapbox/features/compose/containers/upload_container.js @@ -1,8 +1,7 @@ import { List as ImmutableList } from 'immutable'; import { connect } from 'react-redux'; -import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose'; -import { submitCompose } from '../../../actions/compose'; +import { undoUploadCompose, changeUploadCompose, submitCompose } from '../../../actions/compose'; import { openModal } from '../../../actions/modals'; import Upload from '../components/upload'; diff --git a/app/soapbox/features/filters/index.js b/app/soapbox/features/filters/index.js index 00e32d5f0..f3ba42899 100644 --- a/app/soapbox/features/filters/index.js +++ b/app/soapbox/features/filters/index.js @@ -8,8 +8,7 @@ import { fetchFilters, createFilter, deleteFilter } from 'soapbox/actions/filter import snackbar from 'soapbox/actions/snackbar'; import Icon from 'soapbox/components/icon'; import ScrollableList from 'soapbox/components/scrollable_list'; -import { Button } from 'soapbox/components/ui'; -import { CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui'; +import { Button, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui'; import { FieldsGroup, Checkbox, diff --git a/app/soapbox/features/groups/removed_accounts/index.js b/app/soapbox/features/groups/removed_accounts/index.js index a6f0d990b..5227e0af0 100644 --- a/app/soapbox/features/groups/removed_accounts/index.js +++ b/app/soapbox/features/groups/removed_accounts/index.js @@ -3,8 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { Spinner } from 'soapbox/components/ui'; diff --git a/app/soapbox/features/list_adder/index.tsx b/app/soapbox/features/list_adder/index.tsx index 10d706519..191c71b75 100644 --- a/app/soapbox/features/list_adder/index.tsx +++ b/app/soapbox/features/list_adder/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { createSelector } from 'reselect'; diff --git a/app/soapbox/features/list_editor/index.tsx b/app/soapbox/features/list_editor/index.tsx index d495f4b2f..c2a784adc 100644 --- a/app/soapbox/features/list_editor/index.tsx +++ b/app/soapbox/features/list_editor/index.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { setupListEditor, resetListEditor } from 'soapbox/actions/lists'; diff --git a/app/soapbox/features/lists/index.tsx b/app/soapbox/features/lists/index.tsx index 1f3930c6c..187c05218 100644 --- a/app/soapbox/features/lists/index.tsx +++ b/app/soapbox/features/lists/index.tsx @@ -8,8 +8,7 @@ import { deleteList, fetchLists } from 'soapbox/actions/lists'; import { openModal } from 'soapbox/actions/modals'; import Icon from 'soapbox/components/icon'; import ScrollableList from 'soapbox/components/scrollable_list'; -import { IconButton, Spinner } from 'soapbox/components/ui'; -import { CardHeader, CardTitle } from 'soapbox/components/ui'; +import { CardHeader, CardTitle, IconButton, Spinner } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import Column from '../ui/components/column'; diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index 49b1da3af..26feb4b47 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { HotKeys } from 'react-hotkeys'; -import { defineMessages, FormattedMessage, IntlShape, MessageDescriptor } from 'react-intl'; -import { useIntl } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage, IntlShape, MessageDescriptor } from 'react-intl'; import { useHistory } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; diff --git a/app/soapbox/features/notifications/containers/column_settings_container.js b/app/soapbox/features/notifications/containers/column_settings_container.js index d21733b18..7e01f2fca 100644 --- a/app/soapbox/features/notifications/containers/column_settings_container.js +++ b/app/soapbox/features/notifications/containers/column_settings_container.js @@ -2,8 +2,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { openModal } from 'soapbox/actions/modals'; -import { setFilter } from 'soapbox/actions/notifications'; -import { clearNotifications } from 'soapbox/actions/notifications'; +import { clearNotifications, setFilter } from 'soapbox/actions/notifications'; import { changeAlerts as changePushNotifications } from 'soapbox/actions/push_notifications'; import { getSettings, changeSetting } from 'soapbox/actions/settings'; import { getFeatures } from 'soapbox/utils/features'; diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 1387fd2cf..203a9836e 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -1,8 +1,7 @@ import classNames from 'classnames'; import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl, WrappedComponentProps as IntlProps } from 'react-intl'; -import { FormattedDate } from 'react-intl'; +import { FormattedDate, FormattedMessage, injectIntl, WrappedComponentProps as IntlProps } from 'react-intl'; import Icon from 'soapbox/components/icon'; import StatusMedia from 'soapbox/components/status-media'; diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 39d768e9f..b3c4452e9 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -45,8 +45,9 @@ import { hideStatus, revealStatus, editStatus, + fetchStatusWithContext, + fetchNext, } from 'soapbox/actions/statuses'; -import { fetchStatusWithContext, fetchNext } from 'soapbox/actions/statuses'; import MissingIndicator from 'soapbox/components/missing_indicator'; import ScrollableList from 'soapbox/components/scrollable_list'; import { textForScreenReader, defaultMediaVisibility } from 'soapbox/components/status'; diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index f364cee68..dd2d8a6fc 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -4,8 +4,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Link } from 'react-router-dom'; -import { logOut, switchAccount } from 'soapbox/actions/auth'; -import { fetchOwnAccounts } from 'soapbox/actions/auth'; +import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth'; import Account from 'soapbox/components/account'; import { Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui'; import { useAppSelector, useFeatures } from 'soapbox/hooks'; diff --git a/app/soapbox/features/ui/components/timeline.tsx b/app/soapbox/features/ui/components/timeline.tsx index af3ff8e8c..109fa7dcb 100644 --- a/app/soapbox/features/ui/components/timeline.tsx +++ b/app/soapbox/features/ui/components/timeline.tsx @@ -3,8 +3,7 @@ import { debounce } from 'lodash'; import React, { useCallback } from 'react'; import { defineMessages } from 'react-intl'; -import { dequeueTimeline } from 'soapbox/actions/timelines'; -import { scrollTopTimeline } from 'soapbox/actions/timelines'; +import { dequeueTimeline, scrollTopTimeline } from 'soapbox/actions/timelines'; import ScrollTopButton from 'soapbox/components/scroll-top-button'; import StatusList, { IStatusList } from 'soapbox/components/status_list'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index c64e26095..9520320e2 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -29,13 +29,11 @@ import AdminPage from 'soapbox/pages/admin_page'; import DefaultPage from 'soapbox/pages/default_page'; // import GroupsPage from 'soapbox/pages/groups_page'; // import GroupPage from 'soapbox/pages/group_page'; -import EmptyPage from 'soapbox/pages/default_page'; import HomePage from 'soapbox/pages/home_page'; import ProfilePage from 'soapbox/pages/profile_page'; import RemoteInstancePage from 'soapbox/pages/remote_instance_page'; import StatusPage from 'soapbox/pages/status_page'; -import { getAccessToken } from 'soapbox/utils/auth'; -import { getVapidKey } from 'soapbox/utils/auth'; +import { getAccessToken, getVapidKey } from 'soapbox/utils/auth'; import { cacheCurrentUrl } from 'soapbox/utils/redirect'; import { isStandalone } from 'soapbox/utils/state'; // import GroupSidebarPanel from '../groups/sidebar_panel'; @@ -120,6 +118,8 @@ import { WrappedRoute } from './util/react_router_helpers'; // Without this it ends up in ~8 very commonly used bundles. import 'soapbox/components/status'; +const EmptyPage = HomePage; + const isMobile = (width: number): boolean => width <= 1190; const messages = defineMessages({ diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index 400f6b6c4..8b76dce34 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -8,8 +8,7 @@ import { logIn, verifyCredentials } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; import { startOnboarding } from 'soapbox/actions/onboarding'; import snackbar from 'soapbox/actions/snackbar'; -import { createAccount } from 'soapbox/actions/verification'; -import { removeStoredVerification } from 'soapbox/actions/verification'; +import { createAccount, removeStoredVerification } from 'soapbox/actions/verification'; import { Button, Form, FormGroup, Input } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import { getRedirectUrl } from 'soapbox/utils/redirect'; diff --git a/app/soapbox/features/verification/steps/email-verification.js b/app/soapbox/features/verification/steps/email-verification.js index fba2862c1..05e463db3 100644 --- a/app/soapbox/features/verification/steps/email-verification.js +++ b/app/soapbox/features/verification/steps/email-verification.js @@ -4,8 +4,7 @@ import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import snackbar from 'soapbox/actions/snackbar'; -import { checkEmailVerification, requestEmailVerification } from 'soapbox/actions/verification'; -import { postEmailVerification } from 'soapbox/actions/verification'; +import { checkEmailVerification, postEmailVerification, requestEmailVerification } from 'soapbox/actions/verification'; import Icon from 'soapbox/components/icon'; import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui'; diff --git a/app/soapbox/reducers/contexts.ts b/app/soapbox/reducers/contexts.ts index bfc830c93..73cbd825e 100644 --- a/app/soapbox/reducers/contexts.ts +++ b/app/soapbox/reducers/contexts.ts @@ -11,10 +11,10 @@ import { ACCOUNT_MUTE_SUCCESS, } from '../actions/accounts'; import { + CONTEXT_FETCH_SUCCESS, STATUS_CREATE_REQUEST, STATUS_CREATE_SUCCESS, } from '../actions/statuses'; -import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; import type { ReducerStatus } from './statuses'; From a02efcadd4002f456c097a2e9d6488b204a6e0b2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 7 Jun 2022 16:56:45 -0500 Subject: [PATCH 07/15] Preserve password reset URL query --- app/soapbox/features/auth_layout/index.tsx | 5 +++-- app/soapbox/features/ui/index.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/auth_layout/index.tsx b/app/soapbox/features/auth_layout/index.tsx index ca72f4107..152236381 100644 --- a/app/soapbox/features/auth_layout/index.tsx +++ b/app/soapbox/features/auth_layout/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { Link, Redirect, Route, Switch, useHistory } from 'react-router-dom'; +import { Link, Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom'; import LandingGradient from 'soapbox/components/landing-gradient'; import SiteLogo from 'soapbox/components/site-logo'; @@ -23,6 +23,7 @@ const messages = defineMessages({ const AuthLayout = () => { const intl = useIntl(); const history = useHistory(); + const { search } = useLocation(); const siteTitle = useAppSelector(state => state.instance.title); const soapboxConfig = useSoapboxConfig(); @@ -76,7 +77,7 @@ const AuthLayout = () => { - + diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 9520320e2..c1057950c 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -5,7 +5,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { HotKeys } from 'react-hotkeys'; import { defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; -import { Switch, useHistory, matchPath, Redirect } from 'react-router-dom'; +import { Switch, useHistory, useLocation, matchPath, Redirect } from 'react-router-dom'; import { fetchFollowRequests } from 'soapbox/actions/accounts'; import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin'; @@ -156,8 +156,8 @@ const keyMap = { }; const SwitchingColumnsArea: React.FC = ({ children }) => { - const history = useHistory(); const features = useFeatures(); + const { search } = useLocation(); const { authenticatedProfile, cryptoAddresses } = useSoapboxConfig(); const hasCrypto = cryptoAddresses.size > 0; @@ -232,7 +232,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { - + @@ -247,7 +247,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { - + From 1a7a1cc8023d7e3a9303a246cf012878b521e32f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 Jun 2022 10:56:27 -0500 Subject: [PATCH 08/15] QuotedStatus: add useState type --- app/soapbox/features/status/components/quoted_status.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/status/components/quoted_status.tsx b/app/soapbox/features/status/components/quoted_status.tsx index 6c0fae410..33ce023c4 100644 --- a/app/soapbox/features/status/components/quoted_status.tsx +++ b/app/soapbox/features/status/components/quoted_status.tsx @@ -32,7 +32,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => const settings = useSettings(); const displayMedia = settings.get('displayMedia'); - const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); + const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); const handleExpandClick = (e: React.MouseEvent) => { if (!status) return; From 165a4cc469ab6222ac5c77dbbd388f49f21d58f0 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 8 Jun 2022 12:05:41 -0400 Subject: [PATCH 09/15] Add new Datepicker component --- .../datepicker/__tests__/datepicker.test.tsx | 83 ++++++++++++++++ .../components/ui/datepicker/datepicker.tsx | 94 +++++++++++++++++++ app/soapbox/components/ui/index.ts | 1 + app/soapbox/locales/en.json | 3 + 4 files changed, 181 insertions(+) create mode 100644 app/soapbox/components/ui/datepicker/__tests__/datepicker.test.tsx create mode 100644 app/soapbox/components/ui/datepicker/datepicker.tsx diff --git a/app/soapbox/components/ui/datepicker/__tests__/datepicker.test.tsx b/app/soapbox/components/ui/datepicker/__tests__/datepicker.test.tsx new file mode 100644 index 000000000..5fe6ca9d6 --- /dev/null +++ b/app/soapbox/components/ui/datepicker/__tests__/datepicker.test.tsx @@ -0,0 +1,83 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { queryAllByRole, render, screen } from '../../../../jest/test-helpers'; +import Datepicker from '../datepicker'; + +describe('', () => { + it('defaults to the current date', () => { + const handler = jest.fn(); + render(); + const today = new Date(); + + expect(screen.getByTestId('datepicker-month')).toHaveValue(String(today.getMonth())); + expect(screen.getByTestId('datepicker-day')).toHaveValue(String(today.getDate())); + expect(screen.getByTestId('datepicker-year')).toHaveValue(String(today.getFullYear())); + }); + + it('changes number of days based on selected month and year', async() => { + const handler = jest.fn(); + render(); + + await userEvent.selectOptions( + screen.getByTestId('datepicker-month'), + screen.getByRole('option', { name: 'February' }), + ); + + await userEvent.selectOptions( + screen.getByTestId('datepicker-year'), + screen.getByRole('option', { name: '2020' }), + ); + + let daySelect: HTMLElement; + daySelect = document.querySelector('[data-testid="datepicker-day"]'); + expect(queryAllByRole(daySelect, 'option')).toHaveLength(29); + + await userEvent.selectOptions( + screen.getByTestId('datepicker-year'), + screen.getByRole('option', { name: '2021' }), + ); + + daySelect = document.querySelector('[data-testid="datepicker-day"]') as HTMLElement; + expect(queryAllByRole(daySelect, 'option')).toHaveLength(28); + }); + + it('ranges from the current year to 120 years ago', () => { + const handler = jest.fn(); + render(); + const today = new Date(); + + const yearSelect = document.querySelector('[data-testid="datepicker-year"]') as HTMLElement; + expect(queryAllByRole(yearSelect, 'option')).toHaveLength(121); + expect(queryAllByRole(yearSelect, 'option')[0]).toHaveValue(String(today.getFullYear())); + expect(queryAllByRole(yearSelect, 'option')[120]).toHaveValue(String(today.getFullYear() - 120)); + }); + + it('calls the onChange function when the inputs change', async() => { + const handler = jest.fn(); + render(); + + expect(handler.mock.calls.length).toEqual(1); + + await userEvent.selectOptions( + screen.getByTestId('datepicker-month'), + screen.getByRole('option', { name: 'February' }), + ); + + expect(handler.mock.calls.length).toEqual(2); + + await userEvent.selectOptions( + screen.getByTestId('datepicker-year'), + screen.getByRole('option', { name: '2020' }), + ); + + expect(handler.mock.calls.length).toEqual(3); + + await userEvent.selectOptions( + screen.getByTestId('datepicker-day'), + screen.getByRole('option', { name: '5' }), + ); + + expect(handler.mock.calls.length).toEqual(4); + }); +}); diff --git a/app/soapbox/components/ui/datepicker/datepicker.tsx b/app/soapbox/components/ui/datepicker/datepicker.tsx new file mode 100644 index 000000000..3c3c9b8e2 --- /dev/null +++ b/app/soapbox/components/ui/datepicker/datepicker.tsx @@ -0,0 +1,94 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; + +import Select from '../select/select'; +import Stack from '../stack/stack'; +import Text from '../text/text'; + +const getDaysInMonth = (month: number, year: number) => new Date(year, month + 1, 0).getDate(); +const currentYear = new Date().getFullYear(); + +interface IDatepicker { + onChange(date: Date): void +} + +/** + * Datepicker that allows a user to select month, day, and year. + */ +const Datepicker = ({ onChange }: IDatepicker) => { + const intl = useIntl(); + + const [month, setMonth] = useState(new Date().getMonth()); + const [day, setDay] = useState(new Date().getDate()); + const [year, setYear] = useState(2022); + + const numberOfDays = useMemo(() => { + return getDaysInMonth(month, year); + }, [month, year]); + + useEffect(() => { + onChange(new Date(year, month, day)); + }, [month, day, year]); + + return ( +
+
+ + + + + + + +
+ +
+ + + + + + + +
+ +
+ + + + + + + +
+
+ ); +}; + +export default Datepicker; diff --git a/app/soapbox/components/ui/index.ts b/app/soapbox/components/ui/index.ts index 24324ee02..27acc184b 100644 --- a/app/soapbox/components/ui/index.ts +++ b/app/soapbox/components/ui/index.ts @@ -4,6 +4,7 @@ export { Card, CardBody, CardHeader, CardTitle } from './card/card'; export { default as Checkbox } from './checkbox/checkbox'; export { default as Column } from './column/column'; export { default as Counter } from './counter/counter'; +export { default as Datepicker } from './datepicker/datepicker'; export { default as Emoji } from './emoji/emoji'; export { default as EmojiSelector } from './emoji-selector/emoji-selector'; export { default as FileInput } from './file-input/file-input'; diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index e6267ac94..74957a487 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -344,6 +344,9 @@ "crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}", "crypto_donate_panel.heading": "Donate Cryptocurrency", "crypto_donate_panel.intro.message": "{siteTitle} accepts cryptocurrency donations to fund our service. Thank you for your support!", + "datepicker.month": "Month", + "datepicker.day": "Day", + "datepicker.year": "Year", "datepicker.hint": "Scheduled to post at…", "datepicker.next_month": "Next month", "datepicker.next_year": "Next year", From 5900068144073f6fbbdab8df8d936b91dd7e5ed0 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 8 Jun 2022 12:06:05 -0400 Subject: [PATCH 10/15] Use new Datepicker on AgeVerification step --- app/soapbox/components/ui/select/select.tsx | 8 ++++-- .../verification/steps/age-verification.js | 26 +++---------------- app/styles/forms.scss | 2 ++ 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/app/soapbox/components/ui/select/select.tsx b/app/soapbox/components/ui/select/select.tsx index 485b3238c..f79d71ddd 100644 --- a/app/soapbox/components/ui/select/select.tsx +++ b/app/soapbox/components/ui/select/select.tsx @@ -1,13 +1,17 @@ import * as React from 'react'; +interface ISelect extends React.SelectHTMLAttributes { + children: Iterable, +} + /** Multiple-select dropdown. */ -const Select = React.forwardRef((props, ref) => { +const Select = React.forwardRef((props, ref) => { const { children, ...filteredProps } = props; return (