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/__tests__/quoted-status.test.tsx b/app/soapbox/components/__tests__/quoted-status.test.tsx new file mode 100644 index 000000000..208a913b2 --- /dev/null +++ b/app/soapbox/components/__tests__/quoted-status.test.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { render, screen, rootState } from '../../jest/test-helpers'; +import { normalizeStatus, normalizeAccount } from '../../normalizers'; +import QuotedStatus from '../quoted-status'; + +describe('', () => { + it('renders content', () => { + const account = normalizeAccount({ + id: '1', + acct: 'alex', + }); + + const status = normalizeStatus({ + id: '1', + account, + content: 'hello world', + contentHtml: 'hello world', + }); + + const state = rootState.setIn(['accounts', '1', account]); + + render(, null, state); + screen.getByText(/hello world/i); + expect(screen.getByTestId('quoted-status')).toHaveTextContent(/hello world/i); + }); +}); 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/features/status/components/quoted_status.tsx b/app/soapbox/components/quoted-status.tsx similarity index 85% rename from app/soapbox/features/status/components/quoted_status.tsx rename to app/soapbox/components/quoted-status.tsx index bf8069aa8..5d6eb526e 100644 --- a/app/soapbox/features/status/components/quoted_status.tsx +++ b/app/soapbox/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; @@ -113,6 +124,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => return ( = ({ status, onCancel, compose }) => dangerouslySetInnerHTML={{ __html: status.contentHtml }} /> - + ); }; 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.tsx b/app/soapbox/components/status.tsx index 2b1325a60..4389b1fc1 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, @@ -431,7 +422,7 @@ class Status extends ImmutablePureComponent { ); } else { - quote = ; + quote = ; } } 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/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 ( + + + + + + + + setMonth(Number(event.target.value))} + data-testid='datepicker-month' + > + {[...Array(12)].map((_, idx) => ( + + {intl.formatDate(new Date(year, idx, 1), { month: 'long' })} + + ))} + + + + + + + + + + + setDay(Number(event.target.value))} + data-testid='datepicker-day' + > + {[...Array(numberOfDays)].map((_, idx) => ( + {idx + 1} + ))} + + + + + + + + + + + setYear(Number(event.target.value))} + data-testid='datepicker-year' + > + {[...Array(121)].map((_, idx) => ( + {currentYear - idx} + ))} + + + + + ); +}; + +export default Datepicker; 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/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/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 ( {children} 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/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/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.tsx b/app/soapbox/features/compose/components/search_results.tsx index 91e7ff287..0b53ea026 100644 --- a/app/soapbox/features/compose/components/search_results.tsx +++ b/app/soapbox/features/compose/components/search_results.tsx @@ -1,8 +1,6 @@ import classNames from 'classnames'; -import React from 'react'; -import { useEffect } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; -import { defineMessages } from 'react-intl'; +import React, { useEffect } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { expandSearch, setFilter } from 'soapbox/actions/search'; import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses'; 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..e1c7b48ba --- /dev/null +++ b/app/soapbox/features/compose/components/sensitive-button.tsx @@ -0,0 +1,42 @@ +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({ + 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 ( + + } + labelTitle={intl.formatMessage(active ? messages.marked : messages.unmarked)} + > + + + + ); +}; + +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/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/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..923ee7baa --- /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/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/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)); 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..640b3d5d2 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'; @@ -116,7 +115,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..f64ad2255 --- /dev/null +++ b/app/soapbox/features/status/containers/quoted_status_container.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import QuotedStatus from 'soapbox/components/quoted-status'; +import { useAppSelector } from 'soapbox/hooks'; +import { makeGetStatus } from 'soapbox/selectors'; + +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/status/index.tsx b/app/soapbox/features/status/index.tsx index 39d768e9f..7d569190c 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -45,17 +45,19 @@ 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'; +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/features/ui/components/modals/report-modal/report-modal.tsx b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx index 2522b9f36..cb4181eaa 100644 --- a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx +++ b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx @@ -1,5 +1,4 @@ import { AxiosError } from 'axios'; -import { Set as ImmutableSet } from 'immutable'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; 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 */} 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..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'; @@ -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({ @@ -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 }) => { - + 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/__tests__/age-verification.test.js b/app/soapbox/features/verification/steps/__tests__/age-verification.test.js index 122df77bd..7dd835313 100644 --- a/app/soapbox/features/verification/steps/__tests__/age-verification.test.js +++ b/app/soapbox/features/verification/steps/__tests__/age-verification.test.js @@ -39,7 +39,10 @@ describe('', () => { store, ); - await userEvent.type(screen.getByLabelText('Birth Date'), '{enter}'); + await userEvent.selectOptions( + screen.getByTestId('datepicker-year'), + screen.getByRole('option', { name: '2020' }), + ); fireEvent.submit( screen.getByRole('button'), { diff --git a/app/soapbox/features/verification/steps/age-verification.js b/app/soapbox/features/verification/steps/age-verification.js index caeed7704..71b33772d 100644 --- a/app/soapbox/features/verification/steps/age-verification.js +++ b/app/soapbox/features/verification/steps/age-verification.js @@ -1,12 +1,11 @@ import PropTypes from 'prop-types'; import * as React from 'react'; -import DatePicker from 'react-datepicker'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import snackbar from 'soapbox/actions/snackbar'; import { verifyAge } from 'soapbox/actions/verification'; -import { Button, Form, FormGroup, Text } from 'soapbox/components/ui'; +import { Button, Datepicker, Form, Text } from 'soapbox/components/ui'; const messages = defineMessages({ fail: { @@ -23,13 +22,6 @@ function meetsAgeMinimum(birthday, ageMinimum) { return new Date(year + ageMinimum, month, day) <= new Date(); } -function getMaximumDate(ageMinimum) { - const date = new Date(); - date.setUTCFullYear(date.getUTCFullYear() - ageMinimum); - - return date; -} - const AgeVerification = () => { const intl = useIntl(); const dispatch = useDispatch(); @@ -67,21 +59,9 @@ const AgeVerification = () => { - + - - - + {siteTitle} requires users to be at least {ageMinimum} years old to 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/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", 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'; diff --git a/app/soapbox/utils/__tests__/status-test.js b/app/soapbox/utils/__tests__/status-test.js index 0dcb3e78a..4556382de 100644 --- a/app/soapbox/utils/__tests__/status-test.js +++ b/app/soapbox/utils/__tests__/status-test.js @@ -2,7 +2,10 @@ import { fromJS } from 'immutable'; import { normalizeStatus } from 'soapbox/normalizers/status'; -import { hasIntegerMediaIds } from '../status'; +import { + hasIntegerMediaIds, + defaultMediaVisibility, +} from '../status'; describe('hasIntegerMediaIds()', () => { it('returns true for a Pleroma deleted status', () => { @@ -10,3 +13,24 @@ describe('hasIntegerMediaIds()', () => { expect(hasIntegerMediaIds(status)).toBe(true); }); }); + +describe('defaultMediaVisibility()', () => { + it('returns false with no status', () => { + expect(defaultMediaVisibility(undefined, 'default')).toBe(false); + }); + + it('hides sensitive media by default', () => { + const status = normalizeStatus({ sensitive: true }); + expect(defaultMediaVisibility(status, 'default')).toBe(false); + }); + + it('hides media when displayMedia is hide_all', () => { + const status = normalizeStatus({}); + expect(defaultMediaVisibility(status, 'hide_all')).toBe(false); + }); + + it('shows sensitive media when displayMedia is show_all', () => { + const status = normalizeStatus({ sensitive: true }); + expect(defaultMediaVisibility(status, 'show_all')).toBe(true); + }); +}); 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 { diff --git a/app/styles/forms.scss b/app/styles/forms.scss index df6888ad5..0194f4a42 100644 --- a/app/styles/forms.scss +++ b/app/styles/forms.scss @@ -1,5 +1,6 @@ select { @apply pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); } .form-error::before,