Merge branch 'drafts' into fork
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
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/outline/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;
|
||||
Reference in New Issue
Block a user