diff --git a/app/soapbox/actions/__tests__/accounts.test.ts b/app/soapbox/actions/__tests__/accounts.test.ts index 310e42a1b..3b1abbf9f 100644 --- a/app/soapbox/actions/__tests__/accounts.test.ts +++ b/app/soapbox/actions/__tests__/accounts.test.ts @@ -148,34 +148,24 @@ describe('fetchAccountByUsername()', () => { const username = 'tiger'; let state, account; - describe('when the account has already been cached in redux', () => { - beforeEach(() => { - account = normalizeAccount({ - id, - acct: username, - display_name: 'Tiger', - avatar: 'test.jpg', - birthday: undefined, - }); - - state = rootReducer(undefined, {}) - .set('accounts', ImmutableMap({ - [id]: account, - })); - - store = mockStore(state); - - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}`).reply(200, account); - }); + beforeEach(() => { + account = normalizeAccount({ + id, + acct: username, + display_name: 'Tiger', + avatar: 'test.jpg', + birthday: undefined, }); - it('should return null', async() => { - const result = await store.dispatch(fetchAccountByUsername(username)); - const actions = store.getActions(); + state = rootReducer(undefined, {}) + .set('accounts', ImmutableMap({ + [id]: account, + })); - expect(actions).toEqual([]); - expect(result).toBeNull(); + store = mockStore(state); + + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${id}`).reply(200, account); }); }); diff --git a/app/soapbox/actions/__tests__/alerts.test.ts b/app/soapbox/actions/__tests__/alerts.test.ts index 52e0c8b5b..f2419893a 100644 --- a/app/soapbox/actions/__tests__/alerts.test.ts +++ b/app/soapbox/actions/__tests__/alerts.test.ts @@ -1,10 +1,10 @@ +import { AxiosError } from 'axios'; + import { mockStore } from 'soapbox/jest/test-helpers'; import rootReducer from 'soapbox/reducers'; import { dismissAlert, showAlert, showAlertForError } from '../alerts'; -import type { AxiosError } from 'axios'; - const buildError = (message: string, status: number) => new AxiosError(message, String(status), null, null, { data: { error: message, diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 5447afa9c..0fbb5c1a5 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -117,6 +117,13 @@ export const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUES export const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS'; export const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL'; +const maybeRedirectLogin = (error, history) => { + // The client is unauthorized - redirect to login. + if (history && error?.response?.status === 401) { + history.push('/login'); + } +}; + export function createAccount(params) { return (dispatch, getState) => { dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); @@ -153,19 +160,10 @@ export function fetchAccount(id) { }; } -export function fetchAccountByUsername(username) { +export function fetchAccountByUsername(username, history) { return (dispatch, getState) => { - const state = getState(); - const account = state.get('accounts').find(account => account.get('acct') === username); - - if (account) { - dispatch(fetchAccount(account.get('id'))); - return null; - } - - const instance = state.get('instance'); + const { instance, me } = getState(); const features = getFeatures(instance); - const me = state.get('me'); if (features.accountByUsername && (me || !features.accountLookup)) { return api(getState).get(`/api/v1/accounts/${username}`).then(response => { @@ -182,6 +180,7 @@ export function fetchAccountByUsername(username) { }).catch(error => { dispatch(fetchAccountFail(null, error)); dispatch(importErrorWhileFetchingAccountByUsername(username)); + maybeRedirectLogin(error, history); }); } else { return dispatch(accountSearch({ diff --git a/app/soapbox/components/autosuggest_input.tsx b/app/soapbox/components/autosuggest_input.tsx index cac979337..cd2c74a07 100644 --- a/app/soapbox/components/autosuggest_input.tsx +++ b/app/soapbox/components/autosuggest_input.tsx @@ -20,7 +20,7 @@ export type AutoSuggestion = string | Emoji; const textAtCursorMatchesToken = (str: string, caretPosition: number, searchTokens: string[]): CursorMatch => { let word: string; - const left: number = str.slice(0, caretPosition).search(/\S+$/); + const left: number = str.slice(0, caretPosition).search(/\S+$/); const right: number = str.slice(caretPosition).search(/\s/); if (right < 0) { @@ -201,13 +201,13 @@ export default class AutosuggestInput extends ImmutablePureComponent; - key = suggestion.id; + key = suggestion.id; } else if (suggestion[0] === '#') { inner = suggestion; - key = suggestion; + key = suggestion; } else { inner = ; - key = suggestion; + key = suggestion; } return ( @@ -279,13 +279,13 @@ export default class AutosuggestInput extends ImmutablePureComponent +
', () => { + it('renders without text', () => { + render(); + + expect(screen.queryAllByTestId('divider-text')).toHaveLength(0); + }); + + it('renders text', () => { + const text = 'Hello'; + render(); + + expect(screen.getByTestId('divider-text')).toHaveTextContent(text); + }); +}); diff --git a/app/soapbox/components/ui/divider/divider.tsx b/app/soapbox/components/ui/divider/divider.tsx new file mode 100644 index 000000000..0670be123 --- /dev/null +++ b/app/soapbox/components/ui/divider/divider.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +interface IDivider { + text?: string +} + +/** Divider */ +const Divider = ({ text }: IDivider) => ( +
+ +); + +export default Divider; diff --git a/app/soapbox/components/ui/index.ts b/app/soapbox/components/ui/index.ts index 27acc184b..cd48426b6 100644 --- a/app/soapbox/components/ui/index.ts +++ b/app/soapbox/components/ui/index.ts @@ -5,6 +5,7 @@ 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 Divider } from './divider/divider'; 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/input/input.tsx b/app/soapbox/components/ui/input/input.tsx index 183b40b04..8ba0d6ff9 100644 --- a/app/soapbox/components/ui/input/input.tsx +++ b/app/soapbox/components/ui/input/input.tsx @@ -64,7 +64,7 @@ const Input = React.forwardRef( type={revealed ? 'text' : type} ref={ref} className={classNames({ - 'dark:bg-slate-800 dark:text-white block w-full sm:text-sm border-gray-300 dark:border-gray-600 rounded-md focus:ring-indigo-500 focus:border-indigo-500': + 'dark:bg-slate-800 dark:text-white block w-full sm:text-sm border-gray-300 dark:border-gray-600 rounded-md focus:ring-primary-500 focus:border-primary-500': true, 'pr-7': isPassword, 'text-red-600 border-red-600': hasError, diff --git a/app/soapbox/features/account_timeline/index.js b/app/soapbox/features/account_timeline/index.js index 18b7d3107..907c54d68 100644 --- a/app/soapbox/features/account_timeline/index.js +++ b/app/soapbox/features/account_timeline/index.js @@ -5,8 +5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; -import { fetchAccount, fetchAccountByUsername } from 'soapbox/actions/accounts'; +import { fetchAccountByUsername } from 'soapbox/actions/accounts'; import { fetchPatronAccount } from 'soapbox/actions/patron'; import { getSettings } from 'soapbox/actions/settings'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; @@ -67,6 +68,7 @@ const makeMapStateToProps = () => { }; export default @connect(makeMapStateToProps) +@withRouter class AccountTimeline extends ImmutablePureComponent { static propTypes = { @@ -82,11 +84,11 @@ class AccountTimeline extends ImmutablePureComponent { }; componentDidMount() { - const { params: { username }, accountId, accountApId, withReplies, patronEnabled } = this.props; + const { params: { username }, accountId, accountApId, withReplies, patronEnabled, history } = this.props; + + this.props.dispatch(fetchAccountByUsername(username, history)); if (accountId && accountId !== -1) { - this.props.dispatch(fetchAccount(accountId)); - if (!withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } @@ -96,17 +98,17 @@ class AccountTimeline extends ImmutablePureComponent { } this.props.dispatch(expandAccountTimeline(accountId, { withReplies })); - } else { - this.props.dispatch(fetchAccountByUsername(username)); } } componentDidUpdate(prevProps) { - const { params: { username }, accountId, withReplies, accountApId, patronEnabled } = this.props; + const { params: { username }, accountId, withReplies, accountApId, patronEnabled, history } = this.props; + + if (username && (username !== prevProps.params.username)) { + this.props.dispatch(fetchAccountByUsername(username, history)); + } if (accountId && (accountId !== -1) && (accountId !== prevProps.accountId) || withReplies !== prevProps.withReplies) { - this.props.dispatch(fetchAccount(accountId)); - if (!withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } @@ -116,8 +118,6 @@ class AccountTimeline extends ImmutablePureComponent { } this.props.dispatch(expandAccountTimeline(accountId, { withReplies })); - } else if (username && (username !== prevProps.params.username)) { - this.props.dispatch(fetchAccountByUsername(username)); } } diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js index 9700c03f4..31e80741c 100644 --- a/app/soapbox/features/compose/components/compose_form.js +++ b/app/soapbox/features/compose/components/compose_form.js @@ -38,6 +38,7 @@ const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u20 const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' }, + pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic...' }, spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, publish: { id: 'compose_form.publish', defaultMessage: 'Post' }, publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, @@ -62,6 +63,7 @@ class ComposeForm extends ImmutablePureComponent { spoilerText: PropTypes.string, focusDate: PropTypes.instanceOf(Date), caretPosition: PropTypes.number, + hasPoll: PropTypes.bool, isSubmitting: PropTypes.bool, isChangingUpload: PropTypes.bool, isEditing: PropTypes.bool, @@ -340,7 +342,7 @@ class ComposeForm extends ImmutablePureComponent { { + onShowDropdown = (e) => { + e.stopPropagation(); + this.setState({ active: true }); if (!EmojiPicker) { @@ -317,7 +319,7 @@ class EmojiPickerDropdown extends React.PureComponent { }); } - const { top } = target.getBoundingClientRect(); + const { top } = e.target.getBoundingClientRect(); this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); } diff --git a/app/soapbox/features/compose/components/poll-form.tsx b/app/soapbox/features/compose/components/poll-form.tsx index b0116ffef..06d178518 100644 --- a/app/soapbox/features/compose/components/poll-form.tsx +++ b/app/soapbox/features/compose/components/poll-form.tsx @@ -1,32 +1,33 @@ 'use strict'; -import classNames from 'classnames'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import AutosuggestInput from 'soapbox/components/autosuggest_input'; -import Icon from 'soapbox/components/icon'; -import IconButton from 'soapbox/components/icon_button'; -import { HStack } from 'soapbox/components/ui'; +import { Button, Divider, HStack, Stack, Text, Toggle } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; +import DurationSelector from './polls/duration-selector'; + import type { AutoSuggestion } from 'soapbox/components/autosuggest_input'; const messages = defineMessages({ - option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' }, - add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, - remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, - poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, - switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' }, - switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' }, + option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Answer #{number}' }, + add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add an answer' }, + remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this answer' }, + pollDuration: { id: 'compose_form.poll.duration', defaultMessage: 'Duration' }, + removePoll: { id: 'compose_form.poll.remove', defaultMessage: 'Remove poll' }, + switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple answers' }, + switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single answer' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, + multiSelect: { id: 'compose_form.poll.multiselect', defaultMessage: 'Multi-Select' }, + multiSelectDetail: { id: 'compose_form.poll.multiselect_detail', defaultMessage: 'Allow users to select multiple answers' }, }); interface IOption { index: number - isPollMultiple?: boolean maxChars: number numOptions: number onChange(index: number, value: string): void @@ -35,7 +36,6 @@ interface IOption { onRemove(index: number): void onRemovePoll(): void onSuggestionSelected(tokenStart: number, token: string, value: string, key: (string | number)[]): void - onToggleMultiple(): void suggestions?: any // list title: string } @@ -43,7 +43,6 @@ interface IOption { const Option = (props: IOption) => { const { index, - isPollMultiple, maxChars, numOptions, onChange, @@ -51,7 +50,6 @@ const Option = (props: IOption) => { onFetchSuggestions, onRemove, onRemovePoll, - onToggleMultiple, suggestions, title, } = props; @@ -68,21 +66,8 @@ const Option = (props: IOption) => { } }; - const handleToggleMultiple = (event: React.MouseEvent | React.KeyboardEvent) => { - event.preventDefault(); - event.stopPropagation(); - - onToggleMultiple(); - }; - const onSuggestionsClearRequested = () => onClearSuggestions(); - const handleCheckboxKeypress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - handleToggleMultiple(event); - } - }; - const onSuggestionsFetchRequested = (token: string) => onFetchSuggestions(token); const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => { @@ -92,19 +77,14 @@ const Option = (props: IOption) => { }; return ( -
  • - + -
    - -
    -
  • + {index > 1 && ( +
    + +
    + )} + ); }; @@ -163,10 +141,7 @@ const PollForm = (props: IPollForm) => { const maxOptionChars = pollLimits.get('max_characters_per_option'); const handleAddOption = () => onAddOption(''); - - const handleSelectDuration = (event: React.ChangeEvent) => - onChangeSettings(event.target.value, isMultiple); - + const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple); const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); if (!options) { @@ -174,8 +149,8 @@ const PollForm = (props: IPollForm) => { } return ( -
    -
      + + {options.map((title: string, i: number) => (
    - - {options.size < maxOptions && ( - - )} + +
    - - -
    + {options.size < maxOptions && ( + + )} +
    + + + + + + + + + {/* Duration */} + + + {intl.formatMessage(messages.pollDuration)} + + + + + + {/* Remove Poll */} +
    + +
    + ); }; diff --git a/app/soapbox/features/compose/components/polls/__tests__/duration-selector.test.tsx b/app/soapbox/features/compose/components/polls/__tests__/duration-selector.test.tsx new file mode 100644 index 000000000..cf689ab43 --- /dev/null +++ b/app/soapbox/features/compose/components/polls/__tests__/duration-selector.test.tsx @@ -0,0 +1,77 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { render, screen } from '../../../../../jest/test-helpers'; +import DurationSelector from '../duration-selector'; + +describe('', () => { + it('defaults to 2 days', () => { + const handler = jest.fn(); + render(); + + expect(screen.getByTestId('duration-selector-days')).toHaveValue('2'); + expect(screen.getByTestId('duration-selector-hours')).toHaveValue('0'); + expect(screen.getByTestId('duration-selector-minutes')).toHaveValue('0'); + }); + + describe('when changing the day', () => { + it('calls the "onDurationChange" callback', async() => { + const handler = jest.fn(); + render(); + + await userEvent.selectOptions( + screen.getByTestId('duration-selector-days'), + screen.getByRole('option', { name: '1 day' }), + ); + + expect(handler.mock.calls[0][0]).toEqual(172800); // 2 days + expect(handler.mock.calls[1][0]).toEqual(86400); // 1 day + }); + + it('should disable the hour/minute select if 7 days selected', async() => { + const handler = jest.fn(); + render(); + + expect(screen.getByTestId('duration-selector-hours')).not.toBeDisabled(); + expect(screen.getByTestId('duration-selector-minutes')).not.toBeDisabled(); + + await userEvent.selectOptions( + screen.getByTestId('duration-selector-days'), + screen.getByRole('option', { name: '7 days' }), + ); + + expect(screen.getByTestId('duration-selector-hours')).toBeDisabled(); + expect(screen.getByTestId('duration-selector-minutes')).toBeDisabled(); + }); + }); + + describe('when changing the hour', () => { + it('calls the "onDurationChange" callback', async() => { + const handler = jest.fn(); + render(); + + await userEvent.selectOptions( + screen.getByTestId('duration-selector-hours'), + screen.getByRole('option', { name: '1 hour' }), + ); + + expect(handler.mock.calls[0][0]).toEqual(172800); // 2 days + expect(handler.mock.calls[1][0]).toEqual(176400); // 2 days, 1 hour + }); + }); + + describe('when changing the minute', () => { + it('calls the "onDurationChange" callback', async() => { + const handler = jest.fn(); + render(); + + await userEvent.selectOptions( + screen.getByTestId('duration-selector-minutes'), + screen.getByRole('option', { name: '15 minutes' }), + ); + + expect(handler.mock.calls[0][0]).toEqual(172800); // 2 days + expect(handler.mock.calls[1][0]).toEqual(173700); // 2 days, 1 minute + }); + }); +}); diff --git a/app/soapbox/features/compose/components/polls/duration-selector.tsx b/app/soapbox/features/compose/components/polls/duration-selector.tsx new file mode 100644 index 000000000..491530d22 --- /dev/null +++ b/app/soapbox/features/compose/components/polls/duration-selector.tsx @@ -0,0 +1,93 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { Select } from 'soapbox/components/ui'; + +const messages = defineMessages({ + minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, + hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, + days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, +}); + +interface IDurationSelector { + onDurationChange(expiresIn: number): void +} + +const DurationSelector = ({ onDurationChange }: IDurationSelector) => { + const intl = useIntl(); + + const [days, setDays] = useState(2); + const [hours, setHours] = useState(0); + const [minutes, setMinutes] = useState(0); + + const value = useMemo(() => { + const now: any = new Date(); + const future: any = new Date(); + now.setDate(now.getDate() + days); + now.setMinutes(now.getMinutes() + minutes); + now.setHours(now.getHours() + hours); + + return (now - future) / 1000; + }, [days, hours, minutes]); + + useEffect(() => { + if (days === 7) { + setHours(0); + setMinutes(0); + } + }, [days]); + + useEffect(() => { + onDurationChange(value); + }, [value]); + + return ( +
    +
    + +
    + +
    + +
    + +
    + +
    +
    + ); +}; + +export default DurationSelector; diff --git a/app/soapbox/features/compose/containers/compose_form_container.js b/app/soapbox/features/compose/containers/compose_form_container.js index c4928065e..d54f1d082 100644 --- a/app/soapbox/features/compose/containers/compose_form_container.js +++ b/app/soapbox/features/compose/containers/compose_form_container.js @@ -26,6 +26,7 @@ const mapStateToProps = state => { privacy: state.getIn(['compose', 'privacy']), focusDate: state.getIn(['compose', 'focusDate']), caretPosition: state.getIn(['compose', 'caretPosition']), + hasPoll: !!state.getIn(['compose', 'poll']), isSubmitting: state.getIn(['compose', 'is_submitting']), isEditing: state.getIn(['compose', 'id']) !== null, isChangingUpload: state.getIn(['compose', 'is_changing_upload']), diff --git a/app/soapbox/features/settings/index.tsx b/app/soapbox/features/settings/index.tsx index 1c82912f6..d342fd8f8 100644 --- a/app/soapbox/features/settings/index.tsx +++ b/app/soapbox/features/settings/index.tsx @@ -73,7 +73,7 @@ const Settings = () => { - {features.security || features.sessions && ( + {(features.security || features.sessions) && ( <> @@ -108,7 +108,7 @@ const Settings = () => { - {features.security || features.accountAliases && ( + {(features.security || features.accountAliases) && ( <> diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 74957a487..9f3894ac5 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -271,12 +271,12 @@ "compose_form.markdown.unmarked": "Post markdown disabled", "compose_form.message": "Message", "compose_form.placeholder": "What's on your mind?", - "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.add_option": "Add an answer", "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", - "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices", - "compose_form.poll.switch_to_single": "Change poll to allow for a single choice", + "compose_form.poll.option_placeholder": "Answer #{number}", + "compose_form.poll.remove_option": "Remove this answer", + "compose_form.poll.switch_to_multiple": "Change poll to allow multiple answers", + "compose_form.poll.switch_to_single": "Change poll to allow for a single answer", "compose_form.publish": "Post", "compose_form.publish_loud": "{publish}!", "compose_form.schedule": "Schedule", diff --git a/app/soapbox/middleware/errors.ts b/app/soapbox/middleware/errors.ts index b87c50249..d24fd9d19 100644 --- a/app/soapbox/middleware/errors.ts +++ b/app/soapbox/middleware/errors.ts @@ -12,9 +12,12 @@ const isRememberFailType = (type: string): boolean => type.endsWith('_REMEMBER_F /** Whether the error contains an Axios response. */ const hasResponse = (error: any): boolean => Boolean(error && error.response); +/** Don't show 401's. */ +const authorized = (error: any): boolean => error?.response?.status !== 401; + /** Whether the error should be shown to the user. */ const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => { - return !skipAlert && hasResponse(error) && isFailType(type) && !isRememberFailType(type); + return !skipAlert && hasResponse(error) && authorized(error) && isFailType(type) && !isRememberFailType(type); }; /** Middleware to display Redux errors to the user. */ diff --git a/app/styles/polls.scss b/app/styles/polls.scss index 231dbf195..2d0dd4142 100644 --- a/app/styles/polls.scss +++ b/app/styles/polls.scss @@ -118,49 +118,6 @@ } } -.compose-form__poll-wrapper { - border-top: 1px solid var(--foreground-color); - - ul { - padding: 10px; - } - - .button.button-secondary { - @apply h-auto py-1.5 px-2.5 text-primary-600 dark:text-primary-400 border-primary-600; - } - - li { - display: flex; - align-items: center; - - .poll__text { - flex: 0 0 auto; - width: calc(100% - (23px + 6px)); - margin-right: 6px; - } - } - - select { - @apply border border-solid border-primary-600 bg-white dark:bg-slate-800; - box-sizing: border-box; - font-size: 14px; - display: inline-block; - width: auto; - outline: 0; - font-family: inherit; - background-repeat: no-repeat; - background-position: right 8px center; - background-size: auto 16px; - border-radius: 4px; - padding: 6px 10px; - padding-right: 30px; - } - - .icon-button.disabled { - color: var(--brand-color); - } -} - .muted .poll { color: var(--primary-text-color);