pl-fe: post previews
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -35,6 +35,8 @@ const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const;
|
||||
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const;
|
||||
const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const;
|
||||
const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const;
|
||||
const COMPOSE_PREVIEW_SUCCESS = 'COMPOSE_PREVIEW_SUCCESS' as const;
|
||||
const COMPOSE_PREVIEW_CANCEL = 'COMPOSE_PREVIEW_CANCEL' as const;
|
||||
const COMPOSE_REPLY = 'COMPOSE_REPLY' as const;
|
||||
const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const;
|
||||
const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const;
|
||||
@ -341,7 +343,7 @@ interface SubmitComposeOpts {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||
const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}, preview = false) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { history, force = false, onSuccess } = opts;
|
||||
|
||||
@ -357,23 +359,25 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||
const { forceImplicitAddressing } = useSettingsStore.getState().settings;
|
||||
const explicitAddressing = state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing;
|
||||
|
||||
if (!validateSchedule(state, composeId)) {
|
||||
toast.error(messages.scheduleError);
|
||||
return;
|
||||
}
|
||||
if (!preview) {
|
||||
if (!validateSchedule(state, composeId)) {
|
||||
toast.error(messages.scheduleError);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!status || !status.length) && media.length === 0) {
|
||||
return;
|
||||
}
|
||||
if ((!status || !status.length) && media.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && needsDescriptions(state, composeId)) {
|
||||
useModalsStore.getState().openModal('MISSING_DESCRIPTION', {
|
||||
onContinue: () => {
|
||||
useModalsStore.getState().closeModal('MISSING_DESCRIPTION');
|
||||
dispatch(submitCompose(composeId, { history, force: true, onSuccess }));
|
||||
},
|
||||
});
|
||||
return;
|
||||
if (!force && needsDescriptions(state, composeId)) {
|
||||
useModalsStore.getState().openModal('MISSING_DESCRIPTION', {
|
||||
onContinue: () => {
|
||||
useModalsStore.getState().closeModal('MISSING_DESCRIPTION');
|
||||
dispatch(submitCompose(composeId, { history, force: true, onSuccess }));
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/30007882 for domain regex
|
||||
@ -383,12 +387,15 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||
to = [...new Set([...to, ...mentions.map(mention => mention.replace(/ /g, '').trim().slice(1))])];
|
||||
}
|
||||
|
||||
dispatch(submitComposeRequest(composeId));
|
||||
useModalsStore.getState().closeModal('COMPOSE');
|
||||
if (!preview) {
|
||||
dispatch(submitComposeRequest(composeId));
|
||||
|
||||
if (compose.language && !statusId) {
|
||||
useSettingsStore.getState().rememberLanguageUse(compose.language);
|
||||
dispatch(saveSettings());
|
||||
useModalsStore.getState().closeModal('COMPOSE');
|
||||
|
||||
if (compose.language && !statusId && !preview) {
|
||||
useSettingsStore.getState().rememberLanguageUse(compose.language);
|
||||
dispatch(saveSettings());
|
||||
}
|
||||
}
|
||||
|
||||
const idempotencyKey = compose.idempotencyKey;
|
||||
@ -403,11 +410,12 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||
spoiler_text: compose.spoiler_text,
|
||||
visibility: compose.privacy,
|
||||
content_type: contentType,
|
||||
scheduled_at: compose.schedule?.toISOString(),
|
||||
scheduled_at: preview ? undefined : compose.schedule?.toISOString(),
|
||||
language: compose.language || compose.suggested_language || undefined,
|
||||
to: explicitAddressing && to.length ? to : undefined,
|
||||
local_only: !compose.federated,
|
||||
interaction_policy: ['public', 'unlisted', 'private'].includes(compose.privacy) && compose.interactionPolicy || undefined,
|
||||
preview,
|
||||
};
|
||||
|
||||
if (compose.poll) {
|
||||
@ -440,7 +448,8 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||
}
|
||||
|
||||
return dispatch(createStatus(params, idempotencyKey, statusId)).then((data) => {
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
if (!preview) handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
else if (data.scheduled_at === null) dispatch(previewComposeSuccess(composeId, data));
|
||||
onSuccess?.();
|
||||
}).catch((error) => {
|
||||
dispatch(submitComposeFail(composeId, error));
|
||||
@ -466,6 +475,17 @@ const submitComposeFail = (composeId: string, error: unknown) => ({
|
||||
error,
|
||||
});
|
||||
|
||||
const previewComposeSuccess = (composeId: string, status: BaseStatus) => ({
|
||||
type: COMPOSE_PREVIEW_SUCCESS,
|
||||
composeId,
|
||||
status,
|
||||
});
|
||||
|
||||
const cancelPreviewCompose = (composeId: string) => ({
|
||||
type: COMPOSE_PREVIEW_CANCEL,
|
||||
composeId,
|
||||
});
|
||||
|
||||
const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
@ -971,6 +991,8 @@ type ComposeAction =
|
||||
| ReturnType<typeof submitComposeRequest>
|
||||
| ReturnType<typeof submitComposeSuccess>
|
||||
| ReturnType<typeof submitComposeFail>
|
||||
| ReturnType<typeof previewComposeSuccess>
|
||||
| ReturnType<typeof cancelPreviewCompose>
|
||||
| ReturnType<typeof changeUploadComposeRequest>
|
||||
| ReturnType<typeof changeUploadComposeSuccess>
|
||||
| ReturnType<typeof changeUploadComposeFail>
|
||||
@ -1019,6 +1041,8 @@ export {
|
||||
COMPOSE_SUBMIT_REQUEST,
|
||||
COMPOSE_SUBMIT_SUCCESS,
|
||||
COMPOSE_SUBMIT_FAIL,
|
||||
COMPOSE_PREVIEW_SUCCESS,
|
||||
COMPOSE_PREVIEW_CANCEL,
|
||||
COMPOSE_REPLY,
|
||||
COMPOSE_REPLY_CANCEL,
|
||||
COMPOSE_EVENT_REPLY,
|
||||
@ -1118,6 +1142,7 @@ export {
|
||||
changeComposeInteractionPolicyOption,
|
||||
suggestClearLink,
|
||||
ignoreClearLinkSuggestion,
|
||||
cancelPreviewCompose,
|
||||
type ComposeReplyAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
type ComposeAction,
|
||||
|
||||
@ -38,10 +38,12 @@ const STATUS_UNFILTER = 'STATUS_UNFILTER' as const;
|
||||
|
||||
const createStatus = (params: CreateStatusParams, idempotencyKey: string, statusId: string | null) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch<StatusesAction>({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId });
|
||||
if (!params.preview) dispatch<StatusesAction>({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId });
|
||||
|
||||
return (statusId === null ? getClient(getState()).statuses.createStatus(params) : getClient(getState()).statuses.editStatus(statusId, params))
|
||||
.then((status) => {
|
||||
if (params.preview) return status;
|
||||
|
||||
// The backend might still be processing the rich media attachment
|
||||
const expectsCard = status.scheduled_at === null && !status.card && shouldHaveCard(status);
|
||||
|
||||
|
||||
@ -216,10 +216,9 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
> = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (onShiftClick && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
onShiftClick(event);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2,11 +2,14 @@ import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import DropdownMenu from 'pl-fe/components/dropdown-menu';
|
||||
|
||||
import Icon from '../icon';
|
||||
|
||||
import { useButtonStyles } from './useButtonStyles';
|
||||
|
||||
import type { ButtonSizes, ButtonThemes } from './useButtonStyles';
|
||||
import type { Menu } from 'pl-fe/components/dropdown-menu';
|
||||
|
||||
interface IButton extends Pick<
|
||||
React.ComponentProps<'button'>,
|
||||
@ -30,6 +33,8 @@ interface IButton extends Pick<
|
||||
href?: string;
|
||||
/** Styles the button visually with a predefined theme. */
|
||||
theme?: ButtonThemes;
|
||||
/** Menu items to display as a secondary action. */
|
||||
actionsMenu?: Menu;
|
||||
}
|
||||
|
||||
/** Customizable button element with various themes. */
|
||||
@ -48,11 +53,12 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>(({
|
||||
href,
|
||||
type = 'button',
|
||||
className,
|
||||
actionsMenu,
|
||||
...props
|
||||
}, ref): JSX.Element => {
|
||||
const body = text || children;
|
||||
|
||||
const themeClass = useButtonStyles({
|
||||
const { innerStyle, outerStyle } = useButtonStyles({
|
||||
theme,
|
||||
block,
|
||||
disabled,
|
||||
@ -68,7 +74,11 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>(({
|
||||
const renderButton = () => (
|
||||
<button
|
||||
{...props}
|
||||
className={clsx('rtl:space-x-reverse', themeClass, className)}
|
||||
className={clsx('rtl:space-x-reverse', {
|
||||
[outerStyle]: !actionsMenu,
|
||||
[innerStyle]: true,
|
||||
[className || '']: true,
|
||||
})}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
ref={ref}
|
||||
@ -85,23 +95,39 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>(({
|
||||
</button>
|
||||
);
|
||||
|
||||
let button = renderButton();
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
button = (
|
||||
<Link to={to} tabIndex={-1} className='inline-flex'>
|
||||
{renderButton()}
|
||||
{button}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
button = (
|
||||
<a href={href} target='_blank' rel='noopener' tabIndex={-1} className='inline-flex'>
|
||||
{renderButton()}
|
||||
{button}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return renderButton();
|
||||
if (actionsMenu?.length) {
|
||||
button = (
|
||||
<div className={outerStyle}>
|
||||
{button}
|
||||
|
||||
<div className='h-5 w-px bg-gray-200/50' />
|
||||
|
||||
<DropdownMenu items={actionsMenu} placement='bottom'>
|
||||
<Icon src={require('@tabler/icons/filled/caret-down.svg')} className='size-4' />
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
});
|
||||
|
||||
export { Button as default };
|
||||
|
||||
@ -14,11 +14,18 @@ const themes = {
|
||||
muted: 'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-800 dark:text-gray-100 focus:ring-primary-500',
|
||||
};
|
||||
|
||||
const gaps = {
|
||||
xs: 'gap-x-1.5',
|
||||
sm: 'gap-x-2',
|
||||
md: 'gap-x-2',
|
||||
lg: 'gap-x-2',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
xs: 'gap-x-1.5 px-2 py-1 text-xs',
|
||||
sm: 'gap-x-2 px-3 py-1.5 text-xs leading-4',
|
||||
md: 'gap-x-2 px-4 py-2 text-sm',
|
||||
lg: 'gap-x-2 px-6 py-3 text-base',
|
||||
xs: 'px-2 py-1 text-xs',
|
||||
sm: 'px-3 py-1.5 text-xs leading-4',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base',
|
||||
};
|
||||
|
||||
type ButtonSizes = keyof typeof sizes
|
||||
@ -38,15 +45,22 @@ const useButtonStyles = ({
|
||||
disabled,
|
||||
size,
|
||||
}: IButtonStyles) => {
|
||||
const buttonStyle = clsx({
|
||||
const outerStyle = clsx({
|
||||
'inline-flex items-center place-content-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true,
|
||||
'select-none disabled:opacity-75 disabled:cursor-default': disabled,
|
||||
[`${themes[theme]}`]: true,
|
||||
[`${sizes[size]}`]: true,
|
||||
[`${gaps[size]}`]: true,
|
||||
'flex w-full justify-center': block,
|
||||
});
|
||||
|
||||
return buttonStyle;
|
||||
const innerStyle = clsx({
|
||||
'inline-flex items-center': true,
|
||||
[`${gaps[size]}`]: true,
|
||||
'flex w-full justify-center': block,
|
||||
});
|
||||
|
||||
return { innerStyle, outerStyle };
|
||||
};
|
||||
|
||||
export { useButtonStyles, ButtonSizes, ButtonThemes };
|
||||
|
||||
@ -25,11 +25,12 @@ interface IIcon extends Pick<React.SVGAttributes<SVGAElement>, 'strokeWidth'> {
|
||||
}
|
||||
|
||||
/** Renders and SVG icon with optional counter. */
|
||||
const Icon: React.FC<IIcon> = ({ src, alt, count, size, countMax, containerClassName, title, ...filteredProps }): JSX.Element => (
|
||||
const Icon: React.FC<IIcon> = React.forwardRef<HTMLDivElement, IIcon>(({ src, alt, count, size, countMax, containerClassName, title, ...filteredProps }, ref): JSX.Element => (
|
||||
<div
|
||||
className={clsx('relative flex shrink-0 flex-col', containerClassName)}
|
||||
data-testid={filteredProps['data-testid'] || 'icon'}
|
||||
title={title}
|
||||
ref={ref}
|
||||
>
|
||||
{count ? (
|
||||
<span className='absolute -right-3 -top-2 flex h-5 min-w-[20px] shrink-0 items-center justify-center whitespace-nowrap break-words'>
|
||||
@ -39,6 +40,6 @@ const Icon: React.FC<IIcon> = ({ src, alt, count, size, countMax, containerClass
|
||||
|
||||
<SvgIcon src={src} size={size} alt={alt} {...filteredProps} />
|
||||
</div>
|
||||
);
|
||||
));
|
||||
|
||||
export { Icon as default };
|
||||
|
||||
@ -25,6 +25,7 @@ import { useDraggedFiles } from 'pl-fe/hooks/use-dragged-files';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useInstance } from 'pl-fe/hooks/use-instance';
|
||||
|
||||
import PreviewComposeContainer from '../containers/preview-compose-container';
|
||||
import QuotedStatusContainer from '../containers/quoted-status-container';
|
||||
import ReplyIndicatorContainer from '../containers/reply-indicator-container';
|
||||
import UploadButtonContainer from '../containers/upload-button-container';
|
||||
@ -52,6 +53,7 @@ import Warning from './warning';
|
||||
|
||||
import type { LinkNode } from '@lexical/link';
|
||||
import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
||||
import type { Menu } from 'pl-fe/components/dropdown-menu';
|
||||
import type { Emoji } from 'pl-fe/features/emoji';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -63,6 +65,7 @@ const messages = defineMessages({
|
||||
message: { id: 'compose_form.message', defaultMessage: 'Message' },
|
||||
schedule: { id: 'compose_form.schedule', defaultMessage: 'Schedule' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
preview: { id: 'compose_form.preview', defaultMessage: 'Preview post' },
|
||||
});
|
||||
|
||||
interface IComposeForm<ID extends string> {
|
||||
@ -151,6 +154,12 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
} }));
|
||||
};
|
||||
|
||||
const handlePreview = (e?: React.FormEvent<Element>) => {
|
||||
e?.preventDefault();
|
||||
|
||||
dispatch(submitCompose(id, { history }, true));
|
||||
};
|
||||
|
||||
const onSuggestionsClearRequested = () => {
|
||||
dispatch(clearComposeSuggestions(id));
|
||||
};
|
||||
@ -253,6 +262,14 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
if (features.richText) selectButtons.push(<ContentTypeButton key='compose-type-button' composeId={id} />);
|
||||
if (features.postLanguages) selectButtons.push(<LanguageDropdown key='language-dropdown' composeId={id} />);
|
||||
|
||||
const actionsMenu: Menu | undefined = features.createStatusPreview ? [
|
||||
{
|
||||
text: intl.formatMessage(messages.preview),
|
||||
action: handlePreview,
|
||||
icon: require('@tabler/icons/outline/eye.svg'),
|
||||
},
|
||||
] : undefined;
|
||||
|
||||
return (
|
||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||
{!!compose.in_reply_to && compose.approvalRequired && (
|
||||
@ -314,6 +331,8 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
|
||||
<QuotedStatusContainer composeId={id} />
|
||||
|
||||
<PreviewComposeContainer composeId={id} />
|
||||
|
||||
<div
|
||||
className={clsx('flex flex-wrap items-center justify-between', {
|
||||
'hidden': condensed,
|
||||
@ -330,7 +349,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<Button type='submit' theme='primary' icon={publishIcon} text={publishText} disabled={!canSubmit} />
|
||||
<Button type='submit' theme='primary' icon={publishIcon} text={publishText} disabled={!canSubmit} actionsMenu={actionsMenu} />
|
||||
</HStack>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { cancelPreviewCompose } from 'pl-fe/actions/compose';
|
||||
import EventPreview from 'pl-fe/components/event-preview';
|
||||
import OutlineBox from 'pl-fe/components/outline-box';
|
||||
import QuotedStatusIndicator from 'pl-fe/components/quoted-status-indicator';
|
||||
import StatusContent from 'pl-fe/components/status-content';
|
||||
import StatusMedia from 'pl-fe/components/status-media';
|
||||
import StatusReplyMentions from 'pl-fe/components/status-reply-mentions';
|
||||
import SensitiveContentOverlay from 'pl-fe/components/statuses/sensitive-content-overlay';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import IconButton from 'pl-fe/components/ui/icon-button';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useCompose } from 'pl-fe/hooks/use-compose';
|
||||
|
||||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: {
|
||||
id: 'compose_form.preview.close',
|
||||
defaultMessage: 'Hide preview',
|
||||
},
|
||||
});
|
||||
|
||||
interface IQuotedStatusContainer {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
/** Previewed status shown in post composer. */
|
||||
const PreviewComposeContainer: React.FC<IQuotedStatusContainer> = ({ composeId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const status = useCompose(composeId).preview as unknown as Status;
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(cancelPreviewCompose(composeId));
|
||||
};
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = status.account;
|
||||
|
||||
return (
|
||||
<OutlineBox>
|
||||
<Stack space={2}>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Icon className='size-4 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/eye.svg')} />
|
||||
<Text theme='muted' size='sm' className='grow'>
|
||||
<FormattedMessage id='compose_form.preview_label' defaultMessage='Preview' />
|
||||
</Text>
|
||||
|
||||
<IconButton
|
||||
src={require('@tabler/icons/outline/x.svg')}
|
||||
title={intl.formatMessage(messages.close)}
|
||||
onClick={handleClose}
|
||||
className='bg-transparent text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
|
||||
iconClassName='h-4 w-4'
|
||||
/>
|
||||
</HStack>
|
||||
<AccountContainer
|
||||
id={account.id}
|
||||
timestamp={status.created_at}
|
||||
withRelationship={false}
|
||||
showAccountHoverCard={false}
|
||||
withLinkToProfile={!false}
|
||||
/>
|
||||
|
||||
<StatusReplyMentions status={status} hoverable={false} />
|
||||
|
||||
{status.event ? <EventPreview status={status} hideAction /> : (
|
||||
<Stack className='relative z-0'>
|
||||
<Stack space={4}>
|
||||
<StatusContent status={status} isQuote />
|
||||
|
||||
{status.quote_id && <QuotedStatusIndicator statusId={status.quote_id} />}
|
||||
|
||||
{status.media_attachments.length > 0 && (
|
||||
<div className='relative'>
|
||||
<SensitiveContentOverlay status={status} />
|
||||
<StatusMedia status={status} muted />
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</OutlineBox>
|
||||
);
|
||||
};
|
||||
|
||||
export { PreviewComposeContainer as default };
|
||||
@ -58,6 +58,8 @@ import {
|
||||
COMPOSE_INTERACTION_POLICY_OPTION_CHANGE,
|
||||
COMPOSE_CLEAR_LINK_SUGGESTION_CREATE,
|
||||
COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE,
|
||||
COMPOSE_PREVIEW_SUCCESS,
|
||||
COMPOSE_PREVIEW_CANCEL,
|
||||
type ComposeAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
} from '../actions/compose';
|
||||
@ -67,7 +69,7 @@ import { FE_NAME } from '../actions/settings';
|
||||
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
|
||||
import { unescapeHTML } from '../utils/html';
|
||||
|
||||
import type { InteractionPolicy, CredentialAccount, Instance, MediaAttachment, Tag } from 'pl-api';
|
||||
import type { CredentialAccount, Instance, InteractionPolicy, MediaAttachment, Status as BaseStatus, Tag } from 'pl-api';
|
||||
import type { Emoji } from 'pl-fe/features/emoji';
|
||||
import type { Language } from 'pl-fe/features/preferences';
|
||||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
@ -139,6 +141,7 @@ interface Compose {
|
||||
interactionPolicy: InteractionPolicy | null;
|
||||
dismissed_clear_links_suggestions: Array<string>;
|
||||
clear_link_suggestion: ClearLinkSuggestion | null;
|
||||
preview: BaseStatus | null;
|
||||
}
|
||||
|
||||
const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
@ -182,6 +185,7 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
interactionPolicy: null,
|
||||
dismissed_clear_links_suggestions: [],
|
||||
clear_link_suggestion: null,
|
||||
preview: null,
|
||||
...params,
|
||||
});
|
||||
|
||||
@ -718,6 +722,14 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
||||
}
|
||||
compose.dismissed_clear_links_suggestions.push(action.key);
|
||||
});
|
||||
case COMPOSE_PREVIEW_SUCCESS:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.preview = action.status;
|
||||
});
|
||||
case COMPOSE_PREVIEW_CANCEL:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.preview = null;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user