From 626a037b4bfb23ba3cb4814ac2d79c91b845232f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Thu, 10 Apr 2025 22:29:30 +0200 Subject: [PATCH] pl-fe: compose previews cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- packages/pl-api/lib/client.ts | 23 +++++++- packages/pl-api/lib/entities/status.ts | 8 ++- packages/pl-api/package.json | 2 +- packages/pl-fe/package.json | 2 +- packages/pl-fe/src/actions/compose.ts | 22 ++++--- .../pl-fe/src/components/status-content.tsx | 2 +- .../pl-fe/src/components/ui/button/index.tsx | 40 +++---------- .../components/ui/button/useButtonStyles.ts | 26 ++------- .../compose/components/compose-form.tsx | 57 ++++++++++++++++++- .../containers/preview-compose-container.tsx | 17 ++++-- packages/pl-fe/src/locales/en.json | 4 ++ packages/pl-fe/src/reducers/compose.ts | 2 +- packages/pl-fe/yarn.lock | 8 +-- 13 files changed, 134 insertions(+), 79 deletions(-) diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index 02241f574..fb28c4306 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -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. diff --git a/packages/pl-api/lib/entities/status.ts b/packages/pl-api/lib/entities/status.ts index aa41ddbc2..fd2fde2e4 100644 --- a/packages/pl-api/lib/entities/status.ts +++ b/packages/pl-api/lib/entities/status.ts @@ -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 & { quote: Status | null; } -export { statusSchema, statusWithoutAccountSchema, type Status, type StatusWithoutAccount }; +export { statusSchema, statusWithoutAccountSchema, partialStatusSchema, type Status, type StatusWithoutAccount }; diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 2bcbf3fa7..734e629e4 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -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": { diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index ce94a7e07..a16e8369a 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -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", diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index e05a697dc..d1e8e415c 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -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) => ({ type: COMPOSE_PREVIEW_SUCCESS, composeId, status, diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index 5ad32d5a4..c2f5b961b 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -96,7 +96,7 @@ const StatusContent: React.FC = 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 => { diff --git a/packages/pl-fe/src/components/ui/button/index.tsx b/packages/pl-fe/src/components/ui/button/index.tsx index 9849d8ae9..501b3a4f9 100644 --- a/packages/pl-fe/src/components/ui/button/index.tsx +++ b/packages/pl-fe/src/components/ui/button/index.tsx @@ -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(({ 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(({ const renderButton = () => ( ); - let button = renderButton(); - if (to) { - button = ( + return ( - {button} + {renderButton()} ); } if (href) { - button = ( + return ( - {button} + {renderButton()} ); } - if (actionsMenu?.length) { - button = ( -
- {button} - -
- - - - -
- ); - } - - return button; + return renderButton(); }); export { Button as default }; diff --git a/packages/pl-fe/src/components/ui/button/useButtonStyles.ts b/packages/pl-fe/src/components/ui/button/useButtonStyles.ts index a02b5fec9..86eb53ed9 100644 --- a/packages/pl-fe/src/components/ui/button/useButtonStyles.ts +++ b/packages/pl-fe/src/components/ui/button/useButtonStyles.ts @@ -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 }; diff --git a/packages/pl-fe/src/features/compose/components/compose-form.tsx b/packages/pl-fe/src/features/compose/components/compose-form.tsx index 704565dad..0e0be9432 100644 --- a/packages/pl-fe/src/features/compose/components/compose-form.tsx +++ b/packages/pl-fe/src/features/compose/components/compose-form.tsx @@ -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 = ({ 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 = ( + + ); + + if (actionsMenu) { + return ( +
+ {button} + + + +
+ ); + } + + return button; +}; + interface IComposeForm { id: ID extends 'default' ? never : ID; shouldCondense?: boolean; @@ -349,7 +402,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab )} -
diff --git a/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx b/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx index 9c052c54c..c0312c595 100644 --- a/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx @@ -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 = ({ 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 ( @@ -66,7 +71,7 @@ const PreviewComposeContainer: React.FC = ({ composeId } /> = ({ composeId } {status.quote_id && } - {status.media_attachments.length > 0 && ( + {status.media_attachments?.length > 0 && (
diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 0d904dd86..e08e84620 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -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", diff --git a/packages/pl-fe/src/reducers/compose.ts b/packages/pl-fe/src/reducers/compose.ts index 55c000d5d..1f47fef8f 100644 --- a/packages/pl-fe/src/reducers/compose.ts +++ b/packages/pl-fe/src/reducers/compose.ts @@ -141,7 +141,7 @@ interface Compose { interactionPolicy: InteractionPolicy | null; dismissed_clear_links_suggestions: Array; clear_link_suggestion: ClearLinkSuggestion | null; - preview: BaseStatus | null; + preview: Partial | null; } const newCompose = (params: Partial = {}): Compose => ({ diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index d818a8963..253335879 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -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"