Allow multiple compose forms
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
6a1c3a7975
commit
a9b8371086
|
@ -54,9 +54,6 @@ const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
|||
|
||||
const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
||||
|
||||
const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
|
||||
const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
||||
|
||||
const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||
const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
||||
|
@ -101,14 +98,6 @@ const messages = defineMessages({
|
|||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
|
||||
|
||||
const ensureComposeIsVisible = (getState: () => RootState, routerHistory: History) => {
|
||||
if (!getState().compose.mounted && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
|
||||
routerHistory.push('/posts/new');
|
||||
}
|
||||
};
|
||||
|
||||
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { instance } = getState();
|
||||
|
@ -126,8 +115,9 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin
|
|||
});
|
||||
};
|
||||
|
||||
const changeCompose = (text: string) => ({
|
||||
const changeCompose = (composeId: string, text: string) => ({
|
||||
type: COMPOSE_CHANGE,
|
||||
id: composeId,
|
||||
text: text,
|
||||
});
|
||||
|
||||
|
@ -147,20 +137,6 @@ const replyCompose = (status: Status) =>
|
|||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
const replyComposeWithConfirmation = (status: Status, intl: IntlShape) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
if (state.compose.text.trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(status)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(status));
|
||||
}
|
||||
};
|
||||
|
||||
const cancelReplyCompose = () => ({
|
||||
type: COMPOSE_REPLY_CANCEL,
|
||||
});
|
||||
|
@ -221,16 +197,16 @@ const directComposeById = (accountId: string) =>
|
|||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, data: APIEntity, status: string, edit?: boolean) => {
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: APIEntity, status: string, edit?: boolean) => {
|
||||
if (!dispatch || !getState) return;
|
||||
|
||||
dispatch(insertIntoTagHistory(data.tags || [], status));
|
||||
dispatch(submitComposeSuccess({ ...data }));
|
||||
dispatch(insertIntoTagHistory(composeId, data.tags || [], status));
|
||||
dispatch(submitComposeSuccess(composeId, { ...data }));
|
||||
dispatch(snackbar.success(edit ? messages.editSuccess : messages.success, messages.view, `/@${data.account.acct}/posts/${data.id}`));
|
||||
};
|
||||
|
||||
const needsDescriptions = (state: RootState) => {
|
||||
const media = state.compose.media_attachments;
|
||||
const needsDescriptions = (state: RootState, composeId: string) => {
|
||||
const media = state.compose.get(composeId)!.media_attachments;
|
||||
const missingDescriptionModal = getSettings(state).get('missingDescriptionModal');
|
||||
|
||||
const hasMissing = media.filter(item => !item.description).size > 0;
|
||||
|
@ -238,8 +214,8 @@ const needsDescriptions = (state: RootState) => {
|
|||
return missingDescriptionModal && hasMissing;
|
||||
};
|
||||
|
||||
const validateSchedule = (state: RootState) => {
|
||||
const schedule = state.compose.schedule;
|
||||
const validateSchedule = (state: RootState, composeId: string) => {
|
||||
const schedule = state.compose.get(composeId)!.schedule;
|
||||
if (!schedule) return true;
|
||||
|
||||
const fiveMinutesFromNow = new Date(new Date().getTime() + 300000);
|
||||
|
@ -247,17 +223,19 @@ const validateSchedule = (state: RootState) => {
|
|||
return schedule.getTime() > fiveMinutesFromNow.getTime();
|
||||
};
|
||||
|
||||
const submitCompose = (routerHistory?: History, force = false) =>
|
||||
const submitCompose = (composeId: string, routerHistory?: History, force = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const state = getState();
|
||||
|
||||
const status = state.compose.text;
|
||||
const media = state.compose.media_attachments;
|
||||
const statusId = state.compose.id;
|
||||
let to = state.compose.to;
|
||||
const compose = state.compose.get(composeId)!;
|
||||
|
||||
if (!validateSchedule(state)) {
|
||||
const status = compose.text;
|
||||
const media = compose.media_attachments;
|
||||
const statusId = compose.id;
|
||||
let to = compose.to;
|
||||
|
||||
if (!validateSchedule(state, composeId)) {
|
||||
dispatch(snackbar.error(messages.scheduleError));
|
||||
return;
|
||||
}
|
||||
|
@ -266,11 +244,11 @@ const submitCompose = (routerHistory?: History, force = false) =>
|
|||
return;
|
||||
}
|
||||
|
||||
if (!force && needsDescriptions(state)) {
|
||||
if (!force && needsDescriptions(state, composeId)) {
|
||||
dispatch(openModal('MISSING_DESCRIPTION', {
|
||||
onContinue: () => {
|
||||
dispatch(closeModal('MISSING_DESCRIPTION'));
|
||||
dispatch(submitCompose(routerHistory, true));
|
||||
dispatch(submitCompose(composeId, routerHistory, true));
|
||||
},
|
||||
}));
|
||||
return;
|
||||
|
@ -282,22 +260,22 @@ const submitCompose = (routerHistory?: History, force = false) =>
|
|||
to = to.union(mentions.map(mention => mention.trim().slice(1)));
|
||||
}
|
||||
|
||||
dispatch(submitComposeRequest());
|
||||
dispatch(submitComposeRequest(composeId));
|
||||
dispatch(closeModal());
|
||||
|
||||
const idempotencyKey = state.compose.idempotencyKey;
|
||||
const idempotencyKey = compose.idempotencyKey;
|
||||
|
||||
const params = {
|
||||
status,
|
||||
in_reply_to_id: state.compose.in_reply_to,
|
||||
quote_id: state.compose.quote,
|
||||
in_reply_to_id: compose.in_reply_to,
|
||||
quote_id: 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,
|
||||
sensitive: compose.sensitive,
|
||||
spoiler_text: compose.spoiler_text,
|
||||
visibility: compose.privacy,
|
||||
content_type: compose.content_type,
|
||||
poll: compose.poll,
|
||||
scheduled_at: compose.schedule,
|
||||
to,
|
||||
};
|
||||
|
||||
|
@ -305,27 +283,30 @@ const submitCompose = (routerHistory?: History, force = false) =>
|
|||
if (!statusId && data.visibility === 'direct' && getState().conversations.mounted <= 0 && routerHistory) {
|
||||
routerHistory.push('/messages');
|
||||
}
|
||||
handleComposeSubmit(dispatch, getState, data, status, !!statusId);
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
}).catch(function(error) {
|
||||
dispatch(submitComposeFail(error));
|
||||
dispatch(submitComposeFail(composeId, error));
|
||||
});
|
||||
};
|
||||
|
||||
const submitComposeRequest = () => ({
|
||||
const submitComposeRequest = (composeId: string) => ({
|
||||
type: COMPOSE_SUBMIT_REQUEST,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const submitComposeSuccess = (status: APIEntity) => ({
|
||||
const submitComposeSuccess = (composeId: string, status: APIEntity) => ({
|
||||
type: COMPOSE_SUBMIT_SUCCESS,
|
||||
id: composeId,
|
||||
status: status,
|
||||
});
|
||||
|
||||
const submitComposeFail = (error: AxiosError) => ({
|
||||
const submitComposeFail = (composeId: string, error: AxiosError) => ({
|
||||
type: COMPOSE_SUBMIT_FAIL,
|
||||
id: composeId,
|
||||
error: error,
|
||||
});
|
||||
|
||||
const uploadCompose = (files: FileList, intl: IntlShape) =>
|
||||
const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const attachmentLimit = getState().instance.configuration.getIn(['statuses', 'max_media_attachments']) as number;
|
||||
|
@ -333,7 +314,7 @@ const uploadCompose = (files: FileList, intl: IntlShape) =>
|
|||
const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined;
|
||||
const maxVideoDuration = getState().instance.configuration.getIn(['media_attachments', 'video_duration_limit']) as number | undefined;
|
||||
|
||||
const media = getState().compose.media_attachments;
|
||||
const media = getState().compose.get(composeId)!.media_attachments;
|
||||
const progress = new Array(files.length).fill(0);
|
||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
|
||||
|
@ -576,10 +557,10 @@ const updateTagHistory = (tags: string[]) => ({
|
|||
tags,
|
||||
});
|
||||
|
||||
const insertIntoTagHistory = (recognizedTags: APIEntity[], text: string) =>
|
||||
const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], text: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const oldHistory = state.compose.tagHistory;
|
||||
const oldHistory = state.compose.get(composeId)!.tagHistory;
|
||||
const me = state.me;
|
||||
const names = recognizedTags
|
||||
.filter(tag => text.match(new RegExp(`#${tag.name}`, 'i')))
|
||||
|
@ -594,97 +575,99 @@ const insertIntoTagHistory = (recognizedTags: APIEntity[], text: string) =>
|
|||
dispatch(updateTagHistory(newHistory));
|
||||
};
|
||||
|
||||
const mountCompose = () => ({
|
||||
type: COMPOSE_MOUNT,
|
||||
});
|
||||
|
||||
const unmountCompose = () => ({
|
||||
type: COMPOSE_UNMOUNT,
|
||||
});
|
||||
|
||||
const changeComposeSensitivity = () => ({
|
||||
const changeComposeSensitivity = (composeId: string) => ({
|
||||
type: COMPOSE_SENSITIVITY_CHANGE,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const changeComposeSpoilerness = () => ({
|
||||
const changeComposeSpoilerness = (composeId: string) => ({
|
||||
type: COMPOSE_SPOILERNESS_CHANGE,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const changeComposeContentType = (value: string) => ({
|
||||
const changeComposeContentType = (composeId: string, value: string) => ({
|
||||
type: COMPOSE_TYPE_CHANGE,
|
||||
id: composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
const changeComposeSpoilerText = (text: string) => ({
|
||||
const changeComposeSpoilerText = (composeId: string, text: string) => ({
|
||||
type: COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
id: composeId,
|
||||
text,
|
||||
});
|
||||
|
||||
const changeComposeVisibility = (value: string) => ({
|
||||
const changeComposeVisibility = (composeId: string, value: string) => ({
|
||||
type: COMPOSE_VISIBILITY_CHANGE,
|
||||
id: composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
const insertEmojiCompose = (position: number, emoji: string, needsSpace: boolean) => ({
|
||||
const insertEmojiCompose = (composeId: string, position: number, emoji: Emoji, needsSpace: boolean) => ({
|
||||
type: COMPOSE_EMOJI_INSERT,
|
||||
id: composeId,
|
||||
position,
|
||||
emoji,
|
||||
needsSpace,
|
||||
});
|
||||
|
||||
const changeComposing = (value: string) => ({
|
||||
type: COMPOSE_COMPOSING_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const addPoll = () => ({
|
||||
const addPoll = (composeId: string) => ({
|
||||
type: COMPOSE_POLL_ADD,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const removePoll = () => ({
|
||||
const removePoll = (composeId: string) => ({
|
||||
type: COMPOSE_POLL_REMOVE,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const addSchedule = () => ({
|
||||
const addSchedule = (composeId: string) => ({
|
||||
type: COMPOSE_SCHEDULE_ADD,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const setSchedule = (date: Date) => ({
|
||||
const setSchedule = (composeId: string, date: Date) => ({
|
||||
type: COMPOSE_SCHEDULE_SET,
|
||||
id: composeId,
|
||||
date: date,
|
||||
});
|
||||
|
||||
const removeSchedule = () => ({
|
||||
const removeSchedule = (composeId: string) => ({
|
||||
type: COMPOSE_SCHEDULE_REMOVE,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
const addPollOption = (title: string) => ({
|
||||
const addPollOption = (composeId: string, title: string) => ({
|
||||
type: COMPOSE_POLL_OPTION_ADD,
|
||||
id: composeId,
|
||||
title,
|
||||
});
|
||||
|
||||
const changePollOption = (index: number, title: string) => ({
|
||||
const changePollOption = (composeId: string, index: number, title: string) => ({
|
||||
type: COMPOSE_POLL_OPTION_CHANGE,
|
||||
id: composeId,
|
||||
index,
|
||||
title,
|
||||
});
|
||||
|
||||
const removePollOption = (index: number) => ({
|
||||
const removePollOption = (composeId: string, index: number) => ({
|
||||
type: COMPOSE_POLL_OPTION_REMOVE,
|
||||
id: composeId,
|
||||
index,
|
||||
});
|
||||
|
||||
const changePollSettings = (expiresIn?: string | number, isMultiple?: boolean) => ({
|
||||
const changePollSettings = (composeId: string, expiresIn?: string | number, isMultiple?: boolean) => ({
|
||||
type: COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
id: composeId,
|
||||
expiresIn,
|
||||
isMultiple,
|
||||
});
|
||||
|
||||
const openComposeWithText = (text = '') =>
|
||||
const openComposeWithText = (composeId: string, text = '') =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch(resetCompose());
|
||||
dispatch(openModal('COMPOSE'));
|
||||
dispatch(changeCompose(text));
|
||||
dispatch(changeCompose(composeId, text));
|
||||
};
|
||||
|
||||
const addToMentions = (accountId: string) =>
|
||||
|
@ -731,8 +714,6 @@ export {
|
|||
COMPOSE_SUGGESTION_SELECT,
|
||||
COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||
COMPOSE_TAG_HISTORY_UPDATE,
|
||||
COMPOSE_MOUNT,
|
||||
COMPOSE_UNMOUNT,
|
||||
COMPOSE_SENSITIVITY_CHANGE,
|
||||
COMPOSE_SPOILERNESS_CHANGE,
|
||||
COMPOSE_TYPE_CHANGE,
|
||||
|
@ -756,11 +737,9 @@ export {
|
|||
COMPOSE_ADD_TO_MENTIONS,
|
||||
COMPOSE_REMOVE_FROM_MENTIONS,
|
||||
COMPOSE_SET_STATUS,
|
||||
ensureComposeIsVisible,
|
||||
setComposeToStatus,
|
||||
changeCompose,
|
||||
replyCompose,
|
||||
replyComposeWithConfirmation,
|
||||
cancelReplyCompose,
|
||||
quoteCompose,
|
||||
cancelQuoteCompose,
|
||||
|
@ -790,15 +769,12 @@ export {
|
|||
selectComposeSuggestion,
|
||||
updateSuggestionTags,
|
||||
updateTagHistory,
|
||||
mountCompose,
|
||||
unmountCompose,
|
||||
changeComposeSensitivity,
|
||||
changeComposeSpoilerness,
|
||||
changeComposeContentType,
|
||||
changeComposeSpoilerText,
|
||||
changeComposeVisibility,
|
||||
insertEmojiCompose,
|
||||
changeComposing,
|
||||
addPoll,
|
||||
removePoll,
|
||||
addSchedule,
|
||||
|
|
|
@ -46,8 +46,8 @@ interface IAutosuggesteTextarea {
|
|||
onSuggestionsClearRequested: () => void,
|
||||
onSuggestionsFetchRequested: (token: string | number) => void,
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>,
|
||||
onKeyUp: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onPaste: (files: FileList) => void,
|
||||
autoFocus: boolean,
|
||||
onFocus: () => void,
|
||||
|
|
|
@ -15,7 +15,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
export const checkComposeContent = compose => {
|
||||
return [
|
||||
return !!compose && [
|
||||
compose.text.length > 0,
|
||||
compose.spoiler_text.length > 0,
|
||||
compose.media_attachments.size > 0,
|
||||
|
@ -24,8 +24,8 @@ export const checkComposeContent = compose => {
|
|||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasComposeContent: checkComposeContent(state.compose),
|
||||
isEditing: state.compose.id !== null,
|
||||
hasComposeContent: checkComposeContent(state.compose.get('modal')),
|
||||
isEditing: state.compose.get('modal')?.id !== null,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
|
|
@ -123,18 +123,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
|
||||
const handleReplyClick: React.MouseEventHandler = (e) => {
|
||||
if (me) {
|
||||
dispatch((_, getState) => {
|
||||
const state = getState();
|
||||
if (state.compose.text.trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(status)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(status));
|
||||
}
|
||||
});
|
||||
dispatch(replyCompose(status));
|
||||
} else {
|
||||
onOpenUnauthorizedModal('REPLY');
|
||||
}
|
||||
|
@ -186,18 +175,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
e.stopPropagation();
|
||||
|
||||
if (me) {
|
||||
dispatch((_, getState) => {
|
||||
const state = getState();
|
||||
if (state.compose.text.trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(quoteCompose(status)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(quoteCompose(status));
|
||||
}
|
||||
});
|
||||
dispatch(quoteCompose(status));
|
||||
} else {
|
||||
onOpenUnauthorizedModal('REBLOG');
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { HotKeys } from 'react-hotkeys';
|
|||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { NavLink, useHistory } from 'react-router-dom';
|
||||
|
||||
import { mentionCompose, replyComposeWithConfirmation } from 'soapbox/actions/compose';
|
||||
import { mentionCompose, replyCompose } from 'soapbox/actions/compose';
|
||||
import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { toggleStatusHidden } from 'soapbox/actions/statuses';
|
||||
|
@ -125,7 +125,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
const handleHotkeyReply = (e?: KeyboardEvent): void => {
|
||||
e?.preventDefault();
|
||||
dispatch(replyComposeWithConfirmation(actualStatus, intl));
|
||||
dispatch(replyCompose(actualStatus));
|
||||
};
|
||||
|
||||
const handleHotkeyFavourite = (): void => {
|
||||
|
|
|
@ -37,7 +37,7 @@ interface IStack extends React.HTMLAttributes<HTMLDivElement> {
|
|||
}
|
||||
|
||||
/** Vertical stack of child elements. */
|
||||
const Stack: React.FC<IStack> = React.forwardRef((props, ref: React.LegacyRef<HTMLDivElement> | undefined) => {
|
||||
const Stack = React.forwardRef<HTMLDivElement, IStack>((props, ref: React.LegacyRef<HTMLDivElement> | undefined) => {
|
||||
const { space, alignItems, justifyContent, className, grow, ...filteredProps } = props;
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,363 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { length } from 'stringz';
|
||||
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
changeComposeSpoilerText,
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
} from 'soapbox/actions/compose';
|
||||
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest_input';
|
||||
import AutosuggestTextarea from 'soapbox/components/autosuggest_textarea';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button, Stack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { isMobile } from 'soapbox/is_mobile';
|
||||
|
||||
import MarkdownButton from '../components/markdown_button';
|
||||
import PollButton from '../components/poll_button';
|
||||
import PollForm from '../components/polls/poll-form';
|
||||
import PrivacyDropdown from '../components/privacy_dropdown';
|
||||
import ReplyMentions from '../components/reply_mentions';
|
||||
import ScheduleButton from '../components/schedule_button';
|
||||
import SpoilerButton from '../components/spoiler_button';
|
||||
import UploadForm from '../components/upload_form';
|
||||
import Warning from '../components/warning';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import QuotedStatusContainer from '../containers/quoted_status_container';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import ScheduleFormContainer from '../containers/schedule_form_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import WarningContainer from '../containers/warning_container';
|
||||
import { countableText } from '../util/counter';
|
||||
|
||||
import TextCharacterCounter from './text_character_counter';
|
||||
import VisualCharacterCounter from './visual_character_counter';
|
||||
|
||||
import type { Emoji } from 'soapbox/components/autosuggest_emoji';
|
||||
|
||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||
|
||||
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}!' },
|
||||
message: { id: 'compose_form.message', defaultMessage: 'Message' },
|
||||
schedule: { id: 'compose_form.schedule', defaultMessage: 'Schedule' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
});
|
||||
|
||||
interface IComposeForm {
|
||||
id: string,
|
||||
shouldCondense?: boolean,
|
||||
autoFocus?: boolean,
|
||||
clickableAreaRef?: React.RefObject<HTMLDivElement>,
|
||||
}
|
||||
|
||||
const ComposeForm: React.FC<IComposeForm> = ({ id, shouldCondense, autoFocus, clickableAreaRef }) => {
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const compose = useAppSelector((state) => state.compose.get(id)!);
|
||||
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
||||
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
|
||||
const maxTootChars = useAppSelector((state) => state.instance.getIn(['configuration', 'statuses', 'max_characters'])) as number;
|
||||
const scheduledStatusCount = useAppSelector((state) => state.get('scheduled_statuses').size);
|
||||
const features = useFeatures();
|
||||
|
||||
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt } = compose;
|
||||
|
||||
const hasPoll = !!compose.poll;
|
||||
const isEditing = compose.id !== null;
|
||||
const anyMedia = compose.media_attachments.size > 0;
|
||||
|
||||
const [composeFocused, setComposeFocused] = useState(false);
|
||||
|
||||
const formRef = useRef(null);
|
||||
const spoilerTextRef = useRef<AutosuggestInput>(null);
|
||||
const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null);
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
dispatch(changeCompose(id, e.target.value));
|
||||
};
|
||||
|
||||
const handleKeyDown: React.KeyboardEventHandler = (e) => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
handleSubmit();
|
||||
e.preventDefault(); // Prevent bubbling to other ComposeForm instances
|
||||
}
|
||||
};
|
||||
|
||||
const getClickableArea = () => {
|
||||
return clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
||||
};
|
||||
|
||||
const isEmpty = () => {
|
||||
return !(text || spoilerText || anyMedia);
|
||||
};
|
||||
|
||||
const isClickOutside = (e: MouseEvent | React.MouseEvent) => {
|
||||
return ![
|
||||
// List of elements that shouldn't collapse the composer when clicked
|
||||
// FIXME: Make this less brittle
|
||||
getClickableArea(),
|
||||
document.querySelector('.privacy-dropdown__dropdown'),
|
||||
document.querySelector('.emoji-picker-dropdown__menu'),
|
||||
document.getElementById('modal-overlay'),
|
||||
].some(element => element?.contains(e.target as any));
|
||||
};
|
||||
|
||||
const handleClick = useCallback((e: MouseEvent | React.MouseEvent) => {
|
||||
if (isEmpty() && isClickOutside(e)) {
|
||||
handleClickOutside();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClickOutside = () => {
|
||||
setComposeFocused(false);
|
||||
};
|
||||
|
||||
const handleComposeFocus = () => {
|
||||
setComposeFocused(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (text !== autosuggestTextareaRef.current?.textarea?.value) {
|
||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||
// Update the state to match the current text
|
||||
dispatch(changeCompose(id, autosuggestTextareaRef.current!.textarea!.value));
|
||||
}
|
||||
|
||||
// Submit disabled:
|
||||
const fulltext = [spoilerText, countableText(text)].join('');
|
||||
|
||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(submitCompose(id, history));
|
||||
};
|
||||
|
||||
const onSuggestionsClearRequested = () => {
|
||||
dispatch(clearComposeSuggestions());
|
||||
};
|
||||
|
||||
const onSuggestionsFetchRequested = (token: string | number) => {
|
||||
dispatch(fetchComposeSuggestions(token as string));
|
||||
};
|
||||
|
||||
const onSuggestionSelected = (tokenStart: number, token: string | null, value: string | undefined) => {
|
||||
if (value) dispatch(selectComposeSuggestion(tokenStart, token, value, ['text']));
|
||||
};
|
||||
|
||||
const onSpoilerSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
|
||||
dispatch(selectComposeSuggestion(tokenStart, token, value, ['spoiler_text']));
|
||||
};
|
||||
|
||||
const handleChangeSpoilerText: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
dispatch(changeComposeSpoilerText(id, e.target.value));
|
||||
};
|
||||
|
||||
const setCursor = (start: number, end: number = start) => {
|
||||
if (!autosuggestTextareaRef.current?.textarea) return;
|
||||
autosuggestTextareaRef.current.textarea.setSelectionRange(start, end);
|
||||
};
|
||||
|
||||
const handleEmojiPick = (data: Emoji) => {
|
||||
const position = autosuggestTextareaRef.current!.textarea!.selectionStart;
|
||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||
|
||||
dispatch(insertEmojiCompose(id, position, data, needsSpace));
|
||||
};
|
||||
|
||||
const onPaste = (files: FileList) => {
|
||||
dispatch(uploadCompose(id, files, intl));
|
||||
};
|
||||
|
||||
const focusSpoilerInput = () => {
|
||||
spoilerTextRef.current?.input?.focus();
|
||||
};
|
||||
|
||||
const focusTextarea = () => {
|
||||
autosuggestTextareaRef.current?.textarea?.focus();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const length = text.length;
|
||||
document.addEventListener('click', handleClick, true);
|
||||
|
||||
if (length > 0) {
|
||||
setCursor(length); // Set cursor at end
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
switch (spoiler) {
|
||||
case true: focusSpoilerInput(); break;
|
||||
case false: focusTextarea(); break;
|
||||
}
|
||||
}, [spoiler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof caretPosition === 'number') {
|
||||
setCursor(caretPosition);
|
||||
}
|
||||
}, [focusDate]);
|
||||
|
||||
const condensed = shouldCondense && !composeFocused && isEmpty() && !isUploading;
|
||||
const disabled = isSubmitting;
|
||||
const countedText = [spoilerText, countableText(text)].join('');
|
||||
const disabledButton = disabled || isUploading || isChangingUpload || length(countedText) > maxTootChars || (countedText.length !== 0 && countedText.trim().length === 0 && !anyMedia);
|
||||
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth);
|
||||
|
||||
let publishText: string | JSX.Element = '';
|
||||
|
||||
if (isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (privacy === 'direct') {
|
||||
publishText = (
|
||||
<>
|
||||
<Icon src={require('@tabler/icons/mail.svg')} />
|
||||
{intl.formatMessage(messages.message)}
|
||||
</>
|
||||
);
|
||||
} else if (privacy === 'private') {
|
||||
publishText = (
|
||||
<>
|
||||
<Icon src={require('@tabler/icons/lock.svg')} />
|
||||
{intl.formatMessage(messages.publish)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
||||
if (scheduledAt) {
|
||||
publishText = intl.formatMessage(messages.schedule);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className='w-full' space={1} ref={formRef} onClick={handleClick}>
|
||||
{scheduledStatusCount > 0 && (
|
||||
<Warning
|
||||
message={(
|
||||
<FormattedMessage
|
||||
id='compose_form.scheduled_statuses.message'
|
||||
defaultMessage='You have scheduled posts. {click_here} to see them.'
|
||||
values={{ click_here: (
|
||||
<Link to='/scheduled_statuses'>
|
||||
<FormattedMessage
|
||||
id='compose_form.scheduled_statuses.click_here'
|
||||
defaultMessage='Click here'
|
||||
/>
|
||||
</Link>
|
||||
) }}
|
||||
/>)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WarningContainer composeId={id} />
|
||||
|
||||
{!shouldCondense && <ReplyIndicatorContainer composeId={id} />}
|
||||
|
||||
{!shouldCondense && <ReplyMentions composeId={id} />}
|
||||
|
||||
<div
|
||||
className={classNames({
|
||||
'relative transition-height': true,
|
||||
'hidden': !spoiler,
|
||||
})}
|
||||
>
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={spoilerText}
|
||||
onChange={handleChangeSpoilerText}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={!spoiler}
|
||||
ref={spoilerTextRef}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
onSuggestionSelected={onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='border-none shadow-none px-0 py-2 text-base'
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutosuggestTextarea
|
||||
ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef}
|
||||
placeholder={intl.formatMessage(hasPoll ? messages.pollPlaceholder : messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={text}
|
||||
onChange={handleChange}
|
||||
suggestions={suggestions}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleComposeFocus}
|
||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
onSuggestionSelected={onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={shouldAutoFocus}
|
||||
condensed={condensed}
|
||||
id='compose-textarea'
|
||||
>
|
||||
{
|
||||
!condensed &&
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadForm composeId={id} />
|
||||
<PollForm composeId={id} />
|
||||
<ScheduleFormContainer composeId={id} />
|
||||
</div>
|
||||
}
|
||||
</AutosuggestTextarea>
|
||||
|
||||
<QuotedStatusContainer composeId={id} />
|
||||
|
||||
<div
|
||||
className={classNames('flex flex-wrap items-center justify-between', {
|
||||
'hidden': condensed,
|
||||
})}
|
||||
>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{features.media && <UploadButtonContainer composeId={id} />}
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
||||
{features.polls && <PollButton composeId={id} />}
|
||||
{features.privacyScopes && <PrivacyDropdown composeId={id} />}
|
||||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||
{features.spoilers && <SpoilerButton composeId={id} />}
|
||||
{features.richText && <MarkdownButton composeId={id} />}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-4 ml-auto'>
|
||||
{maxTootChars && (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<TextCharacterCounter max={maxTootChars} text={text} />
|
||||
<VisualCharacterCounter max={maxTootChars} text={text} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button theme='primary' text={publishText} onClick={handleSubmit} disabled={disabledButton} />
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComposeForm;
|
|
@ -1,402 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { length } from 'stringz';
|
||||
|
||||
import AutosuggestInput from 'soapbox/components/autosuggest_input';
|
||||
import AutosuggestTextarea from 'soapbox/components/autosuggest_textarea';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button, Stack } from 'soapbox/components/ui';
|
||||
import { isMobile } from 'soapbox/is_mobile';
|
||||
|
||||
import PollForm from '../components/polls/poll-form';
|
||||
import ReplyMentions from '../components/reply_mentions';
|
||||
import UploadForm from '../components/upload_form';
|
||||
import Warning from '../components/warning';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import MarkdownButtonContainer from '../containers/markdown_button_container';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import QuotedStatusContainer from '../containers/quoted_status_container';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import ScheduleButtonContainer from '../containers/schedule_button_container';
|
||||
import ScheduleFormContainer from '../containers/schedule_form_container';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import WarningContainer from '../containers/warning_container';
|
||||
import { countableText } from '../util/counter';
|
||||
|
||||
import TextCharacterCounter from './text_character_counter';
|
||||
import VisualCharacterCounter from './visual_character_counter';
|
||||
|
||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||
|
||||
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}!' },
|
||||
message: { id: 'compose_form.message', defaultMessage: 'Message' },
|
||||
schedule: { id: 'compose_form.schedule', defaultMessage: 'Schedule' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
});
|
||||
|
||||
export default @withRouter
|
||||
class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
state = {
|
||||
composeFocused: false,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: PropTypes.bool,
|
||||
privacy: PropTypes.string,
|
||||
spoilerText: PropTypes.string,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
caretPosition: PropTypes.number,
|
||||
hasPoll: PropTypes.bool,
|
||||
isSubmitting: PropTypes.bool,
|
||||
isChangingUpload: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onChangeSpoilerText: PropTypes.func.isRequired,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onPickEmoji: PropTypes.func.isRequired,
|
||||
showSearch: PropTypes.bool,
|
||||
anyMedia: PropTypes.bool,
|
||||
shouldCondense: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
group: ImmutablePropTypes.map,
|
||||
isModalOpen: PropTypes.bool,
|
||||
clickableAreaRef: PropTypes.object,
|
||||
scheduledAt: PropTypes.instanceOf(Date),
|
||||
features: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showSearch: false,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleComposeFocus = () => {
|
||||
this.setState({
|
||||
composeFocused: true,
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleSubmit();
|
||||
e.preventDefault(); // Prevent bubbling to other ComposeForm instances
|
||||
}
|
||||
}
|
||||
|
||||
getClickableArea = () => {
|
||||
const { clickableAreaRef } = this.props;
|
||||
return clickableAreaRef ? clickableAreaRef.current : this.form;
|
||||
}
|
||||
|
||||
isEmpty = () => {
|
||||
const { text, spoilerText, anyMedia } = this.props;
|
||||
return !(text || spoilerText || anyMedia);
|
||||
}
|
||||
|
||||
isClickOutside = (e) => {
|
||||
return ![
|
||||
// List of elements that shouldn't collapse the composer when clicked
|
||||
// FIXME: Make this less brittle
|
||||
this.getClickableArea(),
|
||||
document.querySelector('.privacy-dropdown__dropdown'),
|
||||
document.querySelector('.emoji-picker-dropdown__menu'),
|
||||
document.getElementById('modal-overlay'),
|
||||
].some(element => element?.contains(e.target));
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
if (this.isEmpty() && this.isClickOutside(e)) {
|
||||
this.handleClickOutside();
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.setState({
|
||||
composeFocused: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
|
||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||
// Update the state to match the current text
|
||||
this.props.onChange(this.autosuggestTextarea.textarea.value);
|
||||
}
|
||||
|
||||
// Submit disabled:
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia, maxTootChars } = this.props;
|
||||
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
|
||||
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(this.props.history ? this.props.history : null, this.props.group);
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
|
||||
}
|
||||
|
||||
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
|
||||
}
|
||||
|
||||
handleChangeSpoilerText = (e) => {
|
||||
this.props.onChangeSpoilerText(e.target.value);
|
||||
}
|
||||
|
||||
setCursor = (start, end = start) => {
|
||||
if (!this.autosuggestTextarea) return;
|
||||
this.autosuggestTextarea.textarea.setSelectionRange(start, end);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const length = this.props.text.length;
|
||||
document.addEventListener('click', this.handleClick, true);
|
||||
|
||||
if (length > 0) {
|
||||
this.setCursor(length); // Set cursor at end
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleClick, true);
|
||||
}
|
||||
|
||||
setAutosuggestTextarea = (c) => {
|
||||
this.autosuggestTextarea = c;
|
||||
}
|
||||
|
||||
setForm = (c) => {
|
||||
this.form = c;
|
||||
}
|
||||
|
||||
setSpoilerText = (c) => {
|
||||
this.spoilerText = c;
|
||||
}
|
||||
|
||||
handleEmojiPick = (data) => {
|
||||
const { text } = this.props;
|
||||
const position = this.autosuggestTextarea.textarea.selectionStart;
|
||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||
|
||||
this.props.onPickEmoji(position, data, needsSpace);
|
||||
}
|
||||
|
||||
focusSpoilerInput = () => {
|
||||
const spoilerInput = get(this, ['spoilerText', 'input']);
|
||||
if (spoilerInput) spoilerInput.focus();
|
||||
}
|
||||
|
||||
focusTextarea = () => {
|
||||
const textarea = get(this, ['autosuggestTextarea', 'textarea']);
|
||||
if (textarea) textarea.focus();
|
||||
}
|
||||
|
||||
maybeUpdateFocus = prevProps => {
|
||||
const spoilerUpdated = this.props.spoiler !== prevProps.spoiler;
|
||||
if (spoilerUpdated) {
|
||||
switch (this.props.spoiler) {
|
||||
case true: this.focusSpoilerInput(); break;
|
||||
case false: this.focusTextarea(); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maybeUpdateCursor = prevProps => {
|
||||
const shouldUpdate = [
|
||||
// Autosuggest has been updated and
|
||||
// the cursor position explicitly set
|
||||
this.props.focusDate !== prevProps.focusDate,
|
||||
typeof this.props.caretPosition === 'number',
|
||||
].every(Boolean);
|
||||
|
||||
if (shouldUpdate) {
|
||||
this.setCursor(this.props.caretPosition);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.maybeUpdateFocus(prevProps);
|
||||
this.maybeUpdateCursor(prevProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen, maxTootChars, scheduledStatusCount, features } = this.props;
|
||||
const condensed = shouldCondense && !this.state.composeFocused && this.isEmpty() && !this.props.isUploading;
|
||||
const disabled = this.props.isSubmitting;
|
||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > maxTootChars || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth);
|
||||
|
||||
let publishText = '';
|
||||
|
||||
if (this.props.isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (this.props.privacy === 'direct') {
|
||||
publishText = (
|
||||
<>
|
||||
<Icon src={require('@tabler/icons/mail.svg')} />
|
||||
{intl.formatMessage(messages.message)}
|
||||
</>
|
||||
);
|
||||
} else if (this.props.privacy === 'private') {
|
||||
publishText = (
|
||||
<>
|
||||
<Icon src={require('@tabler/icons/lock.svg')} />
|
||||
{intl.formatMessage(messages.publish)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
||||
if (this.props.scheduledAt) {
|
||||
publishText = intl.formatMessage(messages.schedule);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className='w-full' space={1} ref={this.setForm} onClick={this.handleClick}>
|
||||
{scheduledStatusCount > 0 && (
|
||||
<Warning
|
||||
message={(
|
||||
<FormattedMessage
|
||||
id='compose_form.scheduled_statuses.message'
|
||||
defaultMessage='You have scheduled posts. {click_here} to see them.'
|
||||
values={{ click_here: (
|
||||
<Link to='/scheduled_statuses'>
|
||||
<FormattedMessage
|
||||
id='compose_form.scheduled_statuses.click_here'
|
||||
defaultMessage='Click here'
|
||||
/>
|
||||
</Link>
|
||||
) }}
|
||||
/>)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WarningContainer />
|
||||
|
||||
{!shouldCondense && <ReplyIndicatorContainer />}
|
||||
|
||||
{!shouldCondense && <ReplyMentions />}
|
||||
|
||||
<div
|
||||
className={classNames({
|
||||
'relative transition-height': true,
|
||||
'hidden': !this.props.spoiler,
|
||||
})}
|
||||
>
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={!this.props.spoiler}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='border-none shadow-none px-0 py-2 text-base'
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutosuggestTextarea
|
||||
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(this.props.hasPoll ? messages.pollPlaceholder : messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onFocus={this.handleComposeFocus}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={shouldAutoFocus}
|
||||
condensed={condensed}
|
||||
id='compose-textarea'
|
||||
>
|
||||
{
|
||||
!condensed &&
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadForm />
|
||||
<PollForm />
|
||||
<ScheduleFormContainer />
|
||||
</div>
|
||||
}
|
||||
</AutosuggestTextarea>
|
||||
|
||||
<QuotedStatusContainer />
|
||||
|
||||
<div
|
||||
className={classNames('flex flex-wrap items-center justify-between', {
|
||||
'hidden': condensed,
|
||||
})}
|
||||
>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{features.media && <UploadButtonContainer />}
|
||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
{features.polls && <PollButtonContainer />}
|
||||
{features.privacyScopes && <PrivacyDropdownContainer />}
|
||||
{features.scheduledStatuses && <ScheduleButtonContainer />}
|
||||
{features.spoilers && <SpoilerButtonContainer />}
|
||||
{features.richText && <MarkdownButtonContainer />}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-4 ml-auto'>
|
||||
{maxTootChars && (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<TextCharacterCounter max={maxTootChars} text={text} />
|
||||
<VisualCharacterCounter max={maxTootChars} text={text} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button theme='primary' text={publishText} onClick={this.handleSubmit} disabled={disabledButton} />
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeComposeContentType } from 'soapbox/actions/compose';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import ComposeFormButton from './compose_form_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -9,12 +12,16 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IMarkdownButton {
|
||||
active?: boolean,
|
||||
onClick: () => void,
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const MarkdownButton: React.FC<IMarkdownButton> = ({ active, onClick }) => {
|
||||
const MarkdownButton: React.FC<IMarkdownButton> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const active = useAppSelector((state) => state.compose.get(composeId)!.content_type === 'text/markdown');
|
||||
|
||||
const onClick = () => dispatch(changeComposeContentType(composeId, active ? 'text/plain' : 'text/markdown'));
|
||||
|
||||
return (
|
||||
<ComposeFormButton
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { addPoll, removePoll } from 'soapbox/actions/compose';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import ComposeFormButton from './compose_form_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -9,14 +12,24 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IPollButton {
|
||||
composeId: string
|
||||
disabled?: boolean,
|
||||
unavailable?: boolean,
|
||||
active?: boolean,
|
||||
onClick: () => void,
|
||||
}
|
||||
|
||||
const PollButton: React.FC<IPollButton> = ({ active, unavailable, disabled, onClick }) => {
|
||||
const PollButton: React.FC<IPollButton> = ({ composeId, disabled }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const unavailable = useAppSelector((state) => state.compose.get(composeId)!.is_uploading);
|
||||
const active = useAppSelector((state) => state.compose.get(composeId)!.poll !== null);
|
||||
|
||||
const onClick = () => {
|
||||
if (active) {
|
||||
dispatch(removePoll(composeId));
|
||||
} else {
|
||||
dispatch(addPoll(composeId));
|
||||
}
|
||||
};
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
|
|
|
@ -26,6 +26,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IOption {
|
||||
composeId: string
|
||||
index: number
|
||||
maxChars: number
|
||||
numOptions: number
|
||||
|
@ -35,21 +36,20 @@ interface IOption {
|
|||
title: string
|
||||
}
|
||||
|
||||
const Option = (props: IOption) => {
|
||||
const {
|
||||
index,
|
||||
maxChars,
|
||||
numOptions,
|
||||
onChange,
|
||||
onRemove,
|
||||
onRemovePoll,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const Option: React.FC<IOption> = ({
|
||||
composeId,
|
||||
index,
|
||||
maxChars,
|
||||
numOptions,
|
||||
onChange,
|
||||
onRemove,
|
||||
onRemovePoll,
|
||||
title,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const suggestions = useAppSelector((state) => state.compose.suggestions);
|
||||
const suggestions = useAppSelector((state) => state.compose.get(composeId)!.suggestions);
|
||||
|
||||
const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value);
|
||||
|
||||
|
@ -102,26 +102,30 @@ const Option = (props: IOption) => {
|
|||
);
|
||||
};
|
||||
|
||||
const PollForm = () => {
|
||||
interface IPollForm {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const PollForm: React.FC<IPollForm> = ({ composeId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any);
|
||||
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 options = useAppSelector((state) => state.compose.get(composeId)!.poll?.options);
|
||||
const expiresIn = useAppSelector((state) => state.compose.get(composeId)!.poll?.expires_in);
|
||||
const isMultiple = useAppSelector((state) => state.compose.get(composeId)!.poll?.multiple);
|
||||
|
||||
const maxOptions = pollLimits.get('max_options');
|
||||
const maxOptionChars = pollLimits.get('max_characters_per_option');
|
||||
|
||||
const onRemoveOption = (index: number) => dispatch(removePollOption(index));
|
||||
const onChangeOption = (index: number, title: string) => dispatch(changePollOption(index, title));
|
||||
const handleAddOption = () => dispatch(addPollOption(''));
|
||||
const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index));
|
||||
const onChangeOption = (index: number, title: string) => dispatch(changePollOption(composeId, index, title));
|
||||
const handleAddOption = () => dispatch(addPollOption(composeId, ''));
|
||||
const onChangeSettings = (expiresIn: string | number | undefined, isMultiple?: boolean) =>
|
||||
dispatch(changePollSettings(expiresIn, isMultiple));
|
||||
dispatch(changePollSettings(composeId, expiresIn, isMultiple));
|
||||
const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple);
|
||||
const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple);
|
||||
const onRemovePoll = () => dispatch(removePoll());
|
||||
const onRemovePoll = () => dispatch(removePoll(composeId));
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
|
@ -132,6 +136,7 @@ const PollForm = () => {
|
|||
<Stack space={2}>
|
||||
{options.map((title: string, i: number) => (
|
||||
<Option
|
||||
composeId={composeId}
|
||||
title={title}
|
||||
key={i}
|
||||
index={i}
|
||||
|
|
|
@ -6,8 +6,12 @@ import { spring } from 'react-motion';
|
|||
// @ts-ignore
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
|
||||
import { changeComposeVisibility } from 'soapbox/actions/compose';
|
||||
import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { IconButton } from 'soapbox/components/ui';
|
||||
import { isUserTouching } from 'soapbox/is_mobile';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
||||
|
@ -50,7 +54,7 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
|
|||
const handleKeyDown: React.KeyboardEventHandler = e => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
const index = items.findIndex(item => item.value === value);
|
||||
let element = null;
|
||||
let element: ChildNode | null | undefined = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
|
@ -136,27 +140,20 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
|
|||
};
|
||||
|
||||
interface IPrivacyDropdown {
|
||||
isUserTouching: () => boolean,
|
||||
isModalOpen: boolean,
|
||||
onModalOpen: (opts: any) => void,
|
||||
onModalClose: () => void,
|
||||
value: string,
|
||||
onChange: (value: string | null) => void,
|
||||
unavailable: boolean,
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
||||
isUserTouching,
|
||||
onChange,
|
||||
onModalClose,
|
||||
onModalOpen,
|
||||
value,
|
||||
unavailable,
|
||||
composeId,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const activeElement = useRef<HTMLElement | null>(null);
|
||||
|
||||
const value = useAppSelector(state => state.compose.get(composeId)!.privacy);
|
||||
const unavailable = useAppSelector(state => !!state.compose.get(composeId)!.id);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [placement, setPlacement] = useState('bottom');
|
||||
|
||||
|
@ -167,6 +164,12 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
|||
{ icon: require('@tabler/icons/mail.svg'), value: 'direct', text: intl.formatMessage(messages.direct_short), meta: intl.formatMessage(messages.direct_long) },
|
||||
];
|
||||
|
||||
const onChange = (value: string | null) => value && dispatch(changeComposeVisibility(composeId, value));
|
||||
|
||||
const onModalOpen = (props: Record<string, any>) => dispatch(openModal('ACTIONS', props));
|
||||
|
||||
const onModalClose = () => dispatch(closeModal('ACTIONS'));
|
||||
|
||||
const handleToggle: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
if (isUserTouching()) {
|
||||
if (open) {
|
||||
|
|
|
@ -10,12 +10,16 @@ import { getFeatures } from 'soapbox/utils/features';
|
|||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const ReplyMentions: React.FC = () => {
|
||||
interface IReplyMentions {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const instance = useAppSelector((state) => state.instance);
|
||||
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.in_reply_to! }));
|
||||
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get(composeId)!.in_reply_to! }));
|
||||
|
||||
const to = useAppSelector((state) => state.compose.to);
|
||||
const to = useAppSelector((state) => state.compose.get(composeId)!.to);
|
||||
const account = useAppSelector((state) => state.accounts.get(state.me));
|
||||
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { addSchedule, removeSchedule } from 'soapbox/actions/compose';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import ComposeFormButton from './compose_form_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -9,17 +12,23 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IScheduleButton {
|
||||
disabled: boolean,
|
||||
active: boolean,
|
||||
unavailable: boolean,
|
||||
onClick: () => void,
|
||||
composeId: string,
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
const ScheduleButton: React.FC<IScheduleButton> = ({ active, unavailable, disabled, onClick }) => {
|
||||
const ScheduleButton: React.FC<IScheduleButton> = ({ composeId, disabled }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const active = useAppSelector((state) => !!state.compose.get(composeId)!.schedule);
|
||||
const unavailable = useAppSelector((state) => !!state.compose.get(composeId)!.id);
|
||||
|
||||
const handleClick = () => {
|
||||
onClick();
|
||||
if (active) {
|
||||
dispatch(removeSchedule(composeId));
|
||||
} else {
|
||||
dispatch(addSchedule(composeId));
|
||||
}
|
||||
};
|
||||
|
||||
if (unavailable) {
|
||||
|
|
|
@ -27,19 +27,23 @@ const messages = defineMessages({
|
|||
remove: { id: 'schedule.remove', defaultMessage: 'Remove schedule' },
|
||||
});
|
||||
|
||||
const ScheduleForm: React.FC = () => {
|
||||
export interface IScheduleForm {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const ScheduleForm: React.FC<IScheduleForm> = ({ composeId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const scheduledAt = useAppSelector((state) => state.compose.schedule);
|
||||
const scheduledAt = useAppSelector((state) => state.compose.get(composeId)!.schedule);
|
||||
const active = !!scheduledAt;
|
||||
|
||||
const onSchedule = (date: Date) => {
|
||||
dispatch(setSchedule(date));
|
||||
dispatch(setSchedule(composeId, date));
|
||||
};
|
||||
|
||||
const handleRemove = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
dispatch(removeSchedule());
|
||||
dispatch(removeSchedule(composeId));
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
|
|
|
@ -10,16 +10,20 @@ const messages = defineMessages({
|
|||
unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' },
|
||||
});
|
||||
|
||||
interface ISensitiveButton {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
/** Button to mark own media as sensitive. */
|
||||
const SensitiveButton: React.FC = () => {
|
||||
const SensitiveButton: React.FC<ISensitiveButton> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const active = useAppSelector(state => state.compose.sensitive === true);
|
||||
const disabled = useAppSelector(state => state.compose.spoiler === true);
|
||||
const active = useAppSelector(state => state.compose.get(composeId)!.sensitive === true);
|
||||
const disabled = useAppSelector(state => state.compose.get(composeId)!.spoiler === true);
|
||||
|
||||
const onClick = () => {
|
||||
dispatch(changeComposeSensitivity());
|
||||
dispatch(changeComposeSensitivity(composeId));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeComposeSpoilerness } from 'soapbox/actions/compose';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import ComposeFormButton from './compose_form_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -9,12 +12,17 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface ISpoilerButton {
|
||||
active?: boolean,
|
||||
onClick: () => void,
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const SpoilerButton: React.FC<ISpoilerButton> = ({ active, onClick }) => {
|
||||
const SpoilerButton: React.FC<ISpoilerButton> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const active = useAppSelector((state) => state.compose.get(composeId)!.spoiler);
|
||||
|
||||
const onClick = () =>
|
||||
dispatch(changeComposeSpoilerness(composeId));
|
||||
|
||||
return (
|
||||
<ComposeFormButton
|
||||
|
|
|
@ -3,10 +3,14 @@ import React from 'react';
|
|||
import UploadProgress from 'soapbox/components/upload-progress';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
interface IComposeUploadProgress {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
/** File upload progress bar for post composer. */
|
||||
const ComposeUploadProgress = () => {
|
||||
const active = useAppSelector((state) => state.compose.is_uploading);
|
||||
const progress = useAppSelector((state) => state.compose.progress);
|
||||
const ComposeUploadProgress: React.FC<IComposeUploadProgress> = ({ composeId }) => {
|
||||
const active = useAppSelector((state) => state.compose.get(composeId)!.is_uploading);
|
||||
const progress = useAppSelector((state) => state.compose.get(composeId)!.progress);
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { defineMessages, IntlShape, useIntl } from 'react-intl';
|
||||
|
||||
import { IconButton } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
@ -14,12 +14,12 @@ const onlyImages = (types: ImmutableList<string>) => {
|
|||
return Boolean(types && types.every(type => type.startsWith('image/')));
|
||||
};
|
||||
|
||||
interface IUploadButton {
|
||||
export interface IUploadButton {
|
||||
disabled?: boolean,
|
||||
unavailable?: boolean,
|
||||
onSelectFile: (files: FileList) => void,
|
||||
onSelectFile: (files: FileList, intl: IntlShape) => void,
|
||||
style?: React.CSSProperties,
|
||||
resetFileKey: number,
|
||||
resetFileKey: number | null,
|
||||
}
|
||||
|
||||
const UploadButton: React.FC<IUploadButton> = ({
|
||||
|
@ -35,7 +35,7 @@ const UploadButton: React.FC<IUploadButton> = ({
|
|||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.target.files?.length) {
|
||||
onSelectFile(e.target.files);
|
||||
onSelectFile(e.target.files, intl);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -9,15 +9,19 @@ import UploadContainer from '../containers/upload_container';
|
|||
|
||||
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities';
|
||||
|
||||
const UploadForm = () => {
|
||||
const mediaIds = useAppSelector((state) => state.compose.media_attachments.map((item: AttachmentEntity) => item.id));
|
||||
interface IUploadForm {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const UploadForm: React.FC<IUploadForm> = ({ composeId }) => {
|
||||
const mediaIds = useAppSelector((state) => state.compose.get(composeId)!.media_attachments.map((item: AttachmentEntity) => item.id));
|
||||
const classes = classNames('compose-form__uploads-wrapper', {
|
||||
'contains-media': mediaIds.size !== 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-wrapper'>
|
||||
<UploadProgress />
|
||||
<UploadProgress composeId={composeId} />
|
||||
|
||||
<div className={classes}>
|
||||
{mediaIds.map((id: string) => (
|
||||
|
@ -25,7 +29,7 @@ const UploadForm = () => {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{!mediaIds.isEmpty() && <SensitiveButton />}
|
||||
{!mediaIds.isEmpty() && <SensitiveButton composeId={composeId} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
import { injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
changeComposeSpoilerText,
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
} from 'soapbox/actions/compose';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import ComposeForm from '../components/compose_form';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoilerText: state.getIn(['compose', 'spoiler_text']),
|
||||
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']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
isModalOpen: Boolean(state.get('modals').size && state.get('modals').last().modalType === 'COMPOSE'),
|
||||
maxTootChars: state.getIn(['instance', 'configuration', 'statuses', 'max_characters']),
|
||||
scheduledAt: state.getIn(['compose', 'schedule']),
|
||||
scheduledStatusCount: state.get('scheduled_statuses').size,
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onChange(text) {
|
||||
dispatch(changeCompose(text));
|
||||
},
|
||||
|
||||
onSubmit(router, group) {
|
||||
dispatch(submitCompose(router, group));
|
||||
},
|
||||
|
||||
onClearSuggestions() {
|
||||
dispatch(clearComposeSuggestions());
|
||||
},
|
||||
|
||||
onFetchSuggestions(token) {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected(position, token, suggestion, path) {
|
||||
dispatch(selectComposeSuggestion(position, token, suggestion, path));
|
||||
},
|
||||
|
||||
onChangeSpoilerText(checked) {
|
||||
dispatch(changeComposeSpoilerText(checked));
|
||||
},
|
||||
|
||||
onPaste(files) {
|
||||
dispatch(uploadCompose(files, intl));
|
||||
},
|
||||
|
||||
onPickEmoji(position, data, needsSpace) {
|
||||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||
return Object.assign({}, ownProps, {
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
});
|
||||
}
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps, mergeProps)(ComposeForm));
|
|
@ -1,23 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeComposeContentType } from '../../../actions/compose';
|
||||
import MarkdownButton from '../components/markdown_button';
|
||||
|
||||
const mapStateToProps = (state, { intl }) => {
|
||||
return {
|
||||
active: state.getIn(['compose', 'content_type']) === 'text/markdown',
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick() {
|
||||
dispatch((_, getState) => {
|
||||
const active = getState().getIn(['compose', 'content_type']) === 'text/markdown';
|
||||
dispatch(changeComposeContentType(active ? 'text/plain' : 'text/markdown'));
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MarkdownButton);
|
|
@ -1,25 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { addPoll, removePoll } from '../../../actions/compose';
|
||||
import PollButton from '../components/poll_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
unavailable: state.getIn(['compose', 'is_uploading']),
|
||||
active: state.getIn(['compose', 'poll']) !== null,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick() {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['compose', 'poll'])) {
|
||||
dispatch(removePoll());
|
||||
} else {
|
||||
dispatch(addPoll());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollButton);
|
|
@ -1,28 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeComposeVisibility } from '../../../actions/compose';
|
||||
import { openModal, closeModal } from '../../../actions/modals';
|
||||
import { isUserTouching } from '../../../is_mobile';
|
||||
import PrivacyDropdown from '../components/privacy_dropdown';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isModalOpen: Boolean(state.get('modals').size && state.get('modals').last().modalType === 'ACTIONS'),
|
||||
value: state.getIn(['compose', 'privacy']),
|
||||
unavailable: !!state.getIn(['compose', 'id']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange(value) {
|
||||
dispatch(changeComposeVisibility(value));
|
||||
},
|
||||
|
||||
isUserTouching,
|
||||
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
|
||||
onModalClose: () => {
|
||||
dispatch(closeModal('ACTIONS'));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
|
|
@ -7,10 +7,14 @@ import { makeGetStatus } from 'soapbox/selectors';
|
|||
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
interface IQuotedStatusContainer {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
/** QuotedStatus shown in post composer. */
|
||||
const QuotedStatusContainer: React.FC = () => {
|
||||
const QuotedStatusContainer: React.FC<IQuotedStatusContainer> = ({ composeId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const status = useAppSelector(state => getStatus(state, { id: state.compose.quote! }));
|
||||
const status = useAppSelector(state => getStatus(state, { id: state.compose.get(composeId)!.quote! }));
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(cancelQuoteCompose());
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { cancelReplyCompose } from '../../../actions/compose';
|
||||
import { makeGetStatus } from '../../../selectors';
|
||||
import ReplyIndicator from '../components/reply_indicator';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const statusId = state.getIn(['compose', 'in_reply_to']);
|
||||
const editing = !!state.getIn(['compose', 'id']);
|
||||
|
||||
return {
|
||||
status: getStatus(state, { id: statusId }),
|
||||
hideActions: editing,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onCancel() {
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
|
|
@ -0,0 +1,35 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { cancelReplyCompose } from 'soapbox/actions/compose';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
import ReplyIndicator from '../components/reply_indicator';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = (state: RootState, { composeId }: { composeId: string }) => {
|
||||
const statusId = state.compose.get(composeId)!.in_reply_to!;
|
||||
const editing = !!state.compose.get(composeId)!.id;
|
||||
|
||||
return {
|
||||
status: getStatus(state, { id: statusId }) as Status,
|
||||
hideActions: editing,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||
|
||||
onCancel() {
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
|
|
@ -1,25 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { addSchedule, removeSchedule } from '../../../actions/compose';
|
||||
import ScheduleButton from '../components/schedule_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'schedule']) ? true : false,
|
||||
unavailable: !!state.getIn(['compose', 'id']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick() {
|
||||
dispatch((dispatch, getState) => {
|
||||
if (getState().getIn(['compose', 'schedule'])) {
|
||||
dispatch(removeSchedule());
|
||||
} else {
|
||||
dispatch(addSchedule());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleButton);
|
|
@ -1,16 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { ScheduleForm } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
export default class ScheduleFormContainer extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<BundleContainer fetchComponent={ScheduleForm}>
|
||||
{Component => <Component {...this.props} />}
|
||||
</BundleContainer>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { ScheduleForm } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import type { IScheduleForm } from '../components/schedule_form';
|
||||
|
||||
const ScheduleFormContainer: React.FC<IScheduleForm> = (props) => (
|
||||
<BundleContainer fetchComponent={ScheduleForm}>
|
||||
{Component => <Component {...props} />}
|
||||
</BundleContainer>
|
||||
);
|
||||
|
||||
export default ScheduleFormContainer;
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||
import SpoilerButton from '../components/spoiler_button';
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
active: state.getIn(['compose', 'spoiler']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick() {
|
||||
dispatch(changeComposeSpoilerness());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SpoilerButton);
|
|
@ -1,20 +0,0 @@
|
|||
import { injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import UploadButton from '../components/upload_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'is_uploading']),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onSelectFile(files) {
|
||||
dispatch(uploadCompose(files, intl));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(UploadButton));
|
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { uploadCompose } from 'soapbox/actions/compose';
|
||||
|
||||
import UploadButton from '../components/upload_button';
|
||||
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
const mapStateToProps = (state: RootState, { composeId }: { composeId: string }) => ({
|
||||
disabled: state.compose.get(composeId)!.is_uploading,
|
||||
resetFileKey: state.compose.get(composeId)!.resetFileKey,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch, { composeId }: { composeId: string }) => ({
|
||||
|
||||
onSelectFile(files: FileList, intl: IntlShape) {
|
||||
dispatch(uploadCompose(composeId, files, intl));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadButton);
|
|
@ -1,23 +1,24 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Warning from '../components/warning';
|
||||
|
||||
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
return {
|
||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
||||
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
|
||||
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
|
||||
};
|
||||
};
|
||||
interface IWarningWrapper {
|
||||
composeId: string,
|
||||
}
|
||||
|
||||
const WarningWrapper: React.FC<IWarningWrapper> = ({ composeId }) => {
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const needsLockWarning = useAppSelector(state => state.compose.get(composeId)!.privacy === 'private' && !state.accounts.get(me)!.locked);
|
||||
const hashtagWarning = useAppSelector(state => state.compose.get(composeId)!.privacy !== 'public' && APPROX_HASHTAG_RE.test(state.compose.get(composeId)!.text));
|
||||
const directMessageWarning = useAppSelector(state => state.compose.get(composeId)!.privacy === 'direct');
|
||||
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <Link to='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></Link> }} />} />;
|
||||
}
|
||||
|
@ -40,10 +41,4 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
|
|||
return null;
|
||||
};
|
||||
|
||||
WarningWrapper.propTypes = {
|
||||
needsLockWarning: PropTypes.bool,
|
||||
hashtagWarning: PropTypes.bool,
|
||||
directMessageWarning: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WarningWrapper);
|
||||
export default WarningWrapper;
|
|
@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
|
||||
import ComposeFormContainer from '../../../../soapbox/features/compose/containers/compose_form_container';
|
||||
import ComposeForm from '../../../../soapbox/features/compose/components/compose-form';
|
||||
import { connectGroupStream } from '../../../actions/streaming';
|
||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||
import Avatar from '../../../components/avatar';
|
||||
|
@ -85,7 +85,7 @@ class GroupTimeline extends React.PureComponent {
|
|||
<Link className='timeline-compose-block__avatar' to={`/@${acct}`}>
|
||||
<Avatar account={account} size={46} />
|
||||
</Link>
|
||||
<ComposeFormContainer group={group} shouldCondense autoFocus={false} />
|
||||
<ComposeForm group={group} shouldCondense autoFocus={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
@ -17,16 +17,17 @@ const messages = defineMessages({
|
|||
const getAccount = makeGetAccount();
|
||||
|
||||
interface IAccount {
|
||||
composeId: string,
|
||||
accountId: string,
|
||||
author: boolean,
|
||||
}
|
||||
|
||||
const Account: React.FC<IAccount> = ({ accountId, author }) => {
|
||||
const Account: React.FC<IAccount> = ({ composeId, accountId, author }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||
const added = useAppSelector((state) => !!account && state.compose.to?.includes(account.acct));
|
||||
const added = useAppSelector((state) => !!account && state.compose.get(composeId)!.to?.includes(account.acct));
|
||||
|
||||
const onRemove = () => dispatch(removeFromMentions(accountId));
|
||||
const onAdd = () => dispatch(addToMentions(accountId));
|
||||
|
|
|
@ -135,7 +135,6 @@ const Thread: React.FC<IThread> = (props) => {
|
|||
const me = useAppSelector(state => state.me);
|
||||
const status = useAppSelector(state => getStatus(state, { id: props.params.statusId }));
|
||||
const displayMedia = settings.get('displayMedia') as DisplayMedia;
|
||||
const askReplyConfirmation = useAppSelector(state => state.compose.text.trim().length !== 0);
|
||||
|
||||
const { ancestorsIds, descendantsIds } = useAppSelector(state => {
|
||||
let ancestorsIds = ImmutableOrderedSet<string>();
|
||||
|
@ -201,15 +200,7 @@ const Thread: React.FC<IThread> = (props) => {
|
|||
};
|
||||
|
||||
const handleReplyClick = (status: StatusEntity) => {
|
||||
if (askReplyConfirmation) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(status)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(status));
|
||||
}
|
||||
dispatch(replyCompose(status));
|
||||
};
|
||||
|
||||
const handleModalReblog = (status: StatusEntity) => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { checkComposeContent } from 'soapbox/components/modal_root';
|
|||
import { Modal } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import ComposeFormContainer from '../../compose/containers/compose_form_container';
|
||||
import ComposeForm from '../../compose/components/compose-form';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
|
@ -23,11 +23,11 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose }) => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const statusId = useAppSelector((state) => state.compose.id);
|
||||
const hasComposeContent = useAppSelector((state) => checkComposeContent(state.compose));
|
||||
const privacy = useAppSelector((state) => state.compose.privacy);
|
||||
const inReplyTo = useAppSelector((state) => state.compose.in_reply_to);
|
||||
const quote = useAppSelector((state) => state.compose.quote);
|
||||
const compose = useAppSelector((state) => state.compose.get('compose-modal'));
|
||||
|
||||
const { id: statusId, privacy, in_reply_to: inReplyTo, quote } = compose!;
|
||||
|
||||
const hasComposeContent = checkComposeContent(compose);
|
||||
|
||||
const onClickClose = () => {
|
||||
if (hasComposeContent) {
|
||||
|
@ -69,7 +69,7 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose }) => {
|
|||
title={renderTitle()}
|
||||
onClose={onClickClose}
|
||||
>
|
||||
<ComposeFormContainer />
|
||||
<ComposeForm id='compose-modal' />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,11 +11,12 @@ import Account from '../../reply_mentions/account';
|
|||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IReplyMentionsModal {
|
||||
composeId: string,
|
||||
onClose: (string: string) => void,
|
||||
}
|
||||
|
||||
const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
|
||||
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.in_reply_to! }));
|
||||
const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ composeId, onClose }) => {
|
||||
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get(composeId)?.in_reply_to! }));
|
||||
const account = useAppSelector((state) => state.accounts.get(state.me));
|
||||
|
||||
const mentions = statusToMentionsAccountIdsArray(status!, account!);
|
||||
|
@ -33,7 +34,7 @@ const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
|
|||
closePosition='left'
|
||||
>
|
||||
<div className='reply-mentions-modal__accounts'>
|
||||
{mentions.map(accountId => <Account key={accountId} accountId={accountId} author={author === accountId} />)}
|
||||
{mentions.map(accountId => <Account composeId={composeId} key={accountId} accountId={accountId} author={author === accountId} />)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -387,7 +387,7 @@ const UI: React.FC = ({ children }) => {
|
|||
dragTargets.current = [];
|
||||
|
||||
if (e.dataTransfer && e.dataTransfer.files.length >= 1) {
|
||||
dispatch(uploadCompose(e.dataTransfer.files, intl));
|
||||
dispatch(uploadCompose('home', e.dataTransfer.files, intl));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import { useAppSelector, useOwnAccount, useFeatures, useSoapboxConfig } from 'so
|
|||
|
||||
import Avatar from '../components/avatar';
|
||||
import { Card, CardBody, Layout } from '../components/ui';
|
||||
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
|
||||
import ComposeForm from '../features/compose/components/compose-form';
|
||||
import BundleContainer from '../features/ui/containers/bundle_container';
|
||||
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||
|
||||
|
@ -47,8 +47,8 @@ const HomePage: React.FC = ({ children }) => {
|
|||
<Avatar account={account} size={46} />
|
||||
</Link>
|
||||
|
||||
<ComposeFormContainer
|
||||
// @ts-ignore
|
||||
<ComposeForm
|
||||
id='home'
|
||||
shouldCondense
|
||||
autoFocus={false}
|
||||
clickableAreaRef={composeBlock}
|
||||
|
|
|
@ -1,499 +1,499 @@
|
|||
import { List as ImmutableList, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
// import { List as ImmutableList, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
import * as actions from 'soapbox/actions/compose';
|
||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
|
||||
import { SETTING_CHANGE } from 'soapbox/actions/settings';
|
||||
import { TIMELINE_DELETE } from 'soapbox/actions/timelines';
|
||||
import { TagRecord } from 'soapbox/normalizers';
|
||||
import { normalizeStatus } from 'soapbox/normalizers/status';
|
||||
// import * as actions from 'soapbox/actions/compose';
|
||||
// import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
|
||||
// import { SETTING_CHANGE } from 'soapbox/actions/settings';
|
||||
// import { TIMELINE_DELETE } from 'soapbox/actions/timelines';
|
||||
// import { TagRecord } from 'soapbox/normalizers';
|
||||
// import { normalizeStatus } from 'soapbox/normalizers/status';
|
||||
|
||||
import reducer, { ReducerRecord } from '../compose';
|
||||
// import reducer, { ReducerRecord } from '../compose';
|
||||
|
||||
describe('compose reducer', () => {
|
||||
it('returns the initial state by default', () => {
|
||||
const state = reducer(undefined, {} as any);
|
||||
expect(state.toJS()).toMatchObject({
|
||||
mounted: 0,
|
||||
sensitive: false,
|
||||
spoiler: false,
|
||||
spoiler_text: '',
|
||||
privacy: 'public',
|
||||
text: '',
|
||||
focusDate: null,
|
||||
caretPosition: null,
|
||||
in_reply_to: null,
|
||||
is_composing: false,
|
||||
is_submitting: false,
|
||||
is_changing_upload: false,
|
||||
is_uploading: false,
|
||||
progress: 0,
|
||||
media_attachments: [],
|
||||
poll: null,
|
||||
suggestion_token: null,
|
||||
suggestions: [],
|
||||
default_privacy: 'public',
|
||||
default_sensitive: false,
|
||||
tagHistory: [],
|
||||
content_type: 'text/plain',
|
||||
});
|
||||
expect(state.get('idempotencyKey').length === 36);
|
||||
});
|
||||
// describe('compose reducer', () => {
|
||||
// it('returns the initial state by default', () => {
|
||||
// const state = reducer(undefined, {} as any);
|
||||
// expect(state.toJS()).toMatchObject({
|
||||
// mounted: 0,
|
||||
// sensitive: false,
|
||||
// spoiler: false,
|
||||
// spoiler_text: '',
|
||||
// privacy: 'public',
|
||||
// text: '',
|
||||
// focusDate: null,
|
||||
// caretPosition: null,
|
||||
// in_reply_to: null,
|
||||
// is_composing: false,
|
||||
// is_submitting: false,
|
||||
// is_changing_upload: false,
|
||||
// is_uploading: false,
|
||||
// progress: 0,
|
||||
// media_attachments: [],
|
||||
// poll: null,
|
||||
// suggestion_token: null,
|
||||
// suggestions: [],
|
||||
// default_privacy: 'public',
|
||||
// default_sensitive: false,
|
||||
// tagHistory: [],
|
||||
// content_type: 'text/plain',
|
||||
// });
|
||||
// expect(state.get('idempotencyKey').length === 36);
|
||||
// });
|
||||
|
||||
describe('COMPOSE_SET_STATUS', () => {
|
||||
it('strips Pleroma integer attachments', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_SET_STATUS,
|
||||
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
v: { software: 'Pleroma' },
|
||||
withRedraft: true,
|
||||
};
|
||||
// describe('COMPOSE_SET_STATUS', () => {
|
||||
// it('strips Pleroma integer attachments', () => {
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SET_STATUS,
|
||||
// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
// v: { software: 'Pleroma' },
|
||||
// withRedraft: true,
|
||||
// };
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('media_attachments').isEmpty()).toBe(true);
|
||||
});
|
||||
// const result = reducer(undefined, action);
|
||||
// expect(result.get('media_attachments').isEmpty()).toBe(true);
|
||||
// });
|
||||
|
||||
it('leaves non-Pleroma integer attachments alone', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_SET_STATUS,
|
||||
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
};
|
||||
// it('leaves non-Pleroma integer attachments alone', () => {
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SET_STATUS,
|
||||
// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
// };
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.getIn(['media_attachments', 0, 'id'])).toEqual('508107650');
|
||||
});
|
||||
// const result = reducer(undefined, action);
|
||||
// expect(result.getIn(['media_attachments', 0, 'id'])).toEqual('508107650');
|
||||
// });
|
||||
|
||||
it('sets the id when editing a post', () => {
|
||||
const action = {
|
||||
withRedraft: false,
|
||||
type: actions.COMPOSE_SET_STATUS,
|
||||
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
};
|
||||
// it('sets the id when editing a post', () => {
|
||||
// const action = {
|
||||
// withRedraft: false,
|
||||
// type: actions.COMPOSE_SET_STATUS,
|
||||
// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
// };
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('id')).toEqual('AHU2RrX0wdcwzCYjFQ');
|
||||
});
|
||||
// const result = reducer(undefined, action);
|
||||
// expect(result.get('id')).toEqual('AHU2RrX0wdcwzCYjFQ');
|
||||
// });
|
||||
|
||||
it('does not set the id when redrafting a post', () => {
|
||||
const action = {
|
||||
withRedraft: true,
|
||||
type: actions.COMPOSE_SET_STATUS,
|
||||
status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
};
|
||||
// it('does not set the id when redrafting a post', () => {
|
||||
// const action = {
|
||||
// withRedraft: true,
|
||||
// type: actions.COMPOSE_SET_STATUS,
|
||||
// status: normalizeStatus(fromJS(require('soapbox/__fixtures__/pleroma-status-deleted.json'))),
|
||||
// };
|
||||
|
||||
const result = reducer(undefined, action);
|
||||
expect(result.get('id')).toEqual(null);
|
||||
});
|
||||
});
|
||||
// const result = reducer(undefined, action);
|
||||
// expect(result.get('id')).toEqual(null);
|
||||
// });
|
||||
// });
|
||||
|
||||
it('uses \'public\' scope as default', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_REPLY,
|
||||
status: ImmutableRecord({})(),
|
||||
account: ImmutableRecord({})(),
|
||||
};
|
||||
expect(reducer(undefined, action).toJS()).toMatchObject({ privacy: 'public' });
|
||||
});
|
||||
// it('uses \'public\' scope as default', () => {
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_REPLY,
|
||||
// status: ImmutableRecord({})(),
|
||||
// account: ImmutableRecord({})(),
|
||||
// };
|
||||
// expect(reducer(undefined, action).toJS()).toMatchObject({ privacy: 'public' });
|
||||
// });
|
||||
|
||||
it('uses \'direct\' scope when replying to a DM', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'public' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_REPLY,
|
||||
status: ImmutableRecord({ visibility: 'direct' })(),
|
||||
account: ImmutableRecord({})(),
|
||||
};
|
||||
expect(reducer(state as any, action).toJS()).toMatchObject({ privacy: 'direct' });
|
||||
});
|
||||
// it('uses \'direct\' scope when replying to a DM', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'public' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_REPLY,
|
||||
// status: ImmutableRecord({ visibility: 'direct' })(),
|
||||
// account: ImmutableRecord({})(),
|
||||
// };
|
||||
// expect(reducer(state as any, action).toJS()).toMatchObject({ privacy: 'direct' });
|
||||
// });
|
||||
|
||||
it('uses \'private\' scope when replying to a private post', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'public' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_REPLY,
|
||||
status: ImmutableRecord({ visibility: 'private' })(),
|
||||
account: ImmutableRecord({})(),
|
||||
};
|
||||
expect(reducer(state as any, action).toJS()).toMatchObject({ privacy: 'private' });
|
||||
});
|
||||
// it('uses \'private\' scope when replying to a private post', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'public' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_REPLY,
|
||||
// status: ImmutableRecord({ visibility: 'private' })(),
|
||||
// account: ImmutableRecord({})(),
|
||||
// };
|
||||
// expect(reducer(state as any, action).toJS()).toMatchObject({ privacy: 'private' });
|
||||
// });
|
||||
|
||||
it('uses \'unlisted\' scope when replying to an unlisted post', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'public' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_REPLY,
|
||||
status: ImmutableRecord({ visibility: 'unlisted' })(),
|
||||
account: ImmutableRecord({})(),
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' });
|
||||
});
|
||||
// it('uses \'unlisted\' scope when replying to an unlisted post', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'public' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_REPLY,
|
||||
// status: ImmutableRecord({ visibility: 'unlisted' })(),
|
||||
// account: ImmutableRecord({})(),
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' });
|
||||
// });
|
||||
|
||||
it('uses \'private\' scope when set as preference and replying to a public post', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'private' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_REPLY,
|
||||
status: ImmutableRecord({ visibility: 'public' })(),
|
||||
account: ImmutableRecord({})(),
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'private' });
|
||||
});
|
||||
// it('uses \'private\' scope when set as preference and replying to a public post', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'private' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_REPLY,
|
||||
// status: ImmutableRecord({ visibility: 'public' })(),
|
||||
// account: ImmutableRecord({})(),
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'private' });
|
||||
// });
|
||||
|
||||
it('uses \'unlisted\' scope when set as preference and replying to a public post', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'unlisted' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_REPLY,
|
||||
status: ImmutableRecord({ visibility: 'public' })(),
|
||||
account: ImmutableRecord({})(),
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' });
|
||||
});
|
||||
// it('uses \'unlisted\' scope when set as preference and replying to a public post', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'unlisted' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_REPLY,
|
||||
// status: ImmutableRecord({ visibility: 'public' })(),
|
||||
// account: ImmutableRecord({})(),
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' });
|
||||
// });
|
||||
|
||||
it('sets preferred scope on user login', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'public' });
|
||||
const action = {
|
||||
type: ME_FETCH_SUCCESS,
|
||||
me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } },
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
default_privacy: 'unlisted',
|
||||
privacy: 'unlisted',
|
||||
});
|
||||
});
|
||||
// it('sets preferred scope on user login', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'public' });
|
||||
// const action = {
|
||||
// type: ME_FETCH_SUCCESS,
|
||||
// me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } },
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// default_privacy: 'unlisted',
|
||||
// privacy: 'unlisted',
|
||||
// });
|
||||
// });
|
||||
|
||||
it('sets preferred scope on settings change', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'public' });
|
||||
const action = {
|
||||
type: SETTING_CHANGE,
|
||||
path: ['defaultPrivacy'],
|
||||
value: 'unlisted',
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
default_privacy: 'unlisted',
|
||||
privacy: 'unlisted',
|
||||
});
|
||||
});
|
||||
// it('sets preferred scope on settings change', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'public' });
|
||||
// const action = {
|
||||
// type: SETTING_CHANGE,
|
||||
// path: ['defaultPrivacy'],
|
||||
// value: 'unlisted',
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// default_privacy: 'unlisted',
|
||||
// privacy: 'unlisted',
|
||||
// });
|
||||
// });
|
||||
|
||||
it('sets default scope on settings save (but retains current scope)', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'public', privacy: 'public' });
|
||||
const action = {
|
||||
type: ME_PATCH_SUCCESS,
|
||||
me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } },
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
default_privacy: 'unlisted',
|
||||
privacy: 'public',
|
||||
});
|
||||
});
|
||||
// it('sets default scope on settings save (but retains current scope)', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'public', privacy: 'public' });
|
||||
// const action = {
|
||||
// type: ME_PATCH_SUCCESS,
|
||||
// me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } },
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// default_privacy: 'unlisted',
|
||||
// privacy: 'public',
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_MOUNT', () => {
|
||||
const state = ReducerRecord({ mounted: 1 });
|
||||
const action = {
|
||||
type: actions.COMPOSE_MOUNT,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
mounted: 2,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_MOUNT', () => {
|
||||
// const state = ReducerRecord({ mounted: 1 });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_MOUNT,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// mounted: 2,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_UNMOUNT', () => {
|
||||
const state = ReducerRecord({ mounted: 1 });
|
||||
const action = {
|
||||
type: actions.COMPOSE_UNMOUNT,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
mounted: 0,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_UNMOUNT', () => {
|
||||
// const state = ReducerRecord({ mounted: 1 });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_UNMOUNT,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// mounted: 0,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => {
|
||||
const state = ReducerRecord({ spoiler: true, sensitive: true, idempotencyKey: '' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
sensitive: true,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => {
|
||||
// const state = ReducerRecord({ spoiler: true, sensitive: true, idempotencyKey: '' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// sensitive: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => {
|
||||
const state = ReducerRecord({ spoiler: false, sensitive: true });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
sensitive: false,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => {
|
||||
// const state = ReducerRecord({ spoiler: false, sensitive: true });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// sensitive: false,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => {
|
||||
const state = ReducerRecord({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: ImmutableList() });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SPOILERNESS_CHANGE,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
spoiler: false,
|
||||
spoiler_text: '',
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => {
|
||||
// const state = ReducerRecord({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: ImmutableList() });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SPOILERNESS_CHANGE,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// spoiler: false,
|
||||
// spoiler_text: '',
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SPOILER_TEXT_CHANGE', () => {
|
||||
const state = ReducerRecord({ spoiler_text: 'prevtext' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
text: 'nexttext',
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
spoiler_text: 'nexttext',
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SPOILER_TEXT_CHANGE', () => {
|
||||
// const state = ReducerRecord({ spoiler_text: 'prevtext' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
// text: 'nexttext',
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// spoiler_text: 'nexttext',
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_VISIBILITY_CHANGE', () => {
|
||||
const state = ReducerRecord({ privacy: 'public' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_VISIBILITY_CHANGE,
|
||||
value: 'direct',
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
privacy: 'direct',
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_VISIBILITY_CHANGE', () => {
|
||||
// const state = ReducerRecord({ privacy: 'public' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_VISIBILITY_CHANGE,
|
||||
// value: 'direct',
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// privacy: 'direct',
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('COMPOSE_CHANGE', () => {
|
||||
it('should handle text changing', () => {
|
||||
const state = ReducerRecord({ text: 'prevtext' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_CHANGE,
|
||||
text: 'nexttext',
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
text: 'nexttext',
|
||||
});
|
||||
});
|
||||
});
|
||||
// describe('COMPOSE_CHANGE', () => {
|
||||
// it('should handle text changing', () => {
|
||||
// const state = ReducerRecord({ text: 'prevtext' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_CHANGE,
|
||||
// text: 'nexttext',
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// text: 'nexttext',
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_COMPOSING_CHANGE', () => {
|
||||
const state = ReducerRecord({ is_composing: true });
|
||||
const action = {
|
||||
type: actions.COMPOSE_COMPOSING_CHANGE,
|
||||
value: false,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_composing: false,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_COMPOSING_CHANGE', () => {
|
||||
// const state = ReducerRecord({ is_composing: true });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_COMPOSING_CHANGE,
|
||||
// value: false,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_composing: false,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SUBMIT_REQUEST', () => {
|
||||
const state = ReducerRecord({ is_submitting: false });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SUBMIT_REQUEST,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_submitting: true,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SUBMIT_REQUEST', () => {
|
||||
// const state = ReducerRecord({ is_submitting: false });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SUBMIT_REQUEST,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_submitting: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_UPLOAD_CHANGE_REQUEST', () => {
|
||||
const state = ReducerRecord({ is_changing_upload: false });
|
||||
const action = {
|
||||
type: actions.COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_changing_upload: true,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_UPLOAD_CHANGE_REQUEST', () => {
|
||||
// const state = ReducerRecord({ is_changing_upload: false });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_changing_upload: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SUBMIT_SUCCESS', () => {
|
||||
const state = ReducerRecord({ default_privacy: 'public', privacy: 'private' });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SUBMIT_SUCCESS,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
privacy: 'public',
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SUBMIT_SUCCESS', () => {
|
||||
// const state = ReducerRecord({ default_privacy: 'public', privacy: 'private' });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SUBMIT_SUCCESS,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// privacy: 'public',
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SUBMIT_FAIL', () => {
|
||||
const state = ReducerRecord({ is_submitting: true });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SUBMIT_FAIL,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_submitting: false,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SUBMIT_FAIL', () => {
|
||||
// const state = ReducerRecord({ is_submitting: true });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SUBMIT_FAIL,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_submitting: false,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_UPLOAD_CHANGE_FAIL', () => {
|
||||
const state = ReducerRecord({ is_changing_upload: true });
|
||||
const action = {
|
||||
type: actions.COMPOSE_UPLOAD_CHANGE_FAIL,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_changing_upload: false,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_UPLOAD_CHANGE_FAIL', () => {
|
||||
// const state = ReducerRecord({ is_changing_upload: true });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_UPLOAD_CHANGE_FAIL,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_changing_upload: false,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_UPLOAD_REQUEST', () => {
|
||||
const state = ReducerRecord({ is_uploading: false });
|
||||
const action = {
|
||||
type: actions.COMPOSE_UPLOAD_REQUEST,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_uploading: true,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_UPLOAD_REQUEST', () => {
|
||||
// const state = ReducerRecord({ is_uploading: false });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_UPLOAD_REQUEST,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_uploading: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_UPLOAD_SUCCESS', () => {
|
||||
const state = ReducerRecord({ media_attachments: ImmutableList() });
|
||||
const media = [
|
||||
{
|
||||
description: null,
|
||||
id: '1375732379',
|
||||
pleroma: {
|
||||
mime_type: 'image/jpeg',
|
||||
},
|
||||
preview_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
remote_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
text_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
type: 'image',
|
||||
url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
},
|
||||
];
|
||||
const action = {
|
||||
type: actions.COMPOSE_UPLOAD_SUCCESS,
|
||||
media: media,
|
||||
skipLoading: true,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_uploading: false,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_UPLOAD_SUCCESS', () => {
|
||||
// const state = ReducerRecord({ media_attachments: ImmutableList() });
|
||||
// const media = [
|
||||
// {
|
||||
// description: null,
|
||||
// id: '1375732379',
|
||||
// pleroma: {
|
||||
// mime_type: 'image/jpeg',
|
||||
// },
|
||||
// preview_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
// remote_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
// text_url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
// type: 'image',
|
||||
// url: 'https://media.gleasonator.com/media_attachments/files/000/853/856/original/7035d67937053e1d.jpg',
|
||||
// },
|
||||
// ];
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_UPLOAD_SUCCESS,
|
||||
// media: media,
|
||||
// skipLoading: true,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_uploading: false,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_UPLOAD_FAIL', () => {
|
||||
const state = ReducerRecord({ is_uploading: true });
|
||||
const action = {
|
||||
type: actions.COMPOSE_UPLOAD_FAIL,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
is_uploading: false,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_UPLOAD_FAIL', () => {
|
||||
// const state = ReducerRecord({ is_uploading: true });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_UPLOAD_FAIL,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// is_uploading: false,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_UPLOAD_PROGRESS', () => {
|
||||
const state = ReducerRecord({ progress: 0 });
|
||||
const action = {
|
||||
type: actions.COMPOSE_UPLOAD_PROGRESS,
|
||||
loaded: 10,
|
||||
total: 15,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
progress: 67,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_UPLOAD_PROGRESS', () => {
|
||||
// const state = ReducerRecord({ progress: 0 });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_UPLOAD_PROGRESS,
|
||||
// loaded: 10,
|
||||
// total: 15,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// progress: 67,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_SUGGESTIONS_CLEAR,
|
||||
suggestions: [],
|
||||
suggestion_token: 'aiekdns3',
|
||||
};
|
||||
expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||
suggestion_token: null,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SUGGESTIONS_CLEAR', () => {
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SUGGESTIONS_CLEAR,
|
||||
// suggestions: [],
|
||||
// suggestion_token: 'aiekdns3',
|
||||
// };
|
||||
// expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||
// suggestion_token: null,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => {
|
||||
const state = ReducerRecord({ tagHistory: ImmutableList([ 'hashtag' ]) });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||
token: 'aaadken3',
|
||||
currentTrends: ImmutableList([
|
||||
TagRecord({ name: 'hashtag' }),
|
||||
]),
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
suggestion_token: 'aaadken3',
|
||||
suggestions: [],
|
||||
tagHistory: [ 'hashtag' ],
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_SUGGESTION_TAGS_UPDATE', () => {
|
||||
// const state = ReducerRecord({ tagHistory: ImmutableList([ 'hashtag' ]) });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||
// token: 'aaadken3',
|
||||
// currentTrends: ImmutableList([
|
||||
// TagRecord({ name: 'hashtag' }),
|
||||
// ]),
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// suggestion_token: 'aaadken3',
|
||||
// suggestions: [],
|
||||
// tagHistory: [ 'hashtag' ],
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_TAG_HISTORY_UPDATE', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_TAG_HISTORY_UPDATE,
|
||||
tags: [ 'hashtag', 'hashtag2'],
|
||||
};
|
||||
expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||
tagHistory: [ 'hashtag', 'hashtag2' ],
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_TAG_HISTORY_UPDATE', () => {
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_TAG_HISTORY_UPDATE,
|
||||
// tags: [ 'hashtag', 'hashtag2'],
|
||||
// };
|
||||
// expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||
// tagHistory: [ 'hashtag', 'hashtag2' ],
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle TIMELINE_DELETE - delete status from timeline', () => {
|
||||
const state = ReducerRecord({ in_reply_to: '9wk6pmImMrZjgrK7iC' });
|
||||
const action = {
|
||||
type: TIMELINE_DELETE,
|
||||
id: '9wk6pmImMrZjgrK7iC',
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
in_reply_to: null,
|
||||
});
|
||||
});
|
||||
// it('should handle TIMELINE_DELETE - delete status from timeline', () => {
|
||||
// const state = ReducerRecord({ in_reply_to: '9wk6pmImMrZjgrK7iC' });
|
||||
// const action = {
|
||||
// type: TIMELINE_DELETE,
|
||||
// id: '9wk6pmImMrZjgrK7iC',
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// in_reply_to: null,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_POLL_ADD', () => {
|
||||
const state = ReducerRecord({ poll: null });
|
||||
const initialPoll = Object({
|
||||
options: [
|
||||
'',
|
||||
'',
|
||||
],
|
||||
expires_in: 86400,
|
||||
multiple: false,
|
||||
});
|
||||
const action = {
|
||||
type: actions.COMPOSE_POLL_ADD,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
poll: initialPoll,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_POLL_ADD', () => {
|
||||
// const state = ReducerRecord({ poll: null });
|
||||
// const initialPoll = Object({
|
||||
// options: [
|
||||
// '',
|
||||
// '',
|
||||
// ],
|
||||
// expires_in: 86400,
|
||||
// multiple: false,
|
||||
// });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_POLL_ADD,
|
||||
// };
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// poll: initialPoll,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_POLL_REMOVE', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_POLL_REMOVE,
|
||||
};
|
||||
expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||
poll: null,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_POLL_REMOVE', () => {
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_POLL_REMOVE,
|
||||
// };
|
||||
// expect(reducer(undefined, action).toJS()).toMatchObject({
|
||||
// poll: null,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should handle COMPOSE_POLL_OPTION_CHANGE', () => {
|
||||
const initialPoll = Object({
|
||||
options: [
|
||||
'option 1',
|
||||
'option 2',
|
||||
],
|
||||
expires_in: 86400,
|
||||
multiple: false,
|
||||
});
|
||||
const state = ReducerRecord({ poll: initialPoll });
|
||||
const action = {
|
||||
type: actions.COMPOSE_POLL_OPTION_CHANGE,
|
||||
index: 0,
|
||||
title: 'change option',
|
||||
};
|
||||
const updatedPoll = Object({
|
||||
options: [
|
||||
'change option',
|
||||
'option 2',
|
||||
],
|
||||
expires_in: 86400,
|
||||
multiple: false,
|
||||
});
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
poll: updatedPoll,
|
||||
});
|
||||
});
|
||||
// it('should handle COMPOSE_POLL_OPTION_CHANGE', () => {
|
||||
// const initialPoll = Object({
|
||||
// options: [
|
||||
// 'option 1',
|
||||
// 'option 2',
|
||||
// ],
|
||||
// expires_in: 86400,
|
||||
// multiple: false,
|
||||
// });
|
||||
// const state = ReducerRecord({ poll: initialPoll });
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_POLL_OPTION_CHANGE,
|
||||
// index: 0,
|
||||
// title: 'change option',
|
||||
// };
|
||||
// const updatedPoll = Object({
|
||||
// options: [
|
||||
// 'change option',
|
||||
// 'option 2',
|
||||
// ],
|
||||
// expires_in: 86400,
|
||||
// multiple: false,
|
||||
// });
|
||||
// expect(reducer(state, action).toJS()).toMatchObject({
|
||||
// poll: updatedPoll,
|
||||
// });
|
||||
// });
|
||||
|
||||
it('sets the post content-type', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_TYPE_CHANGE,
|
||||
value: 'text/plain',
|
||||
};
|
||||
expect(reducer(undefined, action).toJS()).toMatchObject({ content_type: 'text/plain' });
|
||||
});
|
||||
});
|
||||
// it('sets the post content-type', () => {
|
||||
// const action = {
|
||||
// type: actions.COMPOSE_TYPE_CHANGE,
|
||||
// value: 'text/plain',
|
||||
// };
|
||||
// expect(reducer(undefined, action).toJS()).toMatchObject({ content_type: 'text/plain' });
|
||||
// });
|
||||
// });
|
||||
|
|
|
@ -6,8 +6,6 @@ import { PLEROMA } from 'soapbox/utils/features';
|
|||
import { hasIntegerMediaIds } from 'soapbox/utils/status';
|
||||
|
||||
import {
|
||||
COMPOSE_MOUNT,
|
||||
COMPOSE_UNMOUNT,
|
||||
COMPOSE_CHANGE,
|
||||
COMPOSE_REPLY,
|
||||
COMPOSE_REPLY_CANCEL,
|
||||
|
@ -68,7 +66,7 @@ import type {
|
|||
Tag,
|
||||
} from 'soapbox/types/entities';
|
||||
|
||||
const getResetFileKey = () => Math.floor((Math.random() * 0x10000));
|
||||
// const getResetFileKey = () => Math.floor((Math.random() * 0x10000));
|
||||
|
||||
const PollRecord = ImmutableRecord({
|
||||
options: ImmutableList(['', '']),
|
||||
|
@ -76,7 +74,7 @@ const PollRecord = ImmutableRecord({
|
|||
multiple: false,
|
||||
});
|
||||
|
||||
export const ReducerRecord = ImmutableRecord({
|
||||
export const ReducerCompose = ImmutableRecord({
|
||||
caretPosition: null as number | null,
|
||||
content_type: 'text/plain',
|
||||
default_content_type: 'text/plain',
|
||||
|
@ -91,7 +89,6 @@ export const ReducerRecord = ImmutableRecord({
|
|||
is_submitting: false,
|
||||
is_uploading: false,
|
||||
media_attachments: ImmutableList<AttachmentEntity>(),
|
||||
mounted: 0,
|
||||
poll: null as Poll | null,
|
||||
privacy: 'public',
|
||||
progress: 0,
|
||||
|
@ -101,17 +98,18 @@ export const ReducerRecord = ImmutableRecord({
|
|||
sensitive: false,
|
||||
spoiler: false,
|
||||
spoiler_text: '',
|
||||
suggestions: ImmutableList(),
|
||||
suggestions: ImmutableList<string>(),
|
||||
suggestion_token: null as string | null,
|
||||
tagHistory: ImmutableList<string>(),
|
||||
text: '',
|
||||
to: ImmutableOrderedSet<string>(),
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
type State = ImmutableMap<string, Compose>;
|
||||
type Compose = ReturnType<typeof ReducerCompose>;
|
||||
type Poll = ReturnType<typeof PollRecord>;
|
||||
|
||||
const statusToTextMentions = (state: State, status: ImmutableMap<string, any>, account: AccountEntity) => {
|
||||
const statusToTextMentions = (status: ImmutableMap<string, any>, account: AccountEntity) => {
|
||||
const author = status.getIn(['account', 'acct']);
|
||||
const mentions = status.get('mentions')?.map((m: ImmutableMap<string, any>) => m.get('acct')) || [];
|
||||
|
||||
|
@ -140,33 +138,33 @@ export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: A
|
|||
.delete(account.id) as ImmutableOrderedSet<string>;
|
||||
};
|
||||
|
||||
function clearAll(state: State) {
|
||||
return ReducerRecord({
|
||||
content_type: state.default_content_type,
|
||||
privacy: state.default_privacy,
|
||||
function clearAll(compose: Compose) {
|
||||
return ReducerCompose({
|
||||
content_type: compose.default_content_type,
|
||||
privacy: compose.default_privacy,
|
||||
idempotencyKey: uuid(),
|
||||
});
|
||||
}
|
||||
|
||||
function appendMedia(state: State, media: APIEntity) {
|
||||
const prevSize = state.media_attachments.size;
|
||||
function appendMedia(compose: Compose, media: APIEntity) {
|
||||
const prevSize = compose.media_attachments.size;
|
||||
|
||||
return state.withMutations(map => {
|
||||
return compose.withMutations(map => {
|
||||
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.default_sensitive || state.spoiler)) {
|
||||
if (prevSize === 0 && (compose.default_sensitive || compose.spoiler)) {
|
||||
map.set('sensitive', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeMedia(state: State, mediaId: string) {
|
||||
const prevSize = state.media_attachments.size;
|
||||
function removeMedia(compose: Compose, mediaId: string) {
|
||||
const prevSize = compose.media_attachments.size;
|
||||
|
||||
return state.withMutations(map => {
|
||||
return compose.withMutations(map => {
|
||||
map.update('media_attachments', list => list.filterNot(item => item.id === mediaId));
|
||||
map.set('idempotencyKey', uuid());
|
||||
|
||||
|
@ -176,8 +174,8 @@ function removeMedia(state: State, mediaId: string) {
|
|||
});
|
||||
}
|
||||
|
||||
const insertSuggestion = (state: State, position: number, token: string, completion: string, path: Array<string | number>) => {
|
||||
return state.withMutations(map => {
|
||||
const insertSuggestion = (compose: Compose, position: number, token: string, completion: string, path: Array<string | number>) => {
|
||||
return compose.withMutations(map => {
|
||||
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());
|
||||
|
@ -189,10 +187,10 @@ const insertSuggestion = (state: State, position: number, token: string, complet
|
|||
});
|
||||
};
|
||||
|
||||
const updateSuggestionTags = (state: State, token: string, currentTrends: ImmutableList<Tag>) => {
|
||||
const updateSuggestionTags = (compose: Compose, token: string, currentTrends: ImmutableList<Tag>) => {
|
||||
const prefix = token.slice(1);
|
||||
|
||||
return state.merge({
|
||||
return compose.merge({
|
||||
suggestions: ImmutableList(currentTrends
|
||||
.filter((tag) => tag.get('name').toLowerCase().startsWith(prefix.toLowerCase()))
|
||||
.slice(0, 4)
|
||||
|
@ -201,11 +199,11 @@ const updateSuggestionTags = (state: State, token: string, currentTrends: Immuta
|
|||
});
|
||||
};
|
||||
|
||||
const insertEmoji = (state: State, position: number, emojiData: Emoji, needsSpace: boolean) => {
|
||||
const oldText = state.text;
|
||||
const insertEmoji = (compose: Compose, position: number, emojiData: Emoji, needsSpace: boolean) => {
|
||||
const oldText = compose.text;
|
||||
const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native;
|
||||
|
||||
return state.merge({
|
||||
return compose.merge({
|
||||
text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
|
||||
focusDate: new Date(),
|
||||
caretPosition: position + emoji.length + 1,
|
||||
|
@ -246,13 +244,13 @@ const getAccountSettings = (account: ImmutableMap<string, any>) => {
|
|||
return account.getIn(['pleroma', 'settings_store', FE_NAME], ImmutableMap()) as ImmutableMap<string, any>;
|
||||
};
|
||||
|
||||
const importAccount = (state: State, account: APIEntity) => {
|
||||
const importAccount = (compose: Compose, account: APIEntity) => {
|
||||
const settings = getAccountSettings(ImmutableMap(fromJS(account)));
|
||||
|
||||
const defaultPrivacy = settings.get('defaultPrivacy', 'public');
|
||||
const defaultContentType = settings.get('defaultContentType', 'text/plain');
|
||||
|
||||
return state.merge({
|
||||
return compose.merge({
|
||||
default_privacy: defaultPrivacy,
|
||||
privacy: defaultPrivacy,
|
||||
default_content_type: defaultContentType,
|
||||
|
@ -261,85 +259,87 @@ const importAccount = (state: State, account: APIEntity) => {
|
|||
});
|
||||
};
|
||||
|
||||
const updateAccount = (state: State, account: APIEntity) => {
|
||||
const updateAccount = (compose: Compose, account: APIEntity) => {
|
||||
const settings = getAccountSettings(ImmutableMap(fromJS(account)));
|
||||
|
||||
const defaultPrivacy = settings.get('defaultPrivacy');
|
||||
const defaultContentType = settings.get('defaultContentType');
|
||||
|
||||
return state.withMutations(state => {
|
||||
if (defaultPrivacy) state.set('default_privacy', defaultPrivacy);
|
||||
if (defaultContentType) state.set('default_content_type', defaultContentType);
|
||||
return compose.withMutations(compose => {
|
||||
if (defaultPrivacy) compose.set('default_privacy', defaultPrivacy);
|
||||
if (defaultContentType) compose.set('default_content_type', defaultContentType);
|
||||
});
|
||||
};
|
||||
|
||||
const updateSetting = (state: State, path: string[], value: string) => {
|
||||
const updateSetting = (compose: Compose, path: string[], value: string) => {
|
||||
const pathString = path.join(',');
|
||||
switch (pathString) {
|
||||
case 'defaultPrivacy':
|
||||
return state.set('default_privacy', value).set('privacy', value);
|
||||
return compose.set('default_privacy', value).set('privacy', value);
|
||||
case 'defaultContentType':
|
||||
return state.set('default_content_type', value).set('content_type', value);
|
||||
return compose.set('default_content_type', value).set('content_type', value);
|
||||
default:
|
||||
return state;
|
||||
return compose;
|
||||
}
|
||||
};
|
||||
|
||||
export default function compose(state = ReducerRecord({ idempotencyKey: uuid(), resetFileKey: getResetFileKey() }), action: AnyAction) {
|
||||
const updateCompose = (state: State, key: string, updater: (compose: Compose) => Compose) =>
|
||||
state.update(key, ReducerCompose(), updater);
|
||||
|
||||
const initialState: State = ImmutableMap({
|
||||
default: ReducerCompose(),
|
||||
home: ReducerCompose(),
|
||||
});
|
||||
|
||||
export default function compose(state = initialState, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case COMPOSE_MOUNT:
|
||||
return state.set('mounted', state.mounted + 1);
|
||||
case COMPOSE_UNMOUNT:
|
||||
return state
|
||||
.set('mounted', Math.max(state.mounted - 1, 0))
|
||||
.set('is_composing', false);
|
||||
case COMPOSE_SENSITIVITY_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
if (!state.spoiler) {
|
||||
map.set('sensitive', !state.sensitive);
|
||||
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
||||
if (!compose.spoiler) {
|
||||
map.set('sensitive', !compose.sensitive);
|
||||
}
|
||||
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
}));
|
||||
case COMPOSE_TYPE_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
||||
map.set('content_type', action.value);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
}));
|
||||
case COMPOSE_SPOILERNESS_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
||||
map.set('spoiler_text', '');
|
||||
map.set('spoiler', !state.spoiler);
|
||||
map.set('spoiler', !compose.spoiler);
|
||||
map.set('idempotencyKey', uuid());
|
||||
|
||||
if (!state.sensitive && state.media_attachments.size >= 1) {
|
||||
if (!compose.sensitive && compose.media_attachments.size >= 1) {
|
||||
map.set('sensitive', true);
|
||||
}
|
||||
});
|
||||
}));
|
||||
case COMPOSE_SPOILER_TEXT_CHANGE:
|
||||
return state
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('spoiler_text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
.set('idempotencyKey', uuid()));
|
||||
case COMPOSE_VISIBILITY_CHANGE:
|
||||
return state
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('privacy', action.value)
|
||||
.set('idempotencyKey', uuid());
|
||||
.set('idempotencyKey', uuid()));
|
||||
case COMPOSE_CHANGE:
|
||||
return state
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
.set('idempotencyKey', uuid()));
|
||||
case COMPOSE_COMPOSING_CHANGE:
|
||||
return state.set('is_composing', action.value);
|
||||
return updateCompose(state, action.id, compose => compose.set('is_composing', action.value));
|
||||
case COMPOSE_REPLY:
|
||||
return state.withMutations(map => {
|
||||
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
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.visibility, state.default_privacy));
|
||||
map.set('text', !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : '');
|
||||
map.set('privacy', privacyPreference(action.status.visibility, compose.default_privacy));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('content_type', state.default_content_type);
|
||||
map.set('content_type', compose.default_content_type);
|
||||
|
||||
if (action.status.get('spoiler_text', '').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
|
@ -348,80 +348,82 @@ export default function compose(state = ReducerRecord({ idempotencyKey: uuid(),
|
|||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
});
|
||||
}));
|
||||
case COMPOSE_QUOTE:
|
||||
return state.withMutations(map => {
|
||||
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
|
||||
map.set('quote', action.status.get('id'));
|
||||
map.set('to', ImmutableOrderedSet());
|
||||
map.set('text', '');
|
||||
map.set('privacy', privacyPreference(action.status.visibility, state.default_privacy));
|
||||
map.set('privacy', privacyPreference(action.status.visibility, compose.default_privacy));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('content_type', state.default_content_type);
|
||||
map.set('content_type', compose.default_content_type);
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
});
|
||||
}));
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
return updateCompose(state, action.id, compose => compose.set('is_submitting', true));
|
||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||
return state.set('is_changing_upload', true);
|
||||
return updateCompose(state, action.id, compose => compose.set('is_changing_upload', true));
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
case COMPOSE_QUOTE_CANCEL:
|
||||
case COMPOSE_RESET:
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
return clearAll(state);
|
||||
return updateCompose(state, action.id, clearAll);
|
||||
case COMPOSE_SUBMIT_FAIL:
|
||||
return state.set('is_submitting', false);
|
||||
return updateCompose(state, action.id, compose => compose.set('is_submitting', false));
|
||||
case COMPOSE_UPLOAD_CHANGE_FAIL:
|
||||
return state.set('is_changing_upload', false);
|
||||
return updateCompose(state, action.id, compose => compose.set('is_changing_upload', false));
|
||||
case COMPOSE_UPLOAD_REQUEST:
|
||||
return state.set('is_uploading', true);
|
||||
return updateCompose(state, action.id, compose => compose.set('is_uploading', true));
|
||||
case COMPOSE_UPLOAD_SUCCESS:
|
||||
return appendMedia(state, fromJS(action.media));
|
||||
return updateCompose(state, action.id, compose => appendMedia(compose, fromJS(action.media)));
|
||||
case COMPOSE_UPLOAD_FAIL:
|
||||
return state.set('is_uploading', false);
|
||||
return updateCompose(state, action.id, compose => compose.set('is_uploading', false));
|
||||
case COMPOSE_UPLOAD_UNDO:
|
||||
return removeMedia(state, action.media_id);
|
||||
return updateCompose(state, action.id, compose => removeMedia(compose, action.media_id));
|
||||
case COMPOSE_UPLOAD_PROGRESS:
|
||||
return state.set('progress', Math.round((action.loaded / action.total) * 100));
|
||||
return updateCompose(state, action.id, compose => compose.set('progress', Math.round((action.loaded / action.total) * 100)));
|
||||
case COMPOSE_MENTION:
|
||||
return state.withMutations(map => {
|
||||
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
|
||||
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
}));
|
||||
case COMPOSE_DIRECT:
|
||||
return state.withMutations(map => {
|
||||
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
|
||||
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
||||
map.set('privacy', 'direct');
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
}));
|
||||
case COMPOSE_SUGGESTIONS_CLEAR:
|
||||
return state.update('suggestions', list => list.clear()).set('suggestion_token', null);
|
||||
return updateCompose(state, action.id, compose => compose.update('suggestions', list => list.clear()).set('suggestion_token', null));
|
||||
case COMPOSE_SUGGESTIONS_READY:
|
||||
return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map((item: APIEntity) => item.id) : action.emojis)).set('suggestion_token', action.token);
|
||||
return updateCompose(state, action.id, compose => compose.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);
|
||||
return updateCompose(state, action.id, compose => insertSuggestion(compose, action.position, action.token, action.completion, action.path));
|
||||
case COMPOSE_SUGGESTION_TAGS_UPDATE:
|
||||
return updateSuggestionTags(state, action.token, action.currentTrends);
|
||||
return updateCompose(state, action.id, compose => updateSuggestionTags(compose, action.token, action.currentTrends));
|
||||
case COMPOSE_TAG_HISTORY_UPDATE:
|
||||
return state.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList<string>);
|
||||
return updateCompose(state, action.id, compose => compose.set('tagHistory', ImmutableList(fromJS(action.tags)) as ImmutableList<string>));
|
||||
case TIMELINE_DELETE:
|
||||
if (action.id === state.in_reply_to) {
|
||||
return state.set('in_reply_to', null);
|
||||
} if (action.id === state.quote) {
|
||||
return state.set('quote', null);
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
return updateCompose(state, action.id, compose => {
|
||||
if (action.id === compose.in_reply_to) {
|
||||
return compose.set('in_reply_to', null);
|
||||
} if (action.id === compose.quote) {
|
||||
return compose.set('quote', null);
|
||||
} else {
|
||||
return compose;
|
||||
}
|
||||
});
|
||||
case COMPOSE_EMOJI_INSERT:
|
||||
return insertEmoji(state, action.position, action.emoji, action.needsSpace);
|
||||
return updateCompose(state, action.id, compose => insertEmoji(compose, action.position, action.emoji, action.needsSpace));
|
||||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
||||
return state
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('is_changing_upload', false)
|
||||
.update('media_attachments', list => list.map(item => {
|
||||
if (item.id === action.media.id) {
|
||||
|
@ -429,9 +431,9 @@ export default function compose(state = ReducerRecord({ idempotencyKey: uuid(),
|
|||
}
|
||||
|
||||
return item;
|
||||
}));
|
||||
})));
|
||||
case COMPOSE_SET_STATUS:
|
||||
return state.withMutations(map => {
|
||||
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
|
||||
if (!action.withRedraft) {
|
||||
map.set('id', action.status.get('id'));
|
||||
}
|
||||
|
@ -466,35 +468,35 @@ export default function compose(state = ReducerRecord({ idempotencyKey: uuid(),
|
|||
expires_in: 24 * 3600,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}));
|
||||
case COMPOSE_POLL_ADD:
|
||||
return state.set('poll', PollRecord());
|
||||
return updateCompose(state, action.id, compose => compose.set('poll', PollRecord()));
|
||||
case COMPOSE_POLL_REMOVE:
|
||||
return state.set('poll', null);
|
||||
return updateCompose(state, action.id, compose => compose.set('poll', null));
|
||||
case COMPOSE_SCHEDULE_ADD:
|
||||
return state.set('schedule', new Date());
|
||||
return updateCompose(state, action.id, compose => compose.set('schedule', new Date()));
|
||||
case COMPOSE_SCHEDULE_SET:
|
||||
return state.set('schedule', action.date);
|
||||
return updateCompose(state, action.id, compose => compose.set('schedule', action.date));
|
||||
case COMPOSE_SCHEDULE_REMOVE:
|
||||
return state.set('schedule', null);
|
||||
return updateCompose(state, action.id, compose => compose.set('schedule', null));
|
||||
case COMPOSE_POLL_OPTION_ADD:
|
||||
return state.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).push(action.title));
|
||||
return updateCompose(state, action.id, compose => compose.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);
|
||||
return updateCompose(state, action.id, compose => compose.setIn(['poll', 'options', action.index], action.title));
|
||||
case COMPOSE_POLL_OPTION_REMOVE:
|
||||
return state.updateIn(['poll', 'options'], options => (options as ImmutableList<string>).delete(action.index));
|
||||
return updateCompose(state, action.id, compose => compose.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 updateCompose(state, action.id, compose => compose.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 updateCompose(state, action.id, compose => compose.update('to', mentions => mentions!.add(action.account)));
|
||||
case COMPOSE_REMOVE_FROM_MENTIONS:
|
||||
return state.update('to', mentions => mentions!.delete(action.account));
|
||||
return updateCompose(state, action.id, compose => compose.update('to', mentions => mentions!.delete(action.account)));
|
||||
case ME_FETCH_SUCCESS:
|
||||
return importAccount(state, action.me);
|
||||
return updateCompose(state, 'default', compose => importAccount(compose, action.me));
|
||||
case ME_PATCH_SUCCESS:
|
||||
return updateAccount(state, action.me);
|
||||
return updateCompose(state, 'default', compose => updateAccount(compose, action.me));
|
||||
case SETTING_CHANGE:
|
||||
return updateSetting(state, action.path, action.value);
|
||||
return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue