Merge branch 'drafts' into HEAD
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
312931876e
25 changed files with 502 additions and 54 deletions
|
@ -8,7 +8,7 @@ import { isNativeEmoji } from 'soapbox/features/emoji';
|
|||
import emojiSearch from 'soapbox/features/emoji/search';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
import { normalizeTag } from 'soapbox/normalizers';
|
||||
import { selectAccount, selectOwnAccount } from 'soapbox/selectors';
|
||||
import { selectAccount, selectOwnAccount, makeGetAccount } from 'soapbox/selectors';
|
||||
import { tagHistory } from 'soapbox/settings';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
|
@ -90,6 +90,8 @@ const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const;
|
|||
|
||||
const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const messages = defineMessages({
|
||||
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' },
|
||||
|
@ -111,9 +113,11 @@ interface ComposeSetStatusAction {
|
|||
contentType?: string | false;
|
||||
v: ReturnType<typeof parseVersion>;
|
||||
withRedraft?: boolean;
|
||||
draftId?: string;
|
||||
editorState?: string | null;
|
||||
}
|
||||
|
||||
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean) =>
|
||||
const setComposeToStatus = (status: Status, rawText: string, spoilerText?: string, contentType?: string | false, withRedraft?: boolean, draftId?: string, editorState?: string | null) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { instance } = getState();
|
||||
const { explicitAddressing } = getFeatures(instance);
|
||||
|
@ -128,6 +132,8 @@ const setComposeToStatus = (status: Status, rawText: string, spoilerText?: strin
|
|||
contentType,
|
||||
v: parseVersion(instance.version),
|
||||
withRedraft,
|
||||
draftId,
|
||||
editorState,
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
|
@ -280,8 +286,13 @@ const directComposeById = (accountId: string) =>
|
|||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: APIEntity, status: string, edit?: boolean, propagate?: boolean) => {
|
||||
if (!dispatch || !getState) return;
|
||||
|
||||
const state = getState();
|
||||
|
||||
const accountUrl = getAccount(state, state.me as string)!.url;
|
||||
const draftId = getState().compose.get(composeId)!.draft_id;
|
||||
|
||||
dispatch(insertIntoTagHistory(composeId, data.tags || [], status));
|
||||
dispatch(submitComposeSuccess(composeId, { ...data }));
|
||||
dispatch(submitComposeSuccess(composeId, { ...data }, accountUrl, draftId));
|
||||
const toastMessage = edit ? messages.editSuccess : messages.success;
|
||||
const toastOpts = {
|
||||
actionLabel: messages.view,
|
||||
|
@ -398,10 +409,12 @@ const submitComposeRequest = (composeId: string) => ({
|
|||
id: composeId,
|
||||
});
|
||||
|
||||
const submitComposeSuccess = (composeId: string, status: APIEntity) => ({
|
||||
const submitComposeSuccess = (composeId: string, status: APIEntity, accountUrl: string, draftId?: string | null) => ({
|
||||
type: COMPOSE_SUBMIT_SUCCESS,
|
||||
id: composeId,
|
||||
status: status,
|
||||
accountUrl,
|
||||
draftId,
|
||||
});
|
||||
|
||||
const submitComposeFail = (composeId: string, error: unknown) => ({
|
||||
|
@ -861,10 +874,11 @@ const eventDiscussionCompose = (composeId: string, status: Status) =>
|
|||
});
|
||||
};
|
||||
|
||||
const setEditorState = (composeId: string, editorState: EditorState | string | null) => ({
|
||||
const setEditorState = (composeId: string, editorState: EditorState | string | null, text?: string) => ({
|
||||
type: COMPOSE_EDITOR_STATE_SET,
|
||||
id: composeId,
|
||||
editorState: editorState,
|
||||
text,
|
||||
});
|
||||
|
||||
type ComposeAction =
|
||||
|
|
66
src/actions/draft-statuses.ts
Normal file
66
src/actions/draft-statuses.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
const DRAFT_STATUSES_FETCH_SUCCESS = 'DRAFT_STATUSES_FETCH_SUCCESS';
|
||||
|
||||
const PERSIST_DRAFT_STATUS = 'PERSIST_DRAFT_STATUS';
|
||||
const CANCEL_DRAFT_STATUS = 'DELETE_DRAFT_STATUS';
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const fetchDraftStatuses = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const accountUrl = getAccount(state, state.me as string)!.url;
|
||||
|
||||
return KVStore.getItem(`drafts:${accountUrl}`).then((statuses) => {
|
||||
dispatch({
|
||||
type: DRAFT_STATUSES_FETCH_SUCCESS,
|
||||
statuses,
|
||||
});
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const saveDraftStatus = (composeId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const accountUrl = getAccount(state, state.me as string)!.url;
|
||||
|
||||
const compose = state.compose.get(composeId)!;
|
||||
|
||||
const draft = {
|
||||
...compose.toJS(),
|
||||
draft_id: compose.draft_id || uuid(),
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: PERSIST_DRAFT_STATUS,
|
||||
status: draft,
|
||||
accountUrl,
|
||||
});
|
||||
};
|
||||
|
||||
const cancelDraftStatus = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const accountUrl = getAccount(state, state.me as string)!.url;
|
||||
|
||||
dispatch({
|
||||
type: CANCEL_DRAFT_STATUS,
|
||||
id,
|
||||
accountUrl,
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
DRAFT_STATUSES_FETCH_SUCCESS,
|
||||
PERSIST_DRAFT_STATUS,
|
||||
CANCEL_DRAFT_STATUS,
|
||||
fetchDraftStatuses,
|
||||
saveDraftStatus,
|
||||
cancelDraftStatus,
|
||||
};
|
|
@ -5,6 +5,7 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { cancelReplyCompose } from 'soapbox/actions/compose';
|
||||
import { saveDraftStatus } from 'soapbox/actions/draft-statuses';
|
||||
import { cancelEventCompose } from 'soapbox/actions/events';
|
||||
import { openModal, closeModal } from 'soapbox/actions/modals';
|
||||
import { useAppDispatch, usePrevious } from 'soapbox/hooks';
|
||||
|
@ -16,6 +17,7 @@ import type { ReducerRecord as ReducerComposeEvent } from 'soapbox/reducers/comp
|
|||
const messages = defineMessages({
|
||||
confirm: { id: 'confirmations.cancel.confirm', defaultMessage: 'Discard' },
|
||||
cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' },
|
||||
saveDraft: { id: 'confirmations.cancel_editing.save_draft', defaultMessage: 'Save draft' },
|
||||
});
|
||||
|
||||
export const checkComposeContent = (compose?: ReturnType<typeof ReducerCompose>) => {
|
||||
|
@ -90,6 +92,12 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
onCancel: () => {
|
||||
dispatch(closeModal('CONFIRM'));
|
||||
},
|
||||
secondary: intl.formatMessage(messages.saveDraft),
|
||||
onSecondary: isEditing ? undefined : () => {
|
||||
dispatch(saveDraftStatus('compose-modal'));
|
||||
dispatch(closeModal('COMPOSE'));
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
}));
|
||||
} else if (hasEventComposeContent && type === 'COMPOSE_EVENT') {
|
||||
const isEditing = getState().compose_event.id !== null;
|
||||
|
|
|
@ -34,6 +34,7 @@ const messages = defineMessages({
|
|||
events: { id: 'column.events', defaultMessage: 'Events' },
|
||||
invites: { id: 'navigation_bar.invites', defaultMessage: 'Invites' },
|
||||
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
||||
drafts: { id: 'navigation.drafts', defaultMessage: 'Drafts' },
|
||||
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
||||
addExternalAccount: { id: 'profile_dropdown.add_external_account', defaultMessage: 'Add account from external instance' },
|
||||
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
|
@ -86,6 +87,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
|
||||
const settings = useAppSelector((state) => getSettings(state));
|
||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
||||
const groupsPath = useGroupsPath();
|
||||
|
||||
const closeButtonRef = React.useRef(null);
|
||||
|
@ -250,6 +252,15 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{draftCount > 0 && (
|
||||
<SidebarLink
|
||||
to='/draft_statuses'
|
||||
icon={require('@tabler/icons/notes.svg')}
|
||||
text={intl.formatMessage(messages.drafts)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.publicTimeline && <>
|
||||
<Divider />
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ const messages = defineMessages({
|
|||
events: { id: 'column.events', defaultMessage: 'Events' },
|
||||
profileDirectory: { id: 'navigation_bar.profile_directory', defaultMessage: 'Profile directory' },
|
||||
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
||||
drafts: { id: 'navigation.drafts', defaultMessage: 'Drafts' },
|
||||
});
|
||||
|
||||
/** Desktop sidebar with links to different views in the app. */
|
||||
|
@ -32,6 +33,7 @@ const SidebarNavigation = () => {
|
|||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||
const draftCount = useAppSelector((state) => state.draft_statuses.size);
|
||||
|
||||
const restrictUnauth = instance.pleroma.metadata.restrict_unauthenticated;
|
||||
|
||||
|
@ -87,6 +89,15 @@ const SidebarNavigation = () => {
|
|||
text: intl.formatMessage(messages.developers),
|
||||
});
|
||||
}
|
||||
|
||||
if (draftCount > 0) {
|
||||
menu.push({
|
||||
to: '/draft_statuses',
|
||||
icon: require('@tabler/icons/notes.svg'),
|
||||
text: intl.formatMessage(messages.drafts),
|
||||
count: draftCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return menu;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { $createRemarkExport } from '@mkljczk/lexical-remark';
|
||||
import clsx from 'clsx';
|
||||
import { CLEAR_EDITOR_COMMAND, TextNode, type LexicalEditor } from 'lexical';
|
||||
import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { length } from 'stringz';
|
||||
|
||||
|
@ -88,6 +87,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
is_uploading: isUploading,
|
||||
schedule: scheduledAt,
|
||||
group_id: groupId,
|
||||
text,
|
||||
} = compose;
|
||||
|
||||
const prevSpoiler = usePrevious(spoiler);
|
||||
|
@ -105,12 +105,12 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
const { isDraggedOver } = useDraggedFiles(formRef);
|
||||
|
||||
const text = editorRef.current?.getEditorState().read($createRemarkExport({
|
||||
handlers: {
|
||||
hashtag: (node) => ({ type: 'text', value: node.getTextContent() }),
|
||||
mention: (node) => ({ type: 'text', value: node.getTextContent() }),
|
||||
},
|
||||
})) ?? '';
|
||||
// const text = editorRef.current?.getEditorState().read($createRemarkExport({
|
||||
// handlers: {
|
||||
// hashtag: (node) => ({ type: 'text', value: node.getTextContent() }),
|
||||
// mention: (node) => ({ type: 'text', value: node.getTextContent() }),
|
||||
// },
|
||||
// })) ?? '';
|
||||
|
||||
const fulltext = [spoilerText, countableText(text)].join('');
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ class EmojiNode extends DecoratorNode<JSX.Element> {
|
|||
decorate(): JSX.Element {
|
||||
const emoji = this.__emoji;
|
||||
if (isNativeEmoji(emoji)) {
|
||||
return <Component emoji={emoji.native} alt={emoji.colons} className='emojione h-4 w-4' />;
|
||||
return <Component emoji={emoji.native} alt={emoji.colons} className='emojione h-4 w-4' />;
|
||||
} else {
|
||||
return <Component src={emoji.imageUrl} alt={emoji.colons} className='emojione h-4 w-4' />;
|
||||
}
|
||||
|
|
|
@ -15,9 +15,10 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId }) => {
|
|||
|
||||
useEffect(() => {
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
const isEmpty = editorState.read(() => $getRoot().getTextContent()) === '';
|
||||
const text = editorState.read(() => $getRoot().getTextContent());
|
||||
const isEmpty = text === '';
|
||||
const data = isEmpty ? null : JSON.stringify(editorState.toJSON());
|
||||
dispatch(setEditorState(composeId, data));
|
||||
dispatch(setEditorState(composeId, data, text));
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
|
|
43
src/features/draft-statuses/builder.tsx
Normal file
43
src/features/draft-statuses/builder.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { normalizeStatus } from 'soapbox/normalizers/status';
|
||||
import { calculateStatus } from 'soapbox/reducers/statuses';
|
||||
|
||||
import type { DraftStatus } from 'soapbox/reducers/draft-statuses';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
const buildPoll = (draftStatus: DraftStatus) => {
|
||||
if (draftStatus.hasIn(['poll', 'options'])) {
|
||||
return draftStatus.poll!
|
||||
.set('id', `${draftStatus.draft_id}-poll`)
|
||||
.update('options', (options: ImmutableMap<string, any>) => {
|
||||
return options.map((title: string) => ImmutableMap({ title }));
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const buildStatus = (state: RootState, draftStatus: DraftStatus) => {
|
||||
const me = state.me as string;
|
||||
const account = state.entities[Entities.ACCOUNTS]?.store[me];
|
||||
|
||||
const status = ImmutableMap({
|
||||
account,
|
||||
content: draftStatus.text.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
|
||||
created_at: draftStatus.schedule,
|
||||
group: draftStatus.group_id,
|
||||
in_reply_to_id: draftStatus.in_reply_to,
|
||||
media_attachments: draftStatus.media_attachments,
|
||||
poll: buildPoll(draftStatus),
|
||||
quote: draftStatus.quote,
|
||||
sensitive: draftStatus.sensitive,
|
||||
spoiler_text: draftStatus.spoiler_text,
|
||||
uri: `/draft_statuses/${draftStatus.draft_id}`,
|
||||
url: `/draft_statuses/${draftStatus.draft_id}`,
|
||||
visibility: draftStatus.privacy,
|
||||
});
|
||||
|
||||
return calculateStatus(normalizeStatus(status));
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { setComposeToStatus } from 'soapbox/actions/compose';
|
||||
import { cancelDraftStatus } from 'soapbox/actions/draft-statuses';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { Button, HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import type { DraftStatus } from 'soapbox/reducers/draft-statuses';
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit: { id: 'draft_status.edit', defaultMessage: 'Edit' },
|
||||
cancel: { id: 'draft_status.cancel', defaultMessage: 'Cancel' },
|
||||
deleteConfirm: { id: 'confirmations.draft_status_delete.confirm', defaultMessage: 'Discard' },
|
||||
deleteHeading: { id: 'confirmations.draft_status_delete.heading', defaultMessage: 'Cancel draft post' },
|
||||
deleteMessage: { id: 'confirmations.draft_status_delete.message', defaultMessage: 'Are you sure you want to discard this draft post?' },
|
||||
});
|
||||
|
||||
interface IDraftStatusActionBar {
|
||||
source: DraftStatus;
|
||||
status: StatusEntity;
|
||||
}
|
||||
|
||||
const DraftStatusActionBar: React.FC<IDraftStatusActionBar> = ({ source, status }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleCancelClick = () => {
|
||||
dispatch((_, getState) => {
|
||||
|
||||
const deleteModal = getSettings(getState()).get('deleteModal');
|
||||
if (!deleteModal) {
|
||||
dispatch(cancelDraftStatus(source.draft_id));
|
||||
} else {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
icon: require('@tabler/icons/calendar-stats.svg'),
|
||||
heading: intl.formatMessage(messages.deleteHeading),
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(cancelDraftStatus(source.draft_id)),
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditClick = () => {
|
||||
dispatch(setComposeToStatus(status, source.text, source.spoiler_text, source.content_type, false, source.draft_id, source.editorState));
|
||||
dispatch(openModal('COMPOSE'));
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack space={2} justifyContent='end'>
|
||||
<Button theme='primary' size='sm' onClick={handleEditClick}>
|
||||
<FormattedMessage id='draft_status.edit' defaultMessage='Edit' />
|
||||
</Button>
|
||||
<Button theme='danger' size='sm' onClick={handleCancelClick}>
|
||||
<FormattedMessage id='draft_status.cancel' defaultMessage='Delete' />
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraftStatusActionBar;
|
88
src/features/draft-statuses/components/draft-status.tsx
Normal file
88
src/features/draft-statuses/components/draft-status.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Account from 'soapbox/components/account';
|
||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
import StatusContent from 'soapbox/components/status-content';
|
||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||
import { HStack, Stack } from 'soapbox/components/ui';
|
||||
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
|
||||
import PollPreview from 'soapbox/features/ui/components/poll-preview';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import { buildStatus } from '../builder';
|
||||
|
||||
import DraftStatusActionBar from './draft-status-action-bar';
|
||||
|
||||
import type { DraftStatus as DraftStatusType } from 'soapbox/reducers/draft-statuses';
|
||||
import type { Poll as PollEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IDraftStatus {
|
||||
draftStatus: DraftStatusType;
|
||||
}
|
||||
|
||||
const DraftStatus: React.FC<IDraftStatus> = ({ draftStatus, ...other }) => {
|
||||
const status = useAppSelector((state) => {
|
||||
if (!draftStatus) return null;
|
||||
return buildStatus(state, draftStatus);
|
||||
}) as StatusEntity | null;
|
||||
|
||||
if (!status) return null;
|
||||
|
||||
const account = status.account;
|
||||
|
||||
let quote;
|
||||
|
||||
if (status.quote) {
|
||||
if (status.pleroma.get('quote_visible', true) === false) {
|
||||
quote = (
|
||||
<div className='quoted-status-tombstone'>
|
||||
<p><FormattedMessage id='statuses.quote_tombstone' defaultMessage='Post is unavailable.' /></p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
quote = <QuotedStatus statusId={status.quote as string} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('status__wrapper py-4', `status__wrapper-${status.visibility}`, { 'status__wrapper-reply': !!status.in_reply_to_id })} tabIndex={0}>
|
||||
<div className={clsx('status', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} data-id={status.id}>
|
||||
<div className='mb-4'>
|
||||
<HStack justifyContent='between' alignItems='start'>
|
||||
<Account
|
||||
key={account.id}
|
||||
account={account}
|
||||
timestamp={status.created_at}
|
||||
futureTimestamp
|
||||
action={<DraftStatusActionBar source={draftStatus} status={status} {...other} />}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<StatusReplyMentions status={status} />
|
||||
|
||||
<Stack space={4}>
|
||||
<StatusContent
|
||||
status={status}
|
||||
collapsable
|
||||
/>
|
||||
|
||||
{status.media_attachments.size > 0 && (
|
||||
<AttachmentThumbs
|
||||
media={status.media_attachments}
|
||||
sensitive={status.sensitive}
|
||||
/>
|
||||
)}
|
||||
|
||||
{quote}
|
||||
|
||||
{status.poll && <PollPreview poll={status.poll as PollEntity} />}
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraftStatus;
|
40
src/features/draft-statuses/index.tsx
Normal file
40
src/features/draft-statuses/index.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchDraftStatuses } from 'soapbox/actions/draft-statuses';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import DraftStatus from './components/draft-status';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.draft_statuses', defaultMessage: 'Drafts' },
|
||||
});
|
||||
|
||||
const DraftStatuses = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const drafts = useAppSelector((state) => state.draft_statuses);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDraftStatuses());
|
||||
}, []);
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.draft_statuses' defaultMessage="You don't have any draft statuses yet. When you add one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
scrollKey='draft_statuses'
|
||||
emptyMessage={emptyMessage}
|
||||
listClassName='divide-y divide-solid divide-gray-200 dark:divide-gray-800'
|
||||
>
|
||||
{drafts.toOrderedSet().reverse().map((draft) => <DraftStatus key={draft.draft_id} draftStatus={draft} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraftStatuses;
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { cancelScheduledStatus } from 'soapbox/actions/scheduled-statuses';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { Button, HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
@ -46,12 +45,9 @@ const ScheduledStatusActionBar: React.FC<IScheduledStatusActionBar> = ({ status
|
|||
|
||||
return (
|
||||
<HStack justifyContent='end'>
|
||||
<IconButton
|
||||
title={intl.formatMessage(messages.cancel)}
|
||||
text={intl.formatMessage(messages.cancel)}
|
||||
src={require('@tabler/icons/x.svg')}
|
||||
onClick={handleCancelClick}
|
||||
/>
|
||||
<Button theme='danger' size='sm' onClick={handleCancelClick}>
|
||||
<FormattedMessage id='scheduled_status.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import Account from 'soapbox/components/account';
|
|||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||
import StatusContent from 'soapbox/components/status-content';
|
||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { HStack, Stack } from 'soapbox/components/ui';
|
||||
import PollPreview from 'soapbox/features/ui/components/poll-preview';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { buildStatus } from '../builder';
|
|||
|
||||
import ScheduledStatusActionBar from './scheduled-status-action-bar';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
import type { Poll as PollEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IScheduledStatus {
|
||||
statusId: string;
|
||||
|
@ -31,7 +31,7 @@ const ScheduledStatus: React.FC<IScheduledStatus> = ({ statusId, ...other }) =>
|
|||
const account = status.account;
|
||||
|
||||
return (
|
||||
<div className={clsx('status__wrapper', `status__wrapper-${status.visibility}`, { 'status__wrapper-reply': !!status.in_reply_to_id })} tabIndex={0}>
|
||||
<div className={clsx('status__wrapper py-4', `status__wrapper-${status.visibility}`, { 'status__wrapper-reply': !!status.in_reply_to_id })} tabIndex={0}>
|
||||
<div className={clsx('status', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} data-id={status.id}>
|
||||
<div className='mb-4'>
|
||||
<HStack justifyContent='between' alignItems='start'>
|
||||
|
@ -40,28 +40,28 @@ const ScheduledStatus: React.FC<IScheduledStatus> = ({ statusId, ...other }) =>
|
|||
account={account}
|
||||
timestamp={status.created_at}
|
||||
futureTimestamp
|
||||
hideActions
|
||||
action={<ScheduledStatusActionBar status={status} {...other} />}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<StatusReplyMentions status={status} />
|
||||
|
||||
<StatusContent
|
||||
status={status}
|
||||
collapsable
|
||||
/>
|
||||
|
||||
{status.media_attachments.size > 0 && (
|
||||
<AttachmentThumbs
|
||||
media={status.media_attachments}
|
||||
sensitive={status.sensitive}
|
||||
<Stack space={4}>
|
||||
<StatusContent
|
||||
status={status}
|
||||
collapsable
|
||||
/>
|
||||
)}
|
||||
|
||||
{status.poll && <PollPreview pollId={status.poll as string} />}
|
||||
{status.media_attachments.size > 0 && (
|
||||
<AttachmentThumbs
|
||||
media={status.media_attachments}
|
||||
sensitive={status.sensitive}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScheduledStatusActionBar status={status} {...other} />
|
||||
{status.poll && <PollPreview poll={status.poll as PollEntity} />}
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -39,6 +39,7 @@ const ScheduledStatuses = () => {
|
|||
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
|
||||
onLoadMore={() => handleLoadMore(dispatch)}
|
||||
emptyMessage={emptyMessage}
|
||||
listClassName='divide-y divide-solid divide-gray-200 dark:divide-gray-800'
|
||||
>
|
||||
{statusIds.map((id: string) => <ScheduledStatus key={id} statusId={id} />)}
|
||||
</ScrollableList>
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { useRef } from 'react';
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { cancelReplyCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose';
|
||||
import { saveDraftStatus } from 'soapbox/actions/draft-statuses';
|
||||
import { openModal, closeModal } from 'soapbox/actions/modals';
|
||||
import { useGroup } from 'soapbox/api/hooks';
|
||||
import { checkComposeContent } from 'soapbox/components/modal-root';
|
||||
|
@ -14,6 +15,7 @@ import ComposeForm from '../../../compose/components/compose-form';
|
|||
const messages = defineMessages({
|
||||
confirm: { id: 'confirmations.cancel.confirm', defaultMessage: 'Discard' },
|
||||
cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' },
|
||||
saveDraft: { id: 'confirmations.cancel_editing.save_draft', defaultMessage: 'Save draft' },
|
||||
});
|
||||
|
||||
interface IComposeModal {
|
||||
|
@ -48,6 +50,12 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose, composeId = 'compose-m
|
|||
dispatch(closeModal('COMPOSE'));
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
secondary: intl.formatMessage(messages.saveDraft),
|
||||
onSecondary: statusId ? undefined : () => {
|
||||
dispatch(saveDraftStatus(composeId));
|
||||
dispatch(closeModal('COMPOSE'));
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
onClose('COMPOSE');
|
||||
|
|
|
@ -14,7 +14,7 @@ import { buildStatus } from '../util/pending-status-builder';
|
|||
|
||||
import PollPreview from './poll-preview';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
import type { Poll as PollEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const shouldHaveCard = (pendingStatus: StatusEntity) => {
|
||||
return Boolean(pendingStatus.content.match(/https?:\/\/\S*/));
|
||||
|
@ -60,7 +60,10 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
|||
<div className={clsx('opacity-50', className)}>
|
||||
<div className={clsx('status', { 'status-reply': !!status.in_reply_to_id, muted })} data-id={status.id}>
|
||||
<Card
|
||||
className={clsx('py-6 sm:p-5', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })}
|
||||
className={clsx(`status-${status.visibility}`, {
|
||||
'py-6 sm:p-5': !thread,
|
||||
'status-reply': !!status.in_reply_to_id,
|
||||
})}
|
||||
variant={thread ? 'default' : 'rounded'}
|
||||
>
|
||||
<div className='mb-4'>
|
||||
|
@ -86,7 +89,7 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
|
|||
|
||||
<PendingStatusMedia status={status} />
|
||||
|
||||
{status.poll && <PollPreview pollId={status.poll as string} />}
|
||||
{status.poll && <PollPreview poll={status.poll as PollEntity} />}
|
||||
|
||||
{status.quote && <QuotedStatus statusId={status.quote as string} />}
|
||||
</Stack>
|
||||
|
|
|
@ -3,17 +3,14 @@ import React from 'react';
|
|||
|
||||
import PollOption from 'soapbox/components/polls/poll-option';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { Poll as PollEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IPollPreview {
|
||||
pollId: string;
|
||||
poll: PollEntity;
|
||||
}
|
||||
|
||||
const PollPreview: React.FC<IPollPreview> = ({ pollId }) => {
|
||||
const poll = useAppSelector((state) => state.polls.get(pollId) as PollEntity);
|
||||
|
||||
if (!poll) {
|
||||
const PollPreview: React.FC<IPollPreview> = ({ poll }) => {
|
||||
if (typeof poll !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { fetchFollowRequests } from 'soapbox/actions/accounts';
|
|||
import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin';
|
||||
import { fetchAnnouncements } from 'soapbox/actions/announcements';
|
||||
import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis';
|
||||
import { fetchDraftStatuses } from 'soapbox/actions/draft-statuses';
|
||||
import { fetchFilters } from 'soapbox/actions/filters';
|
||||
import { fetchMarker } from 'soapbox/actions/markers';
|
||||
import { expandNotifications } from 'soapbox/actions/notifications';
|
||||
|
@ -141,6 +142,7 @@ import {
|
|||
EditIdentity,
|
||||
Domains,
|
||||
NostrRelays,
|
||||
DraftStatuses,
|
||||
} from './util/async-components';
|
||||
import GlobalHotkeys from './util/global-hotkeys';
|
||||
import { WrappedRoute } from './util/react-router-helpers';
|
||||
|
@ -307,6 +309,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
|
||||
<WrappedRoute path='/statuses/:statusId' exact page={StatusPage} component={Status} content={children} />
|
||||
{features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />}
|
||||
<WrappedRoute path='/draft_statuses' page={DefaultPage} component={DraftStatuses} content={children} />
|
||||
|
||||
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
|
||||
{features.nip05 && <WrappedRoute path='/settings/identity' page={DefaultPage} component={EditIdentity} content={children} />}
|
||||
|
@ -402,6 +405,8 @@ const UI: React.FC<IUI> = ({ children }) => {
|
|||
const loadAccountData = () => {
|
||||
if (!account) return;
|
||||
|
||||
dispatch(fetchDraftStatuses());
|
||||
|
||||
dispatch(expandHomeTimeline({}, intl, () => {
|
||||
dispatch(fetchSuggestionsForTimeline());
|
||||
}));
|
||||
|
|
|
@ -169,4 +169,5 @@ export const SelectBookmarkFolderModal = lazy(() => import('soapbox/features/ui/
|
|||
export const EditIdentity = lazy(() => import('soapbox/features/edit-identity'));
|
||||
export const Domains = lazy(() => import('soapbox/features/admin/domains'));
|
||||
export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal'));
|
||||
export const NostrRelays = lazy(() => import('soapbox/features/nostr-relays'));
|
||||
export const NostrRelays = lazy(() => import('soapbox/features/nostr-relays'));
|
||||
export const DraftStatuses = lazy(() => import('soapbox/features/draft-statuses'));
|
||||
|
|
|
@ -339,6 +339,7 @@
|
|||
"column.directory": "Browse profiles",
|
||||
"column.dislikes": "Dislikes",
|
||||
"column.domain_blocks": "Hidden domains",
|
||||
"column.draft_statuses": "Drafts",
|
||||
"column.edit_profile": "Edit profile",
|
||||
"column.event_map": "Event location",
|
||||
"column.event_participants": "Event participants",
|
||||
|
@ -511,6 +512,7 @@
|
|||
"confirmations.cancel_editing.confirm": "Cancel editing",
|
||||
"confirmations.cancel_editing.heading": "Cancel post editing",
|
||||
"confirmations.cancel_editing.message": "Are you sure you want to cancel editing this post? All changes will be lost.",
|
||||
"confirmations.cancel_editing.save_draft": "Save draft",
|
||||
"confirmations.cancel_event_editing.heading": "Cancel event editing",
|
||||
"confirmations.cancel_event_editing.message": "Are you sure you want to cancel editing this event? All changes will be lost.",
|
||||
"confirmations.delete.confirm": "Delete",
|
||||
|
@ -532,6 +534,9 @@
|
|||
"confirmations.domain_block.confirm": "Hide entire domain",
|
||||
"confirmations.domain_block.heading": "Block {domain}",
|
||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.",
|
||||
"confirmations.draft_status_delete.confirm": "Discard",
|
||||
"confirmations.draft_status_delete.heading": "Cancel draft post",
|
||||
"confirmations.draft_status_delete.message": "Are you sure you want to discard this draft post?",
|
||||
"confirmations.kick_from_group.confirm": "Kick",
|
||||
"confirmations.kick_from_group.message": "Are you sure you want to kick @{name} from this group?",
|
||||
"confirmations.leave_event.confirm": "Leave event",
|
||||
|
@ -602,6 +607,8 @@
|
|||
"directory.local": "From {domain} only",
|
||||
"directory.new_arrivals": "New arrivals",
|
||||
"directory.recently_active": "Recently active",
|
||||
"draft_status.cancel": "Delete",
|
||||
"draft_status.edit": "Edit",
|
||||
"edit_bookmark_folder_modal.confirm": "Save",
|
||||
"edit_bookmark_folder_modal.header_title": "Edit folder",
|
||||
"edit_email.header": "Change Email",
|
||||
|
@ -688,6 +695,7 @@
|
|||
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
|
||||
"empty_column.dislikes": "No one has disliked this post yet. When someone does, they will show up here.",
|
||||
"empty_column.domain_blocks": "There are no hidden domains yet.",
|
||||
"empty_column.draft_statuses": "You don't have any draft statuses yet. When you add one, it will show up here.",
|
||||
"empty_column.event_participant_requests": "There are no pending event participation requests.",
|
||||
"empty_column.event_participants": "No one joined this event yet. When someone does, they will show up here.",
|
||||
"empty_column.favourited_statuses": "You don't have any liked posts yet. When you like one, it will show up here.",
|
||||
|
@ -1074,6 +1082,7 @@
|
|||
"navigation.dashboard": "Dashboard",
|
||||
"navigation.developers": "Developers",
|
||||
"navigation.direct_messages": "Messages",
|
||||
"navigation.drafts": "Drafts",
|
||||
"navigation.home": "Home",
|
||||
"navigation.notifications": "Notifications",
|
||||
"navigation.search": "Search",
|
||||
|
|
|
@ -82,6 +82,7 @@ const PollRecord = ImmutableRecord({
|
|||
export const ReducerCompose = ImmutableRecord({
|
||||
caretPosition: null as number | null,
|
||||
content_type: 'text/plain',
|
||||
draft_id: null as string | null,
|
||||
editorState: null as string | null,
|
||||
focusDate: null as Date | null,
|
||||
group_id: null as string | null,
|
||||
|
@ -111,7 +112,7 @@ export const ReducerCompose = ImmutableRecord({
|
|||
});
|
||||
|
||||
type State = ImmutableMap<string, Compose>;
|
||||
type Compose = ReturnType<typeof ReducerCompose>;
|
||||
export type Compose = ReturnType<typeof ReducerCompose>;
|
||||
type Poll = ReturnType<typeof PollRecord>;
|
||||
|
||||
const statusToTextMentions = (status: Status, account: Account) => {
|
||||
|
@ -438,7 +439,7 @@ export default function compose(state = initialState, action: ComposeAction | Ev
|
|||
})));
|
||||
case COMPOSE_SET_STATUS:
|
||||
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
|
||||
if (!action.withRedraft) {
|
||||
if (!action.withRedraft && !action.draftId) {
|
||||
map.set('id', action.status.id);
|
||||
}
|
||||
map.set('text', action.rawText || unescapeHTML(expandMentions(action.status)));
|
||||
|
@ -473,6 +474,14 @@ export default function compose(state = initialState, action: ComposeAction | Ev
|
|||
expires_in: 24 * 3600,
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.draftId) {
|
||||
map.set('draft_id', action.draftId);
|
||||
}
|
||||
|
||||
if (action.editorState) {
|
||||
map.set('editorState', action.editorState);
|
||||
}
|
||||
}));
|
||||
case COMPOSE_POLL_ADD:
|
||||
return updateCompose(state, action.id, compose => compose.set('poll', PollRecord()));
|
||||
|
@ -514,7 +523,9 @@ export default function compose(state = initialState, action: ComposeAction | Ev
|
|||
case SETTING_CHANGE:
|
||||
return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value));
|
||||
case COMPOSE_EDITOR_STATE_SET:
|
||||
return updateCompose(state, action.id, compose => compose.set('editorState', action.editorState as string));
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('editorState', action.editorState as string)
|
||||
.set('text', action.text as string));
|
||||
case EVENT_COMPOSE_CANCEL:
|
||||
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', ''));
|
||||
case EVENT_FORM_SET:
|
||||
|
|
64
src/reducers/draft-statuses.ts
Normal file
64
src/reducers/draft-statuses.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
import { COMPOSE_SUBMIT_SUCCESS } from 'soapbox/actions/compose';
|
||||
import { DRAFT_STATUSES_FETCH_SUCCESS, PERSIST_DRAFT_STATUS, CANCEL_DRAFT_STATUS } from 'soapbox/actions/draft-statuses';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { StatusVisibility } from 'soapbox/normalizers/status';
|
||||
import type { APIEntity, Attachment as AttachmentEntity } from 'soapbox/types/entities';
|
||||
|
||||
const DraftStatusRecord = ImmutableRecord({
|
||||
content_type: 'text/plain',
|
||||
draft_id: '',
|
||||
editorState: null as string | null,
|
||||
group_id: null as string | null,
|
||||
in_reply_to: null as string | null,
|
||||
media_attachments: ImmutableList<AttachmentEntity>(),
|
||||
poll: null as ImmutableMap<string, any> | null,
|
||||
privacy: 'public' as StatusVisibility,
|
||||
quote: null as string | null,
|
||||
schedule: null as Date | null,
|
||||
sensitive: false,
|
||||
spoiler: false,
|
||||
spoiler_text: '',
|
||||
text: '',
|
||||
to: ImmutableOrderedSet<string>(),
|
||||
});
|
||||
|
||||
export type DraftStatus = ReturnType<typeof DraftStatusRecord>;
|
||||
type State = ImmutableMap<string, DraftStatus>;
|
||||
|
||||
const initialState: State = ImmutableMap();
|
||||
|
||||
const importStatus = (state: State, status: APIEntity) => {
|
||||
return state.set(status.draft_id, DraftStatusRecord(ImmutableMap(fromJS(status))));
|
||||
};
|
||||
|
||||
const importStatuses = (state: State, statuses: APIEntity[]) =>
|
||||
state.withMutations(mutable => Object.values(statuses || {}).forEach(status => importStatus(mutable, status)));
|
||||
|
||||
const deleteStatus = (state: State, id: string) => {
|
||||
if (id) return state.delete(id);
|
||||
return state;
|
||||
};
|
||||
|
||||
const persistState = (state: State, accountUrl: string) => {
|
||||
KVStore.setItem(`drafts:${accountUrl}`, state.toJS());
|
||||
return state;
|
||||
};
|
||||
|
||||
export default function scheduled_statuses(state: State = initialState, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case DRAFT_STATUSES_FETCH_SUCCESS:
|
||||
return importStatuses(state, action.statuses);
|
||||
case PERSIST_DRAFT_STATUS:
|
||||
return persistState(importStatus(state, action.status), action.accountUrl);
|
||||
case CANCEL_DRAFT_STATUS:
|
||||
return persistState(deleteStatus(state, action.id), action.accountUrl);
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
return persistState(deleteStatus(state, action.draftId), action.accountUrl);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import contexts from './contexts';
|
|||
import conversations from './conversations';
|
||||
import custom_emojis from './custom-emojis';
|
||||
import domain_lists from './domain-lists';
|
||||
import draft_statuses from './draft-statuses';
|
||||
import dropdown_menu from './dropdown-menu';
|
||||
import filters from './filters';
|
||||
import followed_tags from './followed-tags';
|
||||
|
@ -84,6 +85,7 @@ const reducers = {
|
|||
conversations,
|
||||
custom_emojis,
|
||||
domain_lists,
|
||||
draft_statuses,
|
||||
dropdown_menu,
|
||||
entities,
|
||||
filters,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
STATUS_CREATE_FAIL,
|
||||
STATUS_CREATE_REQUEST,
|
||||
STATUS_CREATE_SUCCESS,
|
||||
} from 'soapbox/actions/statuses';
|
||||
|
@ -36,6 +37,7 @@ export default function pending_statuses(state = initialState, action: AnyAction
|
|||
switch (action.type) {
|
||||
case STATUS_CREATE_REQUEST:
|
||||
return action.editing ? state : importStatus(state, ImmutableMap(fromJS(action.params)), action.idempotencyKey);
|
||||
case STATUS_CREATE_FAIL:
|
||||
case STATUS_CREATE_SUCCESS:
|
||||
return deleteStatus(state, action.idempotencyKey);
|
||||
default:
|
||||
|
|
Loading…
Reference in a new issue