= ({ status, expanded = false, onE
output.push(
);
}
- if (status.poll && typeof status.poll === 'string') {
+ const hasPoll = status.poll && typeof status.poll === 'string';
+
+ if (hasPoll) {
output.push(
);
}
- return <>{output}>;
+ return
{output}
;
} else {
const output = [
', () => {
+ 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) => (
+
+
+
+ {text && (
+
+ {text}
+
+ )}
+
+);
+
+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/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 {
{
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 && (
+
+
+
+ )}
+
);
};
@@ -162,21 +140,22 @@ const PollForm = (props: IPollForm) => {
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 handleAddOption = () => {
+ // autofocus on new input
+ // use streamfield
+ onAddOption('');
+ };
+ 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 */}
+
+
+ {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/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);