Reducers: TypeScript

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-develop-3zknud/deployments/332^2
marcin mikołajczak 2022-06-20 19:59:51 +02:00
rodzic af695e3812
commit 419ab93077
24 zmienionych plików z 349 dodań i 320 usunięć

Wyświetl plik

@ -1,5 +1,4 @@
import axios, { AxiosError, Canceler } from 'axios';
import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import throttle from 'lodash/throttle';
import { defineMessages, IntlShape } from 'react-intl';
@ -100,7 +99,7 @@ const messages = defineMessages({
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
const ensureComposeIsVisible = (getState: () => RootState, routerHistory: History) => {
if (!getState().compose.get('mounted') && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
if (!getState().compose.mounted && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
routerHistory.push('/posts/new');
}
};
@ -212,16 +211,16 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, d
};
const needsDescriptions = (state: RootState) => {
const media = state.compose.get('media_attachments') as ImmutableList<ImmutableMap<string, any>>;
const media = state.compose.media_attachments;
const missingDescriptionModal = getSettings(state).get('missingDescriptionModal');
const hasMissing = media.filter(item => !item.get('description')).size > 0;
const hasMissing = media.filter(item => !item.description).size > 0;
return missingDescriptionModal && hasMissing;
};
const validateSchedule = (state: RootState) => {
const schedule = state.compose.get('schedule');
const schedule = state.compose.schedule;
if (!schedule) return true;
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000);
@ -234,10 +233,10 @@ const submitCompose = (routerHistory: History, force = false) =>
if (!isLoggedIn(getState)) return;
const state = getState();
const status = state.compose.get('text') || '';
const media = state.compose.get('media_attachments') as ImmutableList<ImmutableMap<string, any>>;
const statusId = state.compose.get('id') || null;
let to = state.compose.get('to') || ImmutableOrderedSet();
const status = state.compose.text;
const media = state.compose.media_attachments;
const statusId = state.compose.id;
let to = state.compose.to;
if (!validateSchedule(state)) {
dispatch(snackbar.error(messages.scheduleError));
@ -259,7 +258,7 @@ const submitCompose = (routerHistory: History, force = false) =>
}
if (to && status) {
const mentions: string[] = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/gi); // not a perfect regex
const mentions: string[] | null = status.match(/(?:^|\s|\.)@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/gi); // not a perfect regex
if (mentions)
to = to.union(mentions.map(mention => mention.trim().slice(1)));
@ -268,19 +267,19 @@ const submitCompose = (routerHistory: History, force = false) =>
dispatch(submitComposeRequest());
dispatch(closeModal());
const idempotencyKey = state.compose.get('idempotencyKey');
const idempotencyKey = state.compose.idempotencyKey;
const params = {
status,
in_reply_to_id: state.compose.get('in_reply_to') || null,
quote_id: state.compose.get('quote') || null,
media_ids: media.map(item => item.get('id')),
sensitive: state.compose.get('sensitive'),
spoiler_text: state.compose.get('spoiler_text') || '',
visibility: state.compose.get('privacy'),
content_type: state.compose.get('content_type'),
poll: state.compose.get('poll') || null,
scheduled_at: state.compose.get('schedule') || null,
in_reply_to_id: state.compose.in_reply_to,
quote_id: state.compose.quote,
media_ids: media.map(item => item.id),
sensitive: state.compose.sensitive,
spoiler_text: state.compose.spoiler_text,
visibility: state.compose.privacy,
content_type: state.compose.content_type,
poll: state.compose.poll,
scheduled_at: state.compose.schedule,
to,
};
@ -315,7 +314,7 @@ const uploadCompose = (files: FileList, intl: IntlShape) =>
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined;
const media = getState().compose.get('media_attachments');
const media = getState().compose.media_attachments;
const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
@ -550,7 +549,7 @@ const updateTagHistory = (tags: string[]) => ({
const insertIntoTagHistory = (recognizedTags: APIEntity[], text: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const oldHistory = state.compose.get('tagHistory') as ImmutableList<string>;
const oldHistory = state.compose.tagHistory;
const me = state.me;
const names = recognizedTags
.filter(tag => text.match(new RegExp(`#${tag.name}`, 'i')))

Wyświetl plik

@ -25,7 +25,7 @@ const getMeId = (state: RootState) => state.me || getAuthUserId(state);
const getMeUrl = (state: RootState) => {
const accountId = getMeId(state);
return state.accounts.get(accountId)!.url || getAuthUserUrl(state);
return state.accounts.get(accountId)?.url || getAuthUserUrl(state);
};
const getMeToken = (state: RootState) => {

Wyświetl plik

@ -47,7 +47,7 @@ const statusExists = (getState: () => RootState, statusId: string) => {
return (getState().statuses.get(statusId) || null) !== null;
};
const createStatus = (params: Record<string, any>, idempotencyKey: string, statusId: string) => {
const createStatus = (params: Record<string, any>, idempotencyKey: string, statusId: string | null) => {
return (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey });

Wyświetl plik

@ -16,18 +16,18 @@ const messages = defineMessages({
const checkComposeContent = compose => {
return [
compose.get('text').length > 0,
compose.get('spoiler_text').length > 0,
compose.get('media_attachments').size > 0,
compose.get('in_reply_to') !== null,
compose.get('quote') !== null,
compose.get('poll') !== null,
compose.text.length > 0,
compose.spoiler_text.length > 0,
compose.media_attachments.size > 0,
compose.in_reply_to !== null,
compose.quote !== null,
compose.poll !== null,
].some(check => check === true);
};
const mapStateToProps = state => ({
hasComposeContent: checkComposeContent(state.get('compose')),
isEditing: state.compose.get('id') !== null,
hasComposeContent: checkComposeContent(state.compose),
isEditing: state.compose.id !== null,
});
const mapDispatchToProps = (dispatch) => ({

Wyświetl plik

@ -22,7 +22,7 @@ const Backups = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const backups = useAppSelector<ImmutableList<ImmutableMap<string, any>>>((state) => state.backups.toList().sortBy((backup: ImmutableMap<string, any>) => backup.get('inserted_at')));
const backups = useAppSelector((state) => state.backups.toList().sortBy((backup) => backup.inserted_at));
const [isLoading, setIsLoading] = useState(true);
@ -63,12 +63,12 @@ const Backups = () => {
>
{backups.map((backup) => (
<div
className={classNames('backup', { 'backup--pending': !backup.get('processed') })}
key={backup.get('id')}
className={classNames('backup', { 'backup--pending': !backup.processed })}
key={backup.id}
>
{backup.get('processed')
? <a href={backup.get('url')} target='_blank'>{backup.get('inserted_at')}</a>
: <div>{intl.formatMessage(messages.pending)}: {backup.get('inserted_at')}</div>
{backup.processed
? <a href={backup.url} target='_blank'>{backup.inserted_at}</a>
: <div>{intl.formatMessage(messages.pending)}: {backup.inserted_at}</div>
}
</div>
))}

Wyświetl plik

@ -49,7 +49,7 @@ const Option = (props: IOption) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const suggestions = useAppSelector((state) => state.compose.get('suggestions'));
const suggestions = useAppSelector((state) => state.compose.suggestions);
const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value);
@ -107,9 +107,9 @@ const PollForm = () => {
const intl = useIntl();
const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any);
const options = useAppSelector((state) => state.compose.getIn(['poll', 'options']));
const expiresIn = useAppSelector((state) => state.compose.getIn(['poll', 'expires_in']));
const isMultiple = useAppSelector((state) => state.compose.getIn(['poll', 'multiple']));
const options = useAppSelector((state) => state.compose.poll?.options);
const expiresIn = useAppSelector((state) => state.compose.poll?.expires_in);
const isMultiple = useAppSelector((state) => state.compose.poll?.multiple);
const maxOptions = pollLimits.get('max_options');
const maxOptionChars = pollLimits.get('max_characters_per_option');

Wyświetl plik

@ -13,9 +13,9 @@ import type { Status as StatusEntity } from 'soapbox/types/entities';
const ReplyMentions: React.FC = () => {
const dispatch = useDispatch();
const instance = useAppSelector((state) => state.instance);
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') }));
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.in_reply_to! }));
const to = useAppSelector((state) => state.compose.get('to'));
const to = useAppSelector((state) => state.compose.to);
const account = useAppSelector((state) => state.accounts.get(state.me));
const { explicitAddressing } = getFeatures(instance);
@ -24,7 +24,7 @@ const ReplyMentions: React.FC = () => {
return null;
}
const parentTo = status && statusToMentionsAccountIdsArray(status, account);
const parentTo = status && statusToMentionsAccountIdsArray(status, account!);
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();

Wyświetl plik

@ -31,7 +31,7 @@ const ScheduleForm: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const scheduledAt = useAppSelector((state) => state.compose.get('schedule'));
const scheduledAt = useAppSelector((state) => state.compose.schedule);
const active = !!scheduledAt;
const onSchedule = (date: Date) => {

Wyświetl plik

@ -15,8 +15,8 @@ const SensitiveButton: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const active = useAppSelector(state => state.compose.get('sensitive') === true);
const disabled = useAppSelector(state => state.compose.get('spoiler') === true);
const active = useAppSelector(state => state.compose.sensitive === true);
const disabled = useAppSelector(state => state.compose.spoiler === true);
const onClick = () => {
dispatch(changeComposeSensitivity());

Wyświetl plik

@ -5,8 +5,8 @@ import { useAppSelector } from 'soapbox/hooks';
/** File upload progress bar for post composer. */
const ComposeUploadProgress = () => {
const active = useAppSelector((state) => state.compose.get('is_uploading'));
const progress = useAppSelector((state) => state.compose.get('progress'));
const active = useAppSelector((state) => state.compose.is_uploading);
const progress = useAppSelector((state) => state.compose.progress);
if (!active) {
return null;

Wyświetl plik

@ -10,7 +10,7 @@ import UploadContainer from '../containers/upload_container';
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities';
const UploadForm = () => {
const mediaIds = useAppSelector((state) => state.compose.get('media_attachments').map((item: AttachmentEntity) => item.get('id')));
const mediaIds = useAppSelector((state) => state.compose.media_attachments.map((item: AttachmentEntity) => item.id));
const classes = classNames('compose-form__uploads-wrapper', {
'contains-media': mediaIds.size !== 0,
});

Wyświetl plik

@ -10,7 +10,7 @@ const getStatus = makeGetStatus();
/** QuotedStatus shown in post composer. */
const QuotedStatusContainer: React.FC = () => {
const dispatch = useAppDispatch();
const status = useAppSelector(state => getStatus(state, { id: state.compose.get('quote') }));
const status = useAppSelector(state => getStatus(state, { id: state.compose.quote! }));
const onCancel = () => {
dispatch(cancelQuoteCompose());

Wyświetl plik

@ -26,7 +26,7 @@ const Account: React.FC<IAccount> = ({ accountId, author }) => {
const dispatch = useAppDispatch();
const account = useAppSelector((state) => getAccount(state, accountId));
const added = useAppSelector((state) => !!account && state.compose.get('to').includes(account.acct));
const added = useAppSelector((state) => !!account && state.compose.to?.includes(account.acct));
const onRemove = () => dispatch(removeFromMentions(accountId));
const onAdd = () => dispatch(addToMentions(accountId));

Wyświetl plik

@ -165,7 +165,7 @@ const makeMapStateToProps = () => {
status,
ancestorsIds,
descendantsIds,
askReplyConfirmation: state.compose.get('text', '').trim().length !== 0,
askReplyConfirmation: state.compose.text.trim().length !== 0,
me: state.me,
displayMedia: getSettings(state).get('displayMedia'),
allowedEmoji: soapbox.allowedEmoji,

Wyświetl plik

@ -1,91 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { cancelReplyCompose } from '../../../actions/compose';
import { openModal, closeModal } from '../../../actions/modals';
import { Modal } from '../../../components/ui';
import ComposeFormContainer from '../../compose/containers/compose_form_container';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
});
const mapStateToProps = state => {
const me = state.get('me');
return {
statusId: state.getIn(['compose', 'id']),
account: state.getIn(['accounts', me]),
composeText: state.getIn(['compose', 'text']),
privacy: state.getIn(['compose', 'privacy']),
inReplyTo: state.getIn(['compose', 'in_reply_to']),
quote: state.getIn(['compose', 'quote']),
};
};
class ComposeModal extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record,
intl: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
composeText: PropTypes.string,
privacy: PropTypes.string,
inReplyTo: PropTypes.string,
quote: PropTypes.string,
dispatch: PropTypes.func.isRequired,
};
onClickClose = () => {
const { composeText, dispatch, onClose, intl } = this.props;
if (composeText) {
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/trash.svg'),
heading: <FormattedMessage id='confirmations.delete.heading' defaultMessage='Delete post' />,
message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this post?' />,
confirm: intl.formatMessage(messages.confirm),
onConfirm: () => {
dispatch(closeModal('COMPOSE'));
dispatch(cancelReplyCompose());
},
}));
} else {
onClose('COMPOSE');
}
};
renderTitle = () => {
const { statusId, privacy, inReplyTo, quote } = this.props;
if (statusId) {
return <FormattedMessage id='navigation_bar.compose_edit' defaultMessage='Edit post' />;
} else if (privacy === 'direct') {
return <FormattedMessage id='navigation_bar.compose_direct' defaultMessage='Direct message' />;
} else if (inReplyTo) {
return <FormattedMessage id='navigation_bar.compose_reply' defaultMessage='Reply to post' />;
} else if (quote) {
return <FormattedMessage id='navigation_bar.compose_quote' defaultMessage='Quote post' />;
} else {
return <FormattedMessage id='navigation_bar.compose' defaultMessage='Compose new post' />;
}
}
render() {
return (
<Modal
title={this.renderTitle()}
onClose={this.onClickClose}
>
<ComposeFormContainer />
</Modal>
);
}
}
export default injectIntl(connect(mapStateToProps)(ComposeModal));

Wyświetl plik

@ -0,0 +1,71 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { cancelReplyCompose } from 'soapbox/actions/compose';
import { openModal, closeModal } from 'soapbox/actions/modals';
import { Modal } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import ComposeFormContainer from '../../compose/containers/compose_form_container';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
});
interface IComposeModal {
onClose: (type?: string) => void,
}
const ComposeModal: React.FC<IComposeModal> = ({ onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const statusId = useAppSelector((state) => state.compose.id);
const composeText = useAppSelector((state) => state.compose.text);
const privacy = useAppSelector((state) => state.compose.privacy);
const inReplyTo = useAppSelector((state) => state.compose.in_reply_to);
const quote = useAppSelector((state) => state.compose.quote);
const onClickClose = () => {
if (composeText) {
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/trash.svg'),
heading: <FormattedMessage id='confirmations.delete.heading' defaultMessage='Delete post' />,
message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this post?' />,
confirm: intl.formatMessage(messages.confirm),
onConfirm: () => {
dispatch(closeModal('COMPOSE'));
dispatch(cancelReplyCompose());
},
}));
} else {
onClose('COMPOSE');
}
};
const renderTitle = () => {
if (statusId) {
return <FormattedMessage id='navigation_bar.compose_edit' defaultMessage='Edit post' />;
} else if (privacy === 'direct') {
return <FormattedMessage id='navigation_bar.compose_direct' defaultMessage='Direct message' />;
} else if (inReplyTo) {
return <FormattedMessage id='navigation_bar.compose_reply' defaultMessage='Reply to post' />;
} else if (quote) {
return <FormattedMessage id='navigation_bar.compose_quote' defaultMessage='Quote post' />;
} else {
return <FormattedMessage id='navigation_bar.compose' defaultMessage='Compose new post' />;
}
};
return (
<Modal
title={renderTitle()}
onClose={onClickClose}
>
<ComposeFormContainer />
</Modal>
);
};
export default ComposeModal;

Wyświetl plik

@ -15,10 +15,10 @@ interface IReplyMentionsModal {
}
const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') }));
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.in_reply_to! }));
const account = useAppSelector((state) => state.accounts.get(state.me));
const mentions = statusToMentionsAccountIdsArray(status, account);
const mentions = statusToMentionsAccountIdsArray(status!, account!);
const author = (status?.account as AccountEntity).id;
const onClickClose = () => {

Wyświetl plik

@ -4,21 +4,22 @@ import { normalizeStatus } from 'soapbox/normalizers/status';
import { calculateStatus } from 'soapbox/reducers/statuses';
import { makeGetAccount } from 'soapbox/selectors';
import type { PendingStatus } from 'soapbox/reducers/pending_statuses';
import type { RootState } from 'soapbox/store';
const getAccount = makeGetAccount();
const buildMentions = (pendingStatus: ImmutableMap<string, any>) => {
if (pendingStatus.get('in_reply_to_id')) {
return ImmutableList(pendingStatus.get('to') || []).map(acct => ImmutableMap({ acct }));
const buildMentions = (pendingStatus: PendingStatus) => {
if (pendingStatus.in_reply_to_id) {
return ImmutableList(pendingStatus.to || []).map(acct => ImmutableMap({ acct }));
} else {
return ImmutableList();
}
};
const buildPoll = (pendingStatus: ImmutableMap<string, any>) => {
const buildPoll = (pendingStatus: PendingStatus) => {
if (pendingStatus.hasIn(['poll', 'options'])) {
return pendingStatus.get('poll').update('options', (options: ImmutableMap<string, any>) => {
return pendingStatus.poll!.update('options', (options: ImmutableMap<string, any>) => {
return options.map((title: string) => ImmutableMap({ title }));
});
} else {
@ -26,23 +27,23 @@ const buildPoll = (pendingStatus: ImmutableMap<string, any>) => {
}
};
export const buildStatus = (state: RootState, pendingStatus: ImmutableMap<string, any>, idempotencyKey: string) => {
export const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotencyKey: string) => {
const me = state.me as string;
const account = getAccount(state, me);
const inReplyToId = pendingStatus.get('in_reply_to_id');
const inReplyToId = pendingStatus.in_reply_to_id;
const status = ImmutableMap({
account,
content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
content: pendingStatus.status.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
id: `末pending-${idempotencyKey}`,
in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null),
in_reply_to_id: inReplyToId,
media_attachments: pendingStatus.get('media_ids', ImmutableList()).map((id: string) => ImmutableMap({ id })),
media_attachments: (pendingStatus.media_ids || ImmutableList()).map((id: string) => ImmutableMap({ id })),
mentions: buildMentions(pendingStatus),
poll: buildPoll(pendingStatus),
quote: pendingStatus.get('quote_id', null),
sensitive: pendingStatus.get('sensitive', false),
visibility: pendingStatus.get('visibility', 'public'),
quote: pendingStatus.quote_id,
sensitive: pendingStatus.sensitive,
visibility: pendingStatus.visibility,
});
return calculateStatus(normalizeStatus(status));

Wyświetl plik

@ -19,7 +19,7 @@ import { normalizePoll } from 'soapbox/normalizers/poll';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, Attachment, Card, Emoji, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct';
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct';
// https://docs.joinmastodon.org/entities/status/
export const StatusRecord = ImmutableRecord({

Wyświetl plik

@ -1,28 +0,0 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import {
BACKUPS_FETCH_SUCCESS,
BACKUPS_CREATE_SUCCESS,
} from '../actions/backups';
const initialState = ImmutableMap();
const importBackup = (state, backup) => {
return state.set(backup.get('inserted_at'), backup);
};
const importBackups = (state, backups) => {
return state.withMutations(mutable => {
backups.forEach(backup => importBackup(mutable, backup));
});
};
export default function backups(state = initialState, action) {
switch (action.type) {
case BACKUPS_FETCH_SUCCESS:
case BACKUPS_CREATE_SUCCESS:
return importBackups(state, fromJS(action.backups));
default:
return state;
}
}

Wyświetl plik

@ -0,0 +1,43 @@
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import {
BACKUPS_FETCH_SUCCESS,
BACKUPS_CREATE_SUCCESS,
} from '../actions/backups';
import type { AnyAction } from 'redux';
import type { APIEntity } from 'soapbox/types/entities';
const BackupRecord = ImmutableRecord({
id: null as number | null,
content_type: '',
url: '',
file_size: null as number | null,
processed: false,
inserted_at: '',
});
type Backup = ReturnType<typeof BackupRecord>;
type State = ImmutableMap<string, Backup>;
const initialState: State = ImmutableMap();
const importBackup = (state: State, backup: APIEntity) => {
return state.set(backup.inserted_at, BackupRecord(backup));
};
const importBackups = (state: State, backups: APIEntity[]) => {
return state.withMutations(mutable => {
backups.forEach(backup => importBackup(mutable, backup));
});
};
export default function backups(state = initialState, action: AnyAction) {
switch (action.type) {
case BACKUPS_FETCH_SUCCESS:
case BACKUPS_CREATE_SUCCESS:
return importBackups(state, action.backups);
default:
return state;
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
import { v4 as uuid } from 'uuid';
import { tagHistory } from 'soapbox/settings';
@ -55,86 +55,103 @@ import {
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me';
import { SETTING_CHANGE, FE_NAME } from '../actions/settings';
import { TIMELINE_DELETE } from '../actions/timelines';
import { normalizeAttachment } from '../normalizers/attachment';
import { unescapeHTML } from '../utils/html';
const initialState = ImmutableMap({
id: null,
mounted: 0,
sensitive: false,
spoiler: false,
spoiler_text: '',
content_type: 'text/plain',
privacy: 'public',
text: '',
focusDate: null,
caretPosition: null,
in_reply_to: null,
quote: null,
is_composing: false,
is_submitting: false,
is_changing_upload: false,
is_uploading: false,
progress: 0,
media_attachments: ImmutableList(),
poll: null,
suggestion_token: null,
suggestions: ImmutableList(),
default_privacy: 'public',
default_sensitive: false,
default_content_type: 'text/plain',
resetFileKey: Math.floor((Math.random() * 0x10000)),
idempotencyKey: uuid(),
tagHistory: ImmutableList(),
});
import type { AnyAction } from 'redux';
import type { Emoji } from 'soapbox/components/autosuggest_emoji';
import type {
Account as AccountEntity,
APIEntity,
Attachment as AttachmentEntity,
Status as StatusEntity,
} from 'soapbox/types/entities';
const initialPoll = ImmutableMap({
const getResetFileKey = () => Math.floor((Math.random() * 0x10000));
const PollRecord = ImmutableRecord({
options: ImmutableList(['', '']),
expires_in: 24 * 3600,
multiple: false,
});
const statusToTextMentions = (state, status, account) => {
const ReducerRecord = ImmutableRecord({
caretPosition: null as number | null,
content_type: 'text/plain',
default_content_type: 'text/plain',
default_privacy: 'public',
default_sensitive: false,
focusDate: null as Date | null,
idempotencyKey: uuid(),
id: null as string | null,
in_reply_to: null as string | null,
is_changing_upload: false,
is_composing: false,
is_submitting: false,
is_uploading: false,
media_attachments: ImmutableList<AttachmentEntity>(),
mounted: 0,
poll: null as Poll | null,
privacy: 'public',
progress: 0,
quote: null as string | null,
resetFileKey: null as number | null,
schedule: null as Date | null,
sensitive: false,
spoiler: false,
spoiler_text: '',
suggestions: ImmutableList(),
suggestion_token: null as string | null,
tagHistory: ImmutableList<string>(),
text: '',
to: null as ImmutableOrderedSet<string> | null,
});
type State = ReturnType<typeof ReducerRecord>;
type Poll = ReturnType<typeof PollRecord>;
const statusToTextMentions = (state: State, status: ImmutableMap<string, any>, account: AccountEntity) => {
const author = status.getIn(['account', 'acct']);
const mentions = status.get('mentions', []).map(m => m.get('acct'));
const mentions = status.get('mentions').map((m: ImmutableMap<string, any>) => m.get('acct'));
return ImmutableOrderedSet([author])
.concat(mentions)
.delete(account.get('acct'))
.delete(account.acct)
.map(m => `@${m} `)
.join('');
};
export const statusToMentionsArray = (status, account) => {
const author = status.getIn(['account', 'acct']);
const mentions = status.get('mentions', []).map(m => m.get('acct'));
export const statusToMentionsArray = (status: ImmutableMap<string, any>, account: AccountEntity) => {
const author = status.getIn(['account', 'acct']) as string;
const mentions = status.get('mentions').map((m: ImmutableMap<string, any>) => m.get('acct'));
return ImmutableOrderedSet([author])
.concat(mentions)
.delete(account.get('acct'));
.delete(account.get('acct')) as ImmutableOrderedSet<string>;
};
export const statusToMentionsAccountIdsArray = (status, account) => {
const author = status.getIn(['account', 'id']);
const mentions = status.get('mentions', []).map(m => m.get('id'));
export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: AccountEntity) => {
const author = (status.account as AccountEntity).id;
const mentions = status.mentions.map((m) => m.id);
return ImmutableOrderedSet([author])
.concat(mentions)
.delete(account.get('id'));
.delete(account.id) as ImmutableOrderedSet<string>;
};
function clearAll(state) {
function clearAll(state: State) {
return state.withMutations(map => {
map.set('id', null);
map.set('text', '');
map.set('to', ImmutableOrderedSet());
map.set('spoiler', false);
map.set('spoiler_text', '');
map.set('content_type', state.get('default_content_type'));
map.set('content_type', state.default_content_type);
map.set('is_submitting', false);
map.set('is_changing_upload', false);
map.set('in_reply_to', null);
map.set('quote', null);
map.set('privacy', state.get('default_privacy'));
map.set('privacy', state.default_privacy);
map.set('sensitive', false);
map.set('media_attachments', ImmutableList());
map.set('poll', null);
@ -143,26 +160,26 @@ function clearAll(state) {
});
}
function appendMedia(state, media) {
const prevSize = state.get('media_attachments').size;
function appendMedia(state: State, media: APIEntity) {
const prevSize = state.media_attachments.size;
return state.withMutations(map => {
map.update('media_attachments', list => list.push(media));
map.update('media_attachments', list => list.push(normalizeAttachment(media)));
map.set('is_uploading', false);
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
map.set('idempotencyKey', uuid());
if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) {
if (prevSize === 0 && (state.default_sensitive || state.spoiler)) {
map.set('sensitive', true);
}
});
}
function removeMedia(state, mediaId) {
const prevSize = state.get('media_attachments').size;
function removeMedia(state: State, mediaId: string) {
const prevSize = state.media_attachments.size;
return state.withMutations(map => {
map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId));
map.update('media_attachments', list => list.filterNot(item => item.id === mediaId));
map.set('idempotencyKey', uuid());
if (prevSize === 1) {
@ -171,9 +188,9 @@ function removeMedia(state, mediaId) {
});
}
const insertSuggestion = (state, position, token, completion, path) => {
const insertSuggestion = (state: State, position: number, token: string, completion: string, path: Array<string | number>) => {
return state.withMutations(map => {
map.updateIn(path, oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
map.updateIn(path, oldText => `${(oldText as string).slice(0, position)}${completion} ${(oldText as string).slice(position + token.length)}`);
map.set('suggestion_token', null);
map.set('suggestions', ImmutableList());
if (path.length === 1 && path[0] === 'text') {
@ -184,11 +201,11 @@ const insertSuggestion = (state, position, token, completion, path) => {
});
};
const updateSuggestionTags = (state, token) => {
const updateSuggestionTags = (state: State, token: string) => {
const prefix = token.slice(1);
return state.merge({
suggestions: state.get('tagHistory')
suggestions: state.tagHistory
.filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase()))
.slice(0, 4)
.map(tag => '#' + tag),
@ -196,8 +213,8 @@ const updateSuggestionTags = (state, token) => {
});
};
const insertEmoji = (state, position, emojiData, needsSpace) => {
const oldText = state.get('text');
const insertEmoji = (state: State, position: number, emojiData: Emoji, needsSpace: boolean) => {
const oldText = state.text;
const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native;
return state.merge({
@ -208,17 +225,17 @@ const insertEmoji = (state, position, emojiData, needsSpace) => {
});
};
const privacyPreference = (a, b) => {
const privacyPreference = (a: string, b: string) => {
const order = ['public', 'unlisted', 'private', 'direct'];
return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
};
const domParser = new DOMParser();
const expandMentions = status => {
const expandMentions = (status: ImmutableMap<string, any>) => {
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
status.get('mentions').forEach(mention => {
status.get('mentions').forEach((mention: ImmutableMap<string, any>) => {
const node = fragment.querySelector(`a[href="${mention.get('url')}"]`);
if (node) node.textContent = `@${mention.get('acct')}`;
});
@ -226,24 +243,23 @@ const expandMentions = status => {
return fragment.innerHTML;
};
const getExplicitMentions = (me, status) => {
const getExplicitMentions = (me: string, status: ImmutableMap<string, any>) => {
const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
const mentions = status
.get('mentions')
.filter(mention => !(fragment.querySelector(`a[href="${mention.get('url')}"]`) || mention.get('id') === me))
.map(m => m.get('acct'));
.filter((mention: ImmutableMap<string, any>) => !(fragment.querySelector(`a[href="${mention.get('url')}"]`) || mention.get('id') === me))
.map((m: ImmutableMap<string, any>) => m.get('acct'));
return ImmutableOrderedSet(mentions);
return ImmutableOrderedSet<string>(mentions);
};
const getAccountSettings = account => {
return account.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap());
const getAccountSettings = (account: ImmutableMap<string, any>) => {
return account.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap()) as ImmutableMap<string, any>;
};
const importAccount = (state, account) => {
account = fromJS(account);
const settings = getAccountSettings(account);
const importAccount = (state: State, account: APIEntity) => {
const settings = getAccountSettings(ImmutableMap(fromJS(account)));
const defaultPrivacy = settings.get('defaultPrivacy', 'public');
const defaultContentType = settings.get('defaultContentType', 'text/plain');
@ -253,13 +269,12 @@ const importAccount = (state, account) => {
privacy: defaultPrivacy,
default_content_type: defaultContentType,
content_type: defaultContentType,
tagHistory: ImmutableList(tagHistory.get(account.get('id'))),
tagHistory: ImmutableList(tagHistory.get(account.id)),
});
};
const updateAccount = (state, account) => {
account = fromJS(account);
const settings = getAccountSettings(account);
const updateAccount = (state: State, account: APIEntity) => {
const settings = getAccountSettings(ImmutableMap(fromJS(account)));
const defaultPrivacy = settings.get('defaultPrivacy');
const defaultContentType = settings.get('defaultContentType');
@ -270,7 +285,7 @@ const updateAccount = (state, account) => {
});
};
const updateSetting = (state, path, value) => {
const updateSetting = (state: State, path: string[], value: string) => {
const pathString = path.join(',');
switch (pathString) {
case 'defaultPrivacy':
@ -282,18 +297,18 @@ const updateSetting = (state, path, value) => {
}
};
export default function compose(state = initialState, action) {
export default function compose(state = ReducerRecord({ resetFileKey: getResetFileKey() }), action: AnyAction) {
switch (action.type) {
case COMPOSE_MOUNT:
return state.set('mounted', state.get('mounted') + 1);
return state.set('mounted', state.mounted + 1);
case COMPOSE_UNMOUNT:
return state
.set('mounted', Math.max(state.get('mounted') - 1, 0))
.set('mounted', Math.max(state.mounted - 1, 0))
.set('is_composing', false);
case COMPOSE_SENSITIVITY_CHANGE:
return state.withMutations(map => {
if (!state.get('spoiler')) {
map.set('sensitive', !state.get('sensitive'));
if (!state.spoiler) {
map.set('sensitive', !state.sensitive);
}
map.set('idempotencyKey', uuid());
@ -306,10 +321,10 @@ export default function compose(state = initialState, action) {
case COMPOSE_SPOILERNESS_CHANGE:
return state.withMutations(map => {
map.set('spoiler_text', '');
map.set('spoiler', !state.get('spoiler'));
map.set('spoiler', !state.spoiler);
map.set('idempotencyKey', uuid());
if (!state.get('sensitive') && state.get('media_attachments').size >= 1) {
if (!state.sensitive && state.media_attachments.size >= 1) {
map.set('sensitive', true);
}
});
@ -330,17 +345,17 @@ export default function compose(state = initialState, action) {
case COMPOSE_REPLY:
return state.withMutations(map => {
map.set('in_reply_to', action.status.get('id'));
map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet());
map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet<string>());
map.set('text', !action.explicitAddressing ? statusToTextMentions(state, action.status, action.account) : '');
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.set('privacy', privacyPreference(action.status.visibility, state.default_privacy));
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('idempotencyKey', uuid());
map.set('content_type', state.get('default_content_type'));
map.set('content_type', state.default_content_type);
if (action.status.get('spoiler_text', '').length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text'));
map.set('spoiler_text', action.status.spoiler_text);
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
@ -349,13 +364,13 @@ export default function compose(state = initialState, action) {
case COMPOSE_QUOTE:
return state.withMutations(map => {
map.set('quote', action.status.get('id'));
map.set('to', undefined);
map.set('to', null);
map.set('text', '');
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.set('privacy', privacyPreference(action.status.visibility, state.default_privacy));
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('idempotencyKey', uuid());
map.set('content_type', state.get('default_content_type'));
map.set('content_type', state.default_content_type);
map.set('spoiler', false);
map.set('spoiler_text', '');
});
@ -398,19 +413,19 @@ export default function compose(state = initialState, action) {
map.set('idempotencyKey', uuid());
});
case COMPOSE_SUGGESTIONS_CLEAR:
return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
return state.update('suggestions', list => list.clear()).set('suggestion_token', null);
case COMPOSE_SUGGESTIONS_READY:
return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map((item: APIEntity) => item.id) : action.emojis)).set('suggestion_token', action.token);
case COMPOSE_SUGGESTION_SELECT:
return insertSuggestion(state, action.position, action.token, action.completion, action.path);
case COMPOSE_SUGGESTION_TAGS_UPDATE:
return updateSuggestionTags(state, action.token);
case COMPOSE_TAG_HISTORY_UPDATE:
return state.set('tagHistory', fromJS(action.tags));
return state.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList<string>);
case TIMELINE_DELETE:
if (action.id === state.get('in_reply_to')) {
if (action.id === state.in_reply_to) {
return state.set('in_reply_to', null);
} if (action.id === state.get('quote')) {
} if (action.id === state.quote) {
return state.set('quote', null);
} else {
return state;
@ -421,8 +436,8 @@ export default function compose(state = initialState, action) {
return state
.set('is_changing_upload', false)
.update('media_attachments', list => list.map(item => {
if (item.get('id') === action.media.id) {
return fromJS(action.media);
if (item.id === action.media.id) {
return normalizeAttachment(action.media);
}
return item;
@ -433,7 +448,7 @@ export default function compose(state = initialState, action) {
map.set('id', action.status.get('id'));
}
map.set('text', action.rawText || unescapeHTML(expandMentions(action.status)));
map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.get('account', 'id'), action.status) : ImmutableOrderedSet());
map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet<string>());
map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('privacy', action.status.get('visibility'));
map.set('focusDate', new Date());
@ -444,7 +459,7 @@ export default function compose(state = initialState, action) {
if (action.v?.software === PLEROMA && hasIntegerMediaIds(action.status)) {
map.set('media_attachments', ImmutableList());
} else {
map.set('media_attachments', action.status.get('media_attachments'));
map.set('media_attachments', action.status.media_attachments);
}
if (action.status.get('spoiler_text').length > 0) {
@ -456,15 +471,15 @@ export default function compose(state = initialState, action) {
}
if (action.status.get('poll')) {
map.set('poll', ImmutableMap({
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
multiple: action.status.getIn(['poll', 'multiple']),
map.set('poll', PollRecord({
options: action.status.poll.options.map((x: APIEntity) => x.get('title')),
multiple: action.status.poll.multiple,
expires_in: 24 * 3600,
}));
}
});
case COMPOSE_POLL_ADD:
return state.set('poll', initialPoll);
return state.set('poll', PollRecord());
case COMPOSE_POLL_REMOVE:
return state.set('poll', null);
case COMPOSE_SCHEDULE_ADD:
@ -474,17 +489,17 @@ export default function compose(state = initialState, action) {
case COMPOSE_SCHEDULE_REMOVE:
return state.set('schedule', null);
case COMPOSE_POLL_OPTION_ADD:
return state.updateIn(['poll', 'options'], options => options.push(action.title));
return state.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).push(action.title));
case COMPOSE_POLL_OPTION_CHANGE:
return state.setIn(['poll', 'options', action.index], action.title);
case COMPOSE_POLL_OPTION_REMOVE:
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
return state.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).delete(action.index));
case COMPOSE_POLL_SETTINGS_CHANGE:
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
return state.update('poll', poll => poll!.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
case COMPOSE_ADD_TO_MENTIONS:
return state.update('to', mentions => mentions.add(action.account));
return state.update('to', mentions => mentions!.add(action.account));
case COMPOSE_REMOVE_FROM_MENTIONS:
return state.update('to', mentions => mentions.delete(action.account));
return state.update('to', mentions => mentions!.delete(action.account));
case ME_FETCH_SUCCESS:
return importAccount(state, action.me);
case ME_PATCH_SUCCESS:

Wyświetl plik

@ -1,25 +0,0 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import {
STATUS_CREATE_REQUEST,
STATUS_CREATE_SUCCESS,
} from 'soapbox/actions/statuses';
const importStatus = (state, params, idempotencyKey) => {
return state.set(idempotencyKey, params);
};
const deleteStatus = (state, idempotencyKey) => state.delete(idempotencyKey);
const initialState = ImmutableMap();
export default function pending_statuses(state = initialState, action) {
switch (action.type) {
case STATUS_CREATE_REQUEST:
return importStatus(state, fromJS(action.params), action.idempotencyKey);
case STATUS_CREATE_SUCCESS:
return deleteStatus(state, action.idempotencyKey);
default:
return state;
}
}

Wyświetl plik

@ -0,0 +1,44 @@
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import { AnyAction } from 'redux';
import {
STATUS_CREATE_REQUEST,
STATUS_CREATE_SUCCESS,
} from 'soapbox/actions/statuses';
import type { StatusVisibility } from 'soapbox/normalizers/status';
const PendingStatusRecord = ImmutableRecord({
content_type: '',
in_reply_to_id: null as string | null,
media_ids: null as ImmutableList<string> | null,
quote_id: null as string | null,
poll: null as ImmutableMap<string, any> | null,
sensitive: false,
spoiler_text: '',
status: '',
to: null as ImmutableList<string> | null,
visibility: 'public' as StatusVisibility,
});
export type PendingStatus = ReturnType<typeof PendingStatusRecord>;
type State = ImmutableMap<string, PendingStatus>;
const initialState: State = ImmutableMap();
const importStatus = (state: State, params: ImmutableMap<string, any>, idempotencyKey: string) => {
return state.set(idempotencyKey, PendingStatusRecord(params));
};
const deleteStatus = (state: State, idempotencyKey: string) => state.delete(idempotencyKey);
export default function pending_statuses(state = initialState, action: AnyAction) {
switch (action.type) {
case STATUS_CREATE_REQUEST:
return importStatus(state, ImmutableMap(fromJS(action.params)), action.idempotencyKey);
case STATUS_CREATE_SUCCESS:
return deleteStatus(state, action.idempotencyKey);
default:
return state;
}
}