Merge remote-tracking branch 'soapbox/develop' into ts
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
7578ca2ee0
19 changed files with 356 additions and 185 deletions
|
@ -148,34 +148,24 @@ describe('fetchAccountByUsername()', () => {
|
||||||
const username = 'tiger';
|
const username = 'tiger';
|
||||||
let state, account;
|
let state, account;
|
||||||
|
|
||||||
describe('when the account has already been cached in redux', () => {
|
beforeEach(() => {
|
||||||
beforeEach(() => {
|
account = normalizeAccount({
|
||||||
account = normalizeAccount({
|
id,
|
||||||
id,
|
acct: username,
|
||||||
acct: username,
|
display_name: 'Tiger',
|
||||||
display_name: 'Tiger',
|
avatar: 'test.jpg',
|
||||||
avatar: 'test.jpg',
|
birthday: undefined,
|
||||||
birthday: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
state = rootReducer(undefined, {})
|
|
||||||
.set('accounts', ImmutableMap({
|
|
||||||
[id]: account,
|
|
||||||
}));
|
|
||||||
|
|
||||||
store = mockStore(state);
|
|
||||||
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet(`/api/v1/accounts/${id}`).reply(200, account);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null', async() => {
|
state = rootReducer(undefined, {})
|
||||||
const result = await store.dispatch(fetchAccountByUsername(username));
|
.set('accounts', ImmutableMap({
|
||||||
const actions = store.getActions();
|
[id]: account,
|
||||||
|
}));
|
||||||
|
|
||||||
expect(actions).toEqual([]);
|
store = mockStore(state);
|
||||||
expect(result).toBeNull();
|
|
||||||
|
__stub((mock) => {
|
||||||
|
mock.onGet(`/api/v1/accounts/${id}`).reply(200, account);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
import { mockStore } from 'soapbox/jest/test-helpers';
|
import { mockStore } from 'soapbox/jest/test-helpers';
|
||||||
import rootReducer from 'soapbox/reducers';
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
import { dismissAlert, showAlert, showAlertForError } from '../alerts';
|
import { dismissAlert, showAlert, showAlertForError } from '../alerts';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
const buildError = (message: string, status: number) => new AxiosError<any>(message, String(status), null, null, {
|
const buildError = (message: string, status: number) => new AxiosError<any>(message, String(status), null, null, {
|
||||||
data: {
|
data: {
|
||||||
error: message,
|
error: message,
|
||||||
|
|
|
@ -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_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
|
||||||
export const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
|
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) {
|
export function createAccount(params) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
|
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) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const { instance, me } = 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 features = getFeatures(instance);
|
const features = getFeatures(instance);
|
||||||
const me = state.get('me');
|
|
||||||
|
|
||||||
if (features.accountByUsername && (me || !features.accountLookup)) {
|
if (features.accountByUsername && (me || !features.accountLookup)) {
|
||||||
return api(getState).get(`/api/v1/accounts/${username}`).then(response => {
|
return api(getState).get(`/api/v1/accounts/${username}`).then(response => {
|
||||||
|
@ -182,6 +180,7 @@ export function fetchAccountByUsername(username) {
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchAccountFail(null, error));
|
dispatch(fetchAccountFail(null, error));
|
||||||
dispatch(importErrorWhileFetchingAccountByUsername(username));
|
dispatch(importErrorWhileFetchingAccountByUsername(username));
|
||||||
|
maybeRedirectLogin(error, history);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return dispatch(accountSearch({
|
return dispatch(accountSearch({
|
||||||
|
|
|
@ -20,7 +20,7 @@ export type AutoSuggestion = string | Emoji;
|
||||||
const textAtCursorMatchesToken = (str: string, caretPosition: number, searchTokens: string[]): CursorMatch => {
|
const textAtCursorMatchesToken = (str: string, caretPosition: number, searchTokens: string[]): CursorMatch => {
|
||||||
let word: string;
|
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/);
|
const right: number = str.slice(caretPosition).search(/\s/);
|
||||||
|
|
||||||
if (right < 0) {
|
if (right < 0) {
|
||||||
|
@ -201,13 +201,13 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
||||||
|
|
||||||
if (typeof suggestion === 'object') {
|
if (typeof suggestion === 'object') {
|
||||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||||
key = suggestion.id;
|
key = suggestion.id;
|
||||||
} else if (suggestion[0] === '#') {
|
} else if (suggestion[0] === '#') {
|
||||||
inner = suggestion;
|
inner = suggestion;
|
||||||
key = suggestion;
|
key = suggestion;
|
||||||
} else {
|
} else {
|
||||||
inner = <AutosuggestAccount id={suggestion} />;
|
inner = <AutosuggestAccount id={suggestion} />;
|
||||||
key = suggestion;
|
key = suggestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -279,13 +279,13 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative w-full'>
|
||||||
<label className='sr-only'>{placeholder}</label>
|
<label className='sr-only'>{placeholder}</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'block w-full sm:text-sm dark:bg-slate-800 dark:text-white dark:placeholder:text-gray-500 focus:ring-indigo-500 focus:border-indigo-500': true,
|
'block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-slate-800 dark:text-white dark:placeholder:text-gray-500 focus:ring-primary-500 focus:border-primary-500': true,
|
||||||
}, className)}
|
}, className)}
|
||||||
ref={this.setInput}
|
ref={this.setInput}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
19
app/soapbox/components/ui/divider/__tests__/divider.test.tsx
Normal file
19
app/soapbox/components/ui/divider/__tests__/divider.test.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen } from '../../../../jest/test-helpers';
|
||||||
|
import Divider from '../divider';
|
||||||
|
|
||||||
|
describe('<Divider />', () => {
|
||||||
|
it('renders without text', () => {
|
||||||
|
render(<Divider />);
|
||||||
|
|
||||||
|
expect(screen.queryAllByTestId('divider-text')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders text', () => {
|
||||||
|
const text = 'Hello';
|
||||||
|
render(<Divider text={text} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('divider-text')).toHaveTextContent(text);
|
||||||
|
});
|
||||||
|
});
|
22
app/soapbox/components/ui/divider/divider.tsx
Normal file
22
app/soapbox/components/ui/divider/divider.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface IDivider {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Divider */
|
||||||
|
const Divider = ({ text }: IDivider) => (
|
||||||
|
<div className='relative' data-testid='divider'>
|
||||||
|
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
|
||||||
|
<div className='w-full border-t-2 border-gray-100 dark:border-slate-700 border-solid' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{text && (
|
||||||
|
<div className='relative flex justify-center'>
|
||||||
|
<span className='px-2 bg-white text-gray-400' data-testid='divider-text'>{text}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Divider;
|
|
@ -5,6 +5,7 @@ export { default as Checkbox } from './checkbox/checkbox';
|
||||||
export { default as Column } from './column/column';
|
export { default as Column } from './column/column';
|
||||||
export { default as Counter } from './counter/counter';
|
export { default as Counter } from './counter/counter';
|
||||||
export { default as Datepicker } from './datepicker/datepicker';
|
export { default as Datepicker } from './datepicker/datepicker';
|
||||||
|
export { default as Divider } from './divider/divider';
|
||||||
export { default as Emoji } from './emoji/emoji';
|
export { default as Emoji } from './emoji/emoji';
|
||||||
export { default as EmojiSelector } from './emoji-selector/emoji-selector';
|
export { default as EmojiSelector } from './emoji-selector/emoji-selector';
|
||||||
export { default as FileInput } from './file-input/file-input';
|
export { default as FileInput } from './file-input/file-input';
|
||||||
|
|
|
@ -64,7 +64,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
||||||
type={revealed ? 'text' : type}
|
type={revealed ? 'text' : type}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames({
|
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,
|
true,
|
||||||
'pr-7': isPassword,
|
'pr-7': isPassword,
|
||||||
'text-red-600 border-red-600': hasError,
|
'text-red-600 border-red-600': hasError,
|
||||||
|
|
|
@ -5,8 +5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
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 { fetchPatronAccount } from 'soapbox/actions/patron';
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
|
@ -67,6 +68,7 @@ const makeMapStateToProps = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default @connect(makeMapStateToProps)
|
export default @connect(makeMapStateToProps)
|
||||||
|
@withRouter
|
||||||
class AccountTimeline extends ImmutablePureComponent {
|
class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -82,11 +84,11 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
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) {
|
if (accountId && accountId !== -1) {
|
||||||
this.props.dispatch(fetchAccount(accountId));
|
|
||||||
|
|
||||||
if (!withReplies) {
|
if (!withReplies) {
|
||||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
}
|
}
|
||||||
|
@ -96,17 +98,17 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||||
} else {
|
|
||||||
this.props.dispatch(fetchAccountByUsername(username));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
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) {
|
if (accountId && (accountId !== -1) && (accountId !== prevProps.accountId) || withReplies !== prevProps.withReplies) {
|
||||||
this.props.dispatch(fetchAccount(accountId));
|
|
||||||
|
|
||||||
if (!withReplies) {
|
if (!withReplies) {
|
||||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
}
|
}
|
||||||
|
@ -116,8 +118,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||||
} else if (username && (username !== prevProps.params.username)) {
|
|
||||||
this.props.dispatch(fetchAccountByUsername(username));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u20
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
|
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' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||||
|
@ -62,6 +63,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
spoilerText: PropTypes.string,
|
spoilerText: PropTypes.string,
|
||||||
focusDate: PropTypes.instanceOf(Date),
|
focusDate: PropTypes.instanceOf(Date),
|
||||||
caretPosition: PropTypes.number,
|
caretPosition: PropTypes.number,
|
||||||
|
hasPoll: PropTypes.bool,
|
||||||
isSubmitting: PropTypes.bool,
|
isSubmitting: PropTypes.bool,
|
||||||
isChangingUpload: PropTypes.bool,
|
isChangingUpload: PropTypes.bool,
|
||||||
isEditing: PropTypes.bool,
|
isEditing: PropTypes.bool,
|
||||||
|
@ -340,7 +342,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
<AutosuggestTextarea
|
<AutosuggestTextarea
|
||||||
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
placeholder={intl.formatMessage(this.props.hasPoll ? messages.pollPlaceholder : messages.placeholder)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={this.props.text}
|
value={this.props.text}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
|
|
|
@ -301,7 +301,9 @@ class EmojiPickerDropdown extends React.PureComponent {
|
||||||
this.dropdown = c;
|
this.dropdown = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowDropdown = ({ target }) => {
|
onShowDropdown = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
this.setState({ active: true });
|
this.setState({ active: true });
|
||||||
|
|
||||||
if (!EmojiPicker) {
|
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' });
|
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,33 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import AutosuggestInput from 'soapbox/components/autosuggest_input';
|
import AutosuggestInput from 'soapbox/components/autosuggest_input';
|
||||||
import Icon from 'soapbox/components/icon';
|
import { Button, Divider, HStack, Stack, Text, Toggle } from 'soapbox/components/ui';
|
||||||
import IconButton from 'soapbox/components/icon_button';
|
|
||||||
import { HStack } from 'soapbox/components/ui';
|
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import DurationSelector from './polls/duration-selector';
|
||||||
|
|
||||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest_input';
|
import type { AutoSuggestion } from 'soapbox/components/autosuggest_input';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Answer #{number}' },
|
||||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add an answer' },
|
||||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
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' },
|
||||||
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
removePoll: { id: 'compose_form.poll.remove', defaultMessage: 'Remove poll' },
|
||||||
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}}' },
|
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
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 {
|
interface IOption {
|
||||||
index: number
|
index: number
|
||||||
isPollMultiple?: boolean
|
|
||||||
maxChars: number
|
maxChars: number
|
||||||
numOptions: number
|
numOptions: number
|
||||||
onChange(index: number, value: string): void
|
onChange(index: number, value: string): void
|
||||||
|
@ -35,7 +36,6 @@ interface IOption {
|
||||||
onRemove(index: number): void
|
onRemove(index: number): void
|
||||||
onRemovePoll(): void
|
onRemovePoll(): void
|
||||||
onSuggestionSelected(tokenStart: number, token: string, value: string, key: (string | number)[]): void
|
onSuggestionSelected(tokenStart: number, token: string, value: string, key: (string | number)[]): void
|
||||||
onToggleMultiple(): void
|
|
||||||
suggestions?: any // list
|
suggestions?: any // list
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,6 @@ interface IOption {
|
||||||
const Option = (props: IOption) => {
|
const Option = (props: IOption) => {
|
||||||
const {
|
const {
|
||||||
index,
|
index,
|
||||||
isPollMultiple,
|
|
||||||
maxChars,
|
maxChars,
|
||||||
numOptions,
|
numOptions,
|
||||||
onChange,
|
onChange,
|
||||||
|
@ -51,7 +50,6 @@ const Option = (props: IOption) => {
|
||||||
onFetchSuggestions,
|
onFetchSuggestions,
|
||||||
onRemove,
|
onRemove,
|
||||||
onRemovePoll,
|
onRemovePoll,
|
||||||
onToggleMultiple,
|
|
||||||
suggestions,
|
suggestions,
|
||||||
title,
|
title,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -68,21 +66,8 @@ const Option = (props: IOption) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleMultiple = (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
onToggleMultiple();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSuggestionsClearRequested = () => onClearSuggestions();
|
const onSuggestionsClearRequested = () => onClearSuggestions();
|
||||||
|
|
||||||
const handleCheckboxKeypress = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
|
||||||
handleToggleMultiple(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSuggestionsFetchRequested = (token: string) => onFetchSuggestions(token);
|
const onSuggestionsFetchRequested = (token: string) => onFetchSuggestions(token);
|
||||||
|
|
||||||
const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
|
const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
|
||||||
|
@ -92,19 +77,14 @@ const Option = (props: IOption) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<HStack alignItems='center' justifyContent='between' space={4}>
|
||||||
<label className='poll__text editable'>
|
<HStack alignItems='center' space={2} grow>
|
||||||
<span
|
<div className='w-6'>
|
||||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
<Text weight='bold'>{index + 1}.</Text>
|
||||||
onClick={handleToggleMultiple}
|
</div>
|
||||||
onKeyPress={handleCheckboxKeypress}
|
|
||||||
role='button'
|
|
||||||
tabIndex={0}
|
|
||||||
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
|
||||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AutosuggestInput
|
<AutosuggestInput
|
||||||
|
className='rounded-md'
|
||||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||||
maxLength={maxChars}
|
maxLength={maxChars}
|
||||||
value={title}
|
value={title}
|
||||||
|
@ -114,18 +94,16 @@ const Option = (props: IOption) => {
|
||||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||||
onSuggestionSelected={onSuggestionSelected}
|
onSuggestionSelected={onSuggestionSelected}
|
||||||
searchTokens={[':']}
|
searchTokens={[':']}
|
||||||
autoFocus
|
autoFocus={index === 0}
|
||||||
/>
|
/>
|
||||||
</label>
|
</HStack>
|
||||||
|
|
||||||
<div className='poll__cancel'>
|
{index > 1 && (
|
||||||
<IconButton
|
<div>
|
||||||
title={intl.formatMessage(messages.remove_option)}
|
<Button theme='danger' size='sm' onClick={handleOptionRemove}>Delete</Button>
|
||||||
src={require('@tabler/icons/icons/x.svg')}
|
</div>
|
||||||
onClick={handleOptionRemove}
|
)}
|
||||||
/>
|
</HStack>
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -163,10 +141,7 @@ const PollForm = (props: IPollForm) => {
|
||||||
const maxOptionChars = pollLimits.get('max_characters_per_option');
|
const maxOptionChars = pollLimits.get('max_characters_per_option');
|
||||||
|
|
||||||
const handleAddOption = () => onAddOption('');
|
const handleAddOption = () => onAddOption('');
|
||||||
|
const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple);
|
||||||
const handleSelectDuration = (event: React.ChangeEvent<HTMLSelectElement>) =>
|
|
||||||
onChangeSettings(event.target.value, isMultiple);
|
|
||||||
|
|
||||||
const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple);
|
const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple);
|
||||||
|
|
||||||
if (!options) {
|
if (!options) {
|
||||||
|
@ -174,8 +149,8 @@ const PollForm = (props: IPollForm) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='compose-form__poll-wrapper'>
|
<Stack space={4}>
|
||||||
<ul>
|
<Stack space={2}>
|
||||||
{options.map((title: string, i: number) => (
|
{options.map((title: string, i: number) => (
|
||||||
<Option
|
<Option
|
||||||
title={title}
|
title={title}
|
||||||
|
@ -183,34 +158,64 @@ const PollForm = (props: IPollForm) => {
|
||||||
index={i}
|
index={i}
|
||||||
onChange={onChangeOption}
|
onChange={onChangeOption}
|
||||||
onRemove={onRemoveOption}
|
onRemove={onRemoveOption}
|
||||||
isPollMultiple={isMultiple}
|
|
||||||
onToggleMultiple={handleToggleMultiple}
|
|
||||||
maxChars={maxOptionChars}
|
maxChars={maxOptionChars}
|
||||||
numOptions={options.size}
|
numOptions={options.size}
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
|
|
||||||
<HStack className='text-black dark:text-white' space={2}>
|
<HStack space={2}>
|
||||||
{options.size < maxOptions && (
|
<div className='w-6' />
|
||||||
<button className='button button-secondary' onClick={handleAddOption}>
|
|
||||||
<Icon src={require('@tabler/icons/icons/plus.svg')} />
|
|
||||||
<FormattedMessage {...messages.add_option} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<select value={expiresIn} onChange={handleSelectDuration}>
|
{options.size < maxOptions && (
|
||||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
<Button
|
||||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
theme='secondary'
|
||||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
onClick={handleAddOption}
|
||||||
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
size='sm'
|
||||||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
block
|
||||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
>
|
||||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
<FormattedMessage {...messages.add_option} />
|
||||||
</select>
|
</Button>
|
||||||
</HStack>
|
)}
|
||||||
</div>
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<button onClick={handleToggleMultiple} className='text-left'>
|
||||||
|
<HStack alignItems='center' justifyContent='between'>
|
||||||
|
<Stack>
|
||||||
|
<Text weight='medium'>
|
||||||
|
{intl.formatMessage(messages.multiSelect)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text theme='muted' size='sm'>
|
||||||
|
{intl.formatMessage(messages.multiSelectDetail)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Toggle checked={isMultiple} onChange={handleToggleMultiple} />
|
||||||
|
</HStack>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Duration */}
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text weight='medium'>
|
||||||
|
{intl.formatMessage(messages.pollDuration)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<DurationSelector onDurationChange={handleSelectDuration} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Remove Poll */}
|
||||||
|
<div className='text-center'>
|
||||||
|
<Button theme='danger' size='sm' onClick={props.onRemovePoll}>
|
||||||
|
{intl.formatMessage(messages.removePoll)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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('<DurationSelector />', () => {
|
||||||
|
it('defaults to 2 days', () => {
|
||||||
|
const handler = jest.fn();
|
||||||
|
render(<DurationSelector onDurationChange={handler} />);
|
||||||
|
|
||||||
|
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(<DurationSelector onDurationChange={handler} />);
|
||||||
|
|
||||||
|
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(<DurationSelector onDurationChange={handler} />);
|
||||||
|
|
||||||
|
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(<DurationSelector onDurationChange={handler} />);
|
||||||
|
|
||||||
|
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(<DurationSelector onDurationChange={handler} />);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<number>(2);
|
||||||
|
const [hours, setHours] = useState<number>(0);
|
||||||
|
const [minutes, setMinutes] = useState<number>(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 (
|
||||||
|
<div className='grid grid-cols-1 gap-y-2 gap-x-2 sm:grid-cols-3'>
|
||||||
|
<div className='sm:col-span-1'>
|
||||||
|
<Select
|
||||||
|
value={days}
|
||||||
|
onChange={(event) => setDays(Number(event.target.value))}
|
||||||
|
data-testid='duration-selector-days'
|
||||||
|
>
|
||||||
|
{[...Array(8).fill(undefined)].map((_, number) => (
|
||||||
|
<option value={number} key={number}>
|
||||||
|
{intl.formatMessage(messages.days, { number })}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sm:col-span-1'>
|
||||||
|
<Select
|
||||||
|
value={hours}
|
||||||
|
onChange={(event) => setHours(Number(event.target.value))}
|
||||||
|
disabled={days === 7}
|
||||||
|
data-testid='duration-selector-hours'
|
||||||
|
>
|
||||||
|
{[...Array(24).fill(undefined)].map((_, number) => (
|
||||||
|
<option value={number} key={number}>
|
||||||
|
{intl.formatMessage(messages.hours, { number })}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sm:col-span-1'>
|
||||||
|
<Select
|
||||||
|
value={minutes}
|
||||||
|
onChange={(event) => setMinutes(Number(event.target.value))}
|
||||||
|
disabled={days === 7}
|
||||||
|
data-testid='duration-selector-minutes'
|
||||||
|
>
|
||||||
|
{[0, 15, 30, 45].map((number) => (
|
||||||
|
<option value={number} key={number}>
|
||||||
|
{intl.formatMessage(messages.minutes, { number })}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DurationSelector;
|
|
@ -26,6 +26,7 @@ const mapStateToProps = state => {
|
||||||
privacy: state.getIn(['compose', 'privacy']),
|
privacy: state.getIn(['compose', 'privacy']),
|
||||||
focusDate: state.getIn(['compose', 'focusDate']),
|
focusDate: state.getIn(['compose', 'focusDate']),
|
||||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||||
|
hasPoll: !!state.getIn(['compose', 'poll']),
|
||||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||||
isEditing: state.getIn(['compose', 'id']) !== null,
|
isEditing: state.getIn(['compose', 'id']) !== null,
|
||||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||||
|
|
|
@ -73,7 +73,7 @@ const Settings = () => {
|
||||||
</List>
|
</List>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
{features.security || features.sessions && (
|
{(features.security || features.sessions) && (
|
||||||
<>
|
<>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle title={intl.formatMessage(messages.security)} />
|
<CardTitle title={intl.formatMessage(messages.security)} />
|
||||||
|
@ -108,7 +108,7 @@ const Settings = () => {
|
||||||
<Preferences />
|
<Preferences />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
{features.security || features.accountAliases && (
|
{(features.security || features.accountAliases) && (
|
||||||
<>
|
<>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle title={intl.formatMessage(messages.other)} />
|
<CardTitle title={intl.formatMessage(messages.other)} />
|
||||||
|
|
|
@ -271,12 +271,12 @@
|
||||||
"compose_form.markdown.unmarked": "Post markdown disabled",
|
"compose_form.markdown.unmarked": "Post markdown disabled",
|
||||||
"compose_form.message": "Message",
|
"compose_form.message": "Message",
|
||||||
"compose_form.placeholder": "What's on your mind?",
|
"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.duration": "Poll duration",
|
||||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
"compose_form.poll.option_placeholder": "Answer #{number}",
|
||||||
"compose_form.poll.remove_option": "Remove this choice",
|
"compose_form.poll.remove_option": "Remove this answer",
|
||||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
"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 choice",
|
"compose_form.poll.switch_to_single": "Change poll to allow for a single answer",
|
||||||
"compose_form.publish": "Post",
|
"compose_form.publish": "Post",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.schedule": "Schedule",
|
"compose_form.schedule": "Schedule",
|
||||||
|
|
|
@ -12,9 +12,12 @@ const isRememberFailType = (type: string): boolean => type.endsWith('_REMEMBER_F
|
||||||
/** Whether the error contains an Axios response. */
|
/** Whether the error contains an Axios response. */
|
||||||
const hasResponse = (error: any): boolean => Boolean(error && error.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. */
|
/** Whether the error should be shown to the user. */
|
||||||
const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => {
|
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. */
|
/** Middleware to display Redux errors to the user. */
|
||||||
|
|
|
@ -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 {
|
.muted .poll {
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue