From 1aef2eaf221340ded583cb51949f434db409b0cc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 Jun 2022 16:00:12 -0500 Subject: [PATCH 01/18] Redirect to /login if viewing an account 401's --- app/soapbox/actions/accounts.js | 21 +++++++++--------- .../features/account_timeline/index.js | 22 +++++++++---------- app/soapbox/middleware/errors.ts | 5 ++++- 3 files changed, 25 insertions(+), 23 deletions(-) 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/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/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. */ From 53cb5f723b5eae690b6e259c678406df9594f6cb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 8 Jun 2022 16:18:51 -0500 Subject: [PATCH 02/18] actions/accounts: remove n/a test --- .../actions/__tests__/accounts.test.ts | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) 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); }); }); From 2cd11adf794ca5d1dd7ec612c98f56e25e19422e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 9 Jun 2022 00:13:21 +0200 Subject: [PATCH 03/18] Fix delete/migrate settings visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/settings/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/settings/index.tsx b/app/soapbox/features/settings/index.tsx index 1c82912f6..6c25d8a92 100644 --- a/app/soapbox/features/settings/index.tsx +++ b/app/soapbox/features/settings/index.tsx @@ -108,7 +108,7 @@ const Settings = () => { - {features.security || features.accountAliases && ( + {(features.security || features.accountAliases) && ( <> From 354159e1faf4dfd8abe6f4e31c30d895b1f43a17 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 6 Jun 2022 14:53:09 -0400 Subject: [PATCH 04/18] Toggle placeholder text --- app/soapbox/features/compose/components/compose_form.js | 4 +++- .../features/compose/containers/compose_form_container.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) 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 { { 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']), From 7782c96ba45cff0320ae197455c1759741a9a5d9 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 7 Jun 2022 11:11:28 -0400 Subject: [PATCH 05/18] Improve design of the Poll Form --- app/soapbox/components/autosuggest_input.tsx | 12 +- app/soapbox/components/ui/input/input.tsx | 2 +- .../features/compose/components/poll-form.tsx | 136 ++++++++---------- .../__tests__/duration-selector.test.tsx | 77 ++++++++++ .../components/polls/duration-selector.tsx | 93 ++++++++++++ app/soapbox/locales/en.json | 10 +- app/styles/polls.scss | 43 ------ 7 files changed, 244 insertions(+), 129 deletions(-) create mode 100644 app/soapbox/features/compose/components/polls/__tests__/duration-selector.test.tsx create mode 100644 app/soapbox/features/compose/components/polls/duration-selector.tsx diff --git a/app/soapbox/components/autosuggest_input.tsx b/app/soapbox/components/autosuggest_input.tsx index cac979337..383d740c6 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 +
( 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/compose/components/poll-form.tsx b/app/soapbox/features/compose/components/poll-form.tsx index 0f8e87387..4939b9e3f 100644 --- a/app/soapbox/features/compose/components/poll-form.tsx +++ b/app/soapbox/features/compose/components/poll-form.tsx @@ -1,24 +1,23 @@ '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, HStack, Stack, Text } 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' }, + 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' }, 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' }, + 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}}' }, @@ -26,7 +25,6 @@ const messages = defineMessages({ interface IOption { index: number - isPollMultiple?: boolean maxChars: number numOptions: number onChange(index: number, value: string): void @@ -35,7 +33,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 +40,6 @@ interface IOption { const Option = (props: IOption) => { const { index, - isPollMultiple, maxChars, numOptions, onChange, @@ -51,7 +47,6 @@ const Option = (props: IOption) => { onFetchSuggestions, onRemove, onRemovePoll, - onToggleMultiple, suggestions, title, } = props; @@ -68,20 +63,20 @@ const Option = (props: IOption) => { } }; - const handleToggleMultiple = (event: React.MouseEvent | React.KeyboardEvent) => { - event.preventDefault(); - event.stopPropagation(); + // const handleToggleMultiple = (event: React.MouseEvent | React.KeyboardEvent) => { + // event.preventDefault(); + // event.stopPropagation(); - onToggleMultiple(); - }; + // onToggleMultiple(); + // }; const onSuggestionsClearRequested = () => onClearSuggestions(); - const handleCheckboxKeypress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - handleToggleMultiple(event); - } - }; + // const handleCheckboxKeypress = (event: React.KeyboardEvent) => { + // if (event.key === 'Enter' || event.key === ' ') { + // handleToggleMultiple(event); + // } + // }; const onSuggestionsFetchRequested = (token: string) => onFetchSuggestions(token); @@ -92,17 +87,11 @@ const Option = (props: IOption) => { }; return ( -
  • - + -
    - -
    -
  • + {index > 1 && ( +
    + +
    + )} + ); }; @@ -156,27 +143,21 @@ const PollForm = (props: IPollForm) => { ...filteredProps } = props; - const intl = useIntl(); - const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any); const maxOptions = pollLimits.get('max_options'); const maxOptionChars = pollLimits.get('max_characters_per_option'); const handleAddOption = () => onAddOption(''); - - const handleSelectDuration = (event: React.ChangeEvent) => - onChangeSettings(event.target.value, isMultiple); - - const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); - + const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple); + // const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); if (!options) { return null; } return ( -
    -
      + + {options.map((title: string, i: number) => (
    - - {options.size < maxOptions && ( - - )} + +
    - - -
    + {options.size < maxOptions && ( + + )} +
    + + + + {/* Duration */} + + Duration + + + + + {/* 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/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/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); From 3dc60f2cd8d4ddc9351b7c557af93df06b62cab9 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 7 Jun 2022 11:24:40 -0400 Subject: [PATCH 06/18] Add Divider --- .../ui/divider/__tests__/divider.test.tsx | 19 ++++++++++++++++ app/soapbox/components/ui/divider/divider.tsx | 22 +++++++++++++++++++ app/soapbox/components/ui/index.ts | 1 + .../features/compose/components/poll-form.tsx | 3 ++- 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/components/ui/divider/__tests__/divider.test.tsx create mode 100644 app/soapbox/components/ui/divider/divider.tsx diff --git a/app/soapbox/components/ui/divider/__tests__/divider.test.tsx b/app/soapbox/components/ui/divider/__tests__/divider.test.tsx new file mode 100644 index 000000000..c0d244187 --- /dev/null +++ b/app/soapbox/components/ui/divider/__tests__/divider.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { render, screen } from '../../../../jest/test-helpers'; +import Divider from '../divider'; + +describe('', () => { + 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..40ba6f310 --- /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/features/compose/components/poll-form.tsx b/app/soapbox/features/compose/components/poll-form.tsx index 4939b9e3f..06c6feaab 100644 --- a/app/soapbox/features/compose/components/poll-form.tsx +++ b/app/soapbox/features/compose/components/poll-form.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import AutosuggestInput from 'soapbox/components/autosuggest_input'; -import { Button, HStack, Stack, Text } from 'soapbox/components/ui'; +import { Button, Divider, HStack, Stack, Text } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import DurationSelector from './polls/duration-selector'; @@ -187,6 +187,7 @@ const PollForm = (props: IPollForm) => { + {/* Duration */} From e60db6decb327da45d162bb369efa4f0ba2a1226 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 7 Jun 2022 11:25:24 -0400 Subject: [PATCH 07/18] Add period after index --- app/soapbox/features/compose/components/poll-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/compose/components/poll-form.tsx b/app/soapbox/features/compose/components/poll-form.tsx index 06c6feaab..134bc7651 100644 --- a/app/soapbox/features/compose/components/poll-form.tsx +++ b/app/soapbox/features/compose/components/poll-form.tsx @@ -90,7 +90,7 @@ const Option = (props: IOption) => {
    - {index + 1} + {index + 1}.
    Date: Tue, 7 Jun 2022 11:38:54 -0400 Subject: [PATCH 08/18] Add multi-select toggle --- .../features/compose/components/poll-form.tsx | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/app/soapbox/features/compose/components/poll-form.tsx b/app/soapbox/features/compose/components/poll-form.tsx index 134bc7651..c765fd5df 100644 --- a/app/soapbox/features/compose/components/poll-form.tsx +++ b/app/soapbox/features/compose/components/poll-form.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import AutosuggestInput from 'soapbox/components/autosuggest_input'; -import { Button, Divider, HStack, Stack, Text } 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'; @@ -15,12 +15,15 @@ const messages = defineMessages({ 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' }, - poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, + 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 { @@ -63,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) => { @@ -143,13 +133,15 @@ const PollForm = (props: IPollForm) => { ...filteredProps } = props; + const intl = useIntl(); + const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any); const maxOptions = pollLimits.get('max_options'); const maxOptionChars = pollLimits.get('max_characters_per_option'); const handleAddOption = () => onAddOption(''); const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple); - // const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); + const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); if (!options) { return null; @@ -189,16 +181,36 @@ const PollForm = (props: IPollForm) => { + + + + {intl.formatMessage(messages.multiSelect)} + + + + {intl.formatMessage(messages.multiSelectDetail)} + + + + + + + + {/* Duration */} - Duration + + {intl.formatMessage(messages.pollDuration)} + {/* Remove Poll */}
    - +
    ); From 6b07a7c3b6126627e7e4b36aca6ff43874616f2e Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 7 Jun 2022 12:40:37 -0400 Subject: [PATCH 09/18] Dark mode support for Polls --- app/soapbox/components/ui/divider/divider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/components/ui/divider/divider.tsx b/app/soapbox/components/ui/divider/divider.tsx index 40ba6f310..0670be123 100644 --- a/app/soapbox/components/ui/divider/divider.tsx +++ b/app/soapbox/components/ui/divider/divider.tsx @@ -8,7 +8,7 @@ interface IDivider { const Divider = ({ text }: IDivider) => (