pl-fe: compose previews cleanup
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@@ -57,6 +57,7 @@ import {
|
||||
notificationRequestSchema,
|
||||
notificationSchema,
|
||||
oauthTokenSchema,
|
||||
partialStatusSchema,
|
||||
pleromaConfigSchema,
|
||||
pollSchema,
|
||||
relationshipSchema,
|
||||
@@ -84,7 +85,7 @@ import { circleSchema } from './entities/circle';
|
||||
import { type GroupedNotificationsResults, groupedNotificationsResultsSchema, type NotificationGroup } from './entities/grouped-notifications-results';
|
||||
import { ShoutMessage, shoutMessageSchema } from './entities/shout-message';
|
||||
import { filteredArray } from './entities/utils';
|
||||
import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, MITRA, PIXELFED } from './features';
|
||||
import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, MITRA, PIXELFED, PLEROMA } from './features';
|
||||
import request, { getNextLink, getPrevLink, type RequestBody, type RequestMeta } from './request';
|
||||
import { buildFullPath } from './utils/url';
|
||||
|
||||
@@ -2166,6 +2167,26 @@ class PlApiClient {
|
||||
return v.parse(statusSchema, response.json);
|
||||
},
|
||||
|
||||
/**
|
||||
* Requires features{@link Features['createStatusPreview']}.
|
||||
*/
|
||||
previewStatus: async (params: CreateStatusParams) => {
|
||||
const input = this.features.version.software === PLEROMA || this.features.version.software === AKKOMA
|
||||
? '/api/v1/statuses'
|
||||
: '/api/v1/statuses/preview';
|
||||
|
||||
if (this.features.version.software === PLEROMA || this.features.version.software === AKKOMA) {
|
||||
params.preview = true;
|
||||
}
|
||||
|
||||
const response = await this.request(input, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
});
|
||||
|
||||
return v.parse(v.partial(partialStatusSchema), response.json);
|
||||
},
|
||||
|
||||
/**
|
||||
* View a single status
|
||||
* Obtain information about a status.
|
||||
|
||||
@@ -166,6 +166,12 @@ const statusWithoutAccountSchema = v.pipe(v.any(), v.transform(preprocess), v.ob
|
||||
quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
|
||||
}));
|
||||
|
||||
const partialStatusSchema = v.partial(v.object({
|
||||
...baseStatusSchema.entries,
|
||||
reblog: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
|
||||
quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
|
||||
}));
|
||||
|
||||
/**
|
||||
* @category Entity types
|
||||
*/
|
||||
@@ -183,4 +189,4 @@ type Status = v.InferOutput<typeof baseStatusSchema> & {
|
||||
quote: Status | null;
|
||||
}
|
||||
|
||||
export { statusSchema, statusWithoutAccountSchema, type Status, type StatusWithoutAccount };
|
||||
export { statusSchema, statusWithoutAccountSchema, partialStatusSchema, type Status, type StatusWithoutAccount };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pl-api",
|
||||
"version": "1.0.0-rc.45",
|
||||
"version": "1.0.0-rc.46",
|
||||
"type": "module",
|
||||
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
|
||||
"repository": {
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"multiselect-react-dropdown": "^2.0.25",
|
||||
"mutative": "^1.1.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pl-api": "^1.0.0-rc.45",
|
||||
"pl-api": "^1.0.0-rc.46",
|
||||
"postcss": "^8.4.49",
|
||||
"process": "^0.11.10",
|
||||
"punycode": "^2.1.1",
|
||||
|
||||
@@ -447,13 +447,19 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}, preview
|
||||
params.group_id = compose.group_id;
|
||||
}
|
||||
|
||||
return dispatch(createStatus(params, idempotencyKey, statusId)).then((data) => {
|
||||
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));
|
||||
});
|
||||
if (preview) {
|
||||
getClient(state).statuses.previewStatus(params).then((data) => {
|
||||
dispatch(previewComposeSuccess(composeId, data));
|
||||
onSuccess?.();
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
return dispatch(createStatus(params, idempotencyKey, statusId)).then((data) => {
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
onSuccess?.();
|
||||
}).catch((error) => {
|
||||
dispatch(submitComposeFail(composeId, error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitComposeRequest = (composeId: string) => ({
|
||||
@@ -475,7 +481,7 @@ const submitComposeFail = (composeId: string, error: unknown) => ({
|
||||
error,
|
||||
});
|
||||
|
||||
const previewComposeSuccess = (composeId: string, status: BaseStatus) => ({
|
||||
const previewComposeSuccess = (composeId: string, status: Partial<BaseStatus>) => ({
|
||||
type: COMPOSE_PREVIEW_SUCCESS,
|
||||
composeId,
|
||||
status,
|
||||
|
||||
@@ -96,7 +96,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
||||
const statusMeta = statusesMeta[status.id] || {};
|
||||
const { data: translation } = useStatusTranslation(status.id, statusMeta.targetLanguage);
|
||||
|
||||
const withSpoiler = status.spoiler_text.length > 0;
|
||||
const withSpoiler = status.spoiler_text?.length > 0;
|
||||
const expanded = !withSpoiler || statusMeta.expanded || false;
|
||||
|
||||
const maybeSetCollapsed = (): void => {
|
||||
|
||||
@@ -2,14 +2,11 @@ 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'>,
|
||||
@@ -33,8 +30,6 @@ 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. */
|
||||
@@ -53,12 +48,11 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>(({
|
||||
href,
|
||||
type = 'button',
|
||||
className,
|
||||
actionsMenu,
|
||||
...props
|
||||
}, ref): JSX.Element => {
|
||||
const body = text || children;
|
||||
|
||||
const { innerStyle, outerStyle } = useButtonStyles({
|
||||
const themeClass = useButtonStyles({
|
||||
theme,
|
||||
block,
|
||||
disabled,
|
||||
@@ -74,11 +68,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>(({
|
||||
const renderButton = () => (
|
||||
<button
|
||||
{...props}
|
||||
className={clsx('rtl:space-x-reverse', {
|
||||
[outerStyle]: !actionsMenu,
|
||||
[innerStyle]: true,
|
||||
[className || '']: true,
|
||||
})}
|
||||
className={clsx('rtl:space-x-reverse', themeClass, className)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
ref={ref}
|
||||
@@ -95,39 +85,23 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>(({
|
||||
</button>
|
||||
);
|
||||
|
||||
let button = renderButton();
|
||||
|
||||
if (to) {
|
||||
button = (
|
||||
return (
|
||||
<Link to={to} tabIndex={-1} className='inline-flex'>
|
||||
{button}
|
||||
{renderButton()}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (href) {
|
||||
button = (
|
||||
return (
|
||||
<a href={href} target='_blank' rel='noopener' tabIndex={-1} className='inline-flex'>
|
||||
{button}
|
||||
{renderButton()}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
return renderButton();
|
||||
});
|
||||
|
||||
export { Button as default };
|
||||
|
||||
@@ -14,18 +14,11 @@ 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: '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',
|
||||
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',
|
||||
};
|
||||
|
||||
type ButtonSizes = keyof typeof sizes
|
||||
@@ -45,22 +38,15 @@ const useButtonStyles = ({
|
||||
disabled,
|
||||
size,
|
||||
}: IButtonStyles) => {
|
||||
const outerStyle = clsx({
|
||||
const buttonStyle = 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,
|
||||
});
|
||||
|
||||
const innerStyle = clsx({
|
||||
'inline-flex items-center': true,
|
||||
[`${gaps[size]}`]: true,
|
||||
'flex w-full justify-center': block,
|
||||
});
|
||||
|
||||
return { innerStyle, outerStyle };
|
||||
return buttonStyle;
|
||||
};
|
||||
|
||||
export { useButtonStyles, ButtonSizes, ButtonThemes };
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
ignoreClearLinkSuggestion,
|
||||
suggestClearLink,
|
||||
} from 'pl-fe/actions/compose';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import DropdownMenu from 'pl-fe/components/dropdown-menu';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import SvgIcon from 'pl-fe/components/ui/svg-icon';
|
||||
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { ComposeEditor } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
@@ -66,8 +68,59 @@ const messages = defineMessages({
|
||||
schedule: { id: 'compose_form.schedule', defaultMessage: 'Schedule' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
preview: { id: 'compose_form.preview', defaultMessage: 'Preview post' },
|
||||
more: { id: 'compose_form.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
interface IComposeButton extends Pick<
|
||||
React.ComponentProps<'button'>,
|
||||
'children' | 'disabled' | 'onClick' | 'onMouseDown' | 'onKeyDown' | 'onKeyPress' | 'title' | 'type'
|
||||
> {
|
||||
/** URL to an SVG icon to render inside the button. */
|
||||
icon?: string;
|
||||
/** Text inside the button. Takes precedence over `children`. */
|
||||
text?: React.ReactNode;
|
||||
/** Menu items to display as a secondary action. */
|
||||
actionsMenu?: Menu;
|
||||
}
|
||||
|
||||
const ComposeButton: React.FC<IComposeButton> = ({ actionsMenu, disabled, icon, text, ...props }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const containerClassName = 'flex items-center gap-px overflow-hidden rounded-full text-sm font-medium text-gray-100';
|
||||
const buttonClassName = 'inline-flex select-none appearance-none border border-transparent bg-primary-500 transition-all hover:bg-primary-400 focus:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-300 focus:ring-offset-2 disabled:cursor-default disabled:opacity-75 dark:hover:bg-primary-600';
|
||||
|
||||
const button = (
|
||||
<button
|
||||
{...props}
|
||||
disabled={disabled}
|
||||
className={clsx({
|
||||
'place-content-center items-center gap-x-2 px-4 py-2 rtl:space-x-reverse': true,
|
||||
[buttonClassName]: true,
|
||||
'pr-2': actionsMenu,
|
||||
[containerClassName]: !actionsMenu,
|
||||
})}
|
||||
>
|
||||
{icon ? <Icon src={icon} className='size-4' /> : null}
|
||||
<span>{text}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (actionsMenu) {
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
{button}
|
||||
<DropdownMenu items={actionsMenu} placement='bottom' disabled={disabled}>
|
||||
<button className={clsx('h-full cursor-pointer py-2.5 pl-1 pr-3', buttonClassName)} title={intl.formatMessage(messages.more)}>
|
||||
<SvgIcon src={require('@tabler/icons/filled/caret-down.svg')} className='size-4' />
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
interface IComposeForm<ID extends string> {
|
||||
id: ID extends 'default' ? never : ID;
|
||||
shouldCondense?: boolean;
|
||||
@@ -349,7 +402,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<Button type='submit' theme='primary' icon={publishIcon} text={publishText} disabled={!canSubmit} actionsMenu={actionsMenu} />
|
||||
<ComposeButton type='submit' icon={publishIcon} text={publishText} disabled={!canSubmit} actionsMenu={actionsMenu} />
|
||||
</HStack>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { cancelPreviewCompose } from 'pl-fe/actions/compose';
|
||||
@@ -17,6 +17,7 @@ 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 { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
|
||||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
|
||||
@@ -35,19 +36,23 @@ interface IQuotedStatusContainer {
|
||||
const PreviewComposeContainer: React.FC<IQuotedStatusContainer> = ({ composeId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const { account: ownAccount } = useOwnAccount();
|
||||
|
||||
const status = useCompose(composeId).preview as unknown as Status;
|
||||
const previewedStatus = useCompose(composeId).preview as unknown as Status;
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(cancelPreviewCompose(composeId));
|
||||
};
|
||||
|
||||
const status = useMemo(() => previewedStatus ? ({
|
||||
...previewedStatus,
|
||||
account: previewedStatus.account || ownAccount,
|
||||
}) : null, [previewedStatus, ownAccount]);
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = status.account;
|
||||
|
||||
return (
|
||||
<OutlineBox>
|
||||
<Stack space={2}>
|
||||
@@ -66,7 +71,7 @@ const PreviewComposeContainer: React.FC<IQuotedStatusContainer> = ({ composeId }
|
||||
/>
|
||||
</HStack>
|
||||
<AccountContainer
|
||||
id={account.id}
|
||||
id={status.account.id}
|
||||
timestamp={status.created_at}
|
||||
withRelationship={false}
|
||||
showAccountHoverCard={false}
|
||||
@@ -82,7 +87,7 @@ const PreviewComposeContainer: React.FC<IQuotedStatusContainer> = ({ composeId }
|
||||
|
||||
{status.quote_id && <QuotedStatusIndicator statusId={status.quote_id} />}
|
||||
|
||||
{status.media_attachments.length > 0 && (
|
||||
{status.media_attachments?.length > 0 && (
|
||||
<div className='relative'>
|
||||
<SensitiveContentOverlay status={status} />
|
||||
<StatusMedia status={status} muted />
|
||||
|
||||
@@ -489,6 +489,7 @@
|
||||
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
|
||||
"compose_form.lock_disclaimer.lock": "locked",
|
||||
"compose_form.message": "Message",
|
||||
"compose_form.more": "More",
|
||||
"compose_form.placeholder": "What's on your mind?",
|
||||
"compose_form.poll.add_option": "Add an answer",
|
||||
"compose_form.poll.duration": "Poll duration",
|
||||
@@ -500,6 +501,9 @@
|
||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple answers",
|
||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single answer",
|
||||
"compose_form.poll_placeholder": "Add a poll topic…",
|
||||
"compose_form.preview": "Preview post",
|
||||
"compose_form.preview.close": "Hide preview",
|
||||
"compose_form.preview_label": "Preview",
|
||||
"compose_form.publish": "Post",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.save_changes": "Save changes",
|
||||
|
||||
@@ -141,7 +141,7 @@ interface Compose {
|
||||
interactionPolicy: InteractionPolicy | null;
|
||||
dismissed_clear_links_suggestions: Array<string>;
|
||||
clear_link_suggestion: ClearLinkSuggestion | null;
|
||||
preview: BaseStatus | null;
|
||||
preview: Partial<BaseStatus> | null;
|
||||
}
|
||||
|
||||
const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
|
||||
@@ -6833,10 +6833,10 @@ pkg-dir@^4.1.0:
|
||||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
pl-api@^1.0.0-rc.45:
|
||||
version "1.0.0-rc.45"
|
||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.45.tgz#6c1986e850ab36ee1ba31248a2b90638bec06170"
|
||||
integrity sha512-NM5QZ9x9sjK3sOxThqO48926Lr+Uw/P911jaLl4CcKEQV+IuLYEdOheYdpeH2Ub5LErcUwxiiriv5Xepx+FEEA==
|
||||
pl-api@^1.0.0-rc.46:
|
||||
version "1.0.0-rc.46"
|
||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.46.tgz#48c350b7c224d0b150075001faf9bfa7238cd37c"
|
||||
integrity sha512-izay+cl5QlxNdPaYwAtnCw9XNBo35tTvRrkIB8FFQ8+yqnvv8usjno8CMLZVslk7Ujjc2wmATesIWLhyWJorhg==
|
||||
dependencies:
|
||||
blurhash "^2.0.5"
|
||||
http-link-header "^1.1.3"
|
||||
|
||||
Reference in New Issue
Block a user