diff --git a/app/javascript/mastodon/components/card/card.stories.tsx b/app/javascript/mastodon/components/card/card.stories.tsx index 9ff326254d9..c8dc7983bdd 100644 --- a/app/javascript/mastodon/components/card/card.stories.tsx +++ b/app/javascript/mastodon/components/card/card.stories.tsx @@ -1,9 +1,7 @@ -import { useMemo } from 'react'; - import type { Meta, StoryObj } from '@storybook/react-vite'; import { fn } from 'storybook/test'; -import { Card, CardBody, CardImage, CardTitle } from './index'; +import { Card, CardBody, CardTitle } from './index'; interface StoryProps { avatar: boolean; @@ -42,14 +40,6 @@ const meta = { timestamp, titleText, }) { - const tsString = useMemo( - () => - timestamp - ? new Date(new Date().setMonth(0, 1)).toDateString() - : undefined, - [timestamp], - ); - const body = bodyText.includes('\n') ? bodyText.split('\n').map((line, index) =>

{line}

) : bodyText; @@ -63,12 +53,18 @@ const meta = { : {}; return ( - - + } + > + } + afterContent={timestamp && '1 Jan'} + > {titleText} {body} - {image && } ); }, diff --git a/app/javascript/mastodon/components/card/index.tsx b/app/javascript/mastodon/components/card/index.tsx index 0219c3860c9..ddb87a449d7 100644 --- a/app/javascript/mastodon/components/card/index.tsx +++ b/app/javascript/mastodon/components/card/index.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { createContext, use, useId, useState } from 'react'; +import { createContext, use, useId } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -10,7 +10,6 @@ import { TrashIcon } from '@phosphor-icons/react'; import type { PolymorphicProps } from '@/types/polymorphic'; import { IconButton } from '../button/redesign'; -import { RelativeTimestamp } from '../relative_timestamp'; import classes from './styles.module.scss'; @@ -18,6 +17,7 @@ type CardProps = PolymorphicProps< { children: React.ReactNode; className?: string; + image?: React.ReactNode; onDelete?: React.MouseEventHandler; }, As @@ -25,43 +25,49 @@ type CardProps = PolymorphicProps< interface CardContext { id: string; - registerImage?: (ref: HTMLImageElement) => void; } const CardContext = createContext({ id: '' }); -export const Card = ({ +export const Card = ({ as: asComp, children, className, + image, onDelete, ...props }: CardProps) => { const Comp = asComp ?? 'div'; const id = useId(); - const [hasImage, registerImage] = useState(null); // Disable the button if this is an interactive element, including a Link component. const hideButton = !onDelete || asComp === 'a' || asComp === 'button' || 'to' in props; + const imageComp = + typeof image === 'string' ? ( + + ) : ( + image &&
{image}
+ ); + return ( - - {children} - + {children} + + {imageComp} {!hideButton && ( @@ -72,32 +78,26 @@ export const Card = ({ interface CardTitleProps { children: React.ReactNode; - imageSrc?: string; - imageAlt?: string; - timestamp?: string; + image?: React.ReactNode; + afterContent?: React.ReactNode; } -export const CardTitle: React.FC = ({ - children, - imageSrc, - imageAlt = '', - timestamp, -}) => { +export const CardTitle: React.FC< + CardTitleProps & React.ComponentPropsWithRef<'div'> +> = ({ children, image, afterContent, className, ...props }) => { const { id } = use(CardContext); return ( -
- {imageSrc && ( - {imageAlt} - )} +
+ {image && {image}} {children} - {timestamp && ( + {afterContent && ( // eslint-disable-next-line no-restricted-syntax -- Allow •  •  - + {afterContent} )}
@@ -130,16 +130,3 @@ export const CardBody = ({ ); }; - -export const CardImage: React.FC> = ({ - alt, - ...props -}) => { - const { registerImage } = use(CardContext); - - return ( -
- {alt} -
- ); -}; diff --git a/app/javascript/mastodon/components/card/styles.module.scss b/app/javascript/mastodon/components/card/styles.module.scss index 3259907641d..d22032836b9 100644 --- a/app/javascript/mastodon/components/card/styles.module.scss +++ b/app/javascript/mastodon/components/card/styles.module.scss @@ -3,10 +3,14 @@ .root { border: 0.5px solid var(--color-border-primary); border-radius: var(--radius-md); - display: block; + box-sizing: border-box; + display: flex; + flex-direction: column; + min-height: 90px; overflow: hidden; padding: var(--space-sm); position: relative; + transition: background 200ms ease-in-out; &:has(.image) { column-gap: var(--space-sm); @@ -40,11 +44,17 @@ a.root { a { color: inherit; text-decoration: none; + + &:hover, + &:active { + text-decoration: underline; + } } } .titleImage { border-radius: var(--radius-round); + display: block; height: var(--space-lg); margin-inline-end: var(--space-xs); overflow: hidden; @@ -55,6 +65,7 @@ a.root { @include mixins.type-body-compact; overflow-wrap: break-word; + flex-grow: 1; a { color: var(--color-text-brand); @@ -67,13 +78,25 @@ a.root { } } +// Linked body +a.body { + padding: 0 var(--space-sm) var(--space-sm); + margin: calc(-1 * var(--space-sm)); + margin-top: 0; + + .root:has(&:hover) { + background-color: var(--color-bg-highlight); + } +} + .image { contain: size; grid-column: 2; grid-row: 1 / span 2; margin-block: calc(-1 * var(--space-sm)); - img { + &, + & > img { height: 100%; object-fit: cover; width: 100%; diff --git a/app/javascript/mastodon/features/compose/redesign/attachments.module.scss b/app/javascript/mastodon/features/compose/redesign/attachments.module.scss index 30d413cebee..cb512e0d3f4 100644 --- a/app/javascript/mastodon/features/compose/redesign/attachments.module.scss +++ b/app/javascript/mastodon/features/compose/redesign/attachments.module.scss @@ -165,3 +165,24 @@ .pollDurationSelect { text-align: center; } + +// Quotes + +.quoteBody { + color: inherit; + text-decoration: none; +} + +// If hovering over an account link, underline all account links. +.quoteTitle:has(.quoteAccountLink:hover) { + .quoteAccountLink { + text-decoration: underline; + } +} + +.quoteSpoiler { + background: var(--color-bg-highlight); + border-radius: var(--radius-sm); + margin: var(--space-2xs) 0; + padding: var(--space-xs); +} diff --git a/app/javascript/mastodon/features/compose/redesign/attachments.tsx b/app/javascript/mastodon/features/compose/redesign/attachments.tsx index 72fce9bbb70..89d1771e933 100644 --- a/app/javascript/mastodon/features/compose/redesign/attachments.tsx +++ b/app/javascript/mastodon/features/compose/redesign/attachments.tsx @@ -4,6 +4,7 @@ import { useAppSelector } from '@/mastodon/store'; import classes from './attachments.module.scss'; import { ComposePoll } from './poll'; +import { ComposeQuote } from './quote'; import { selectComposeAttachments, selectComposeHasAttachments, @@ -25,7 +26,7 @@ export const ComposeAttachments: React.FC<{ className?: string }> = ({
{hasPoll && } {hasAttachments && } - {quotedStatusId && } + {quotedStatusId && }
); }; @@ -56,7 +57,3 @@ const ComposeMediaAttachments: React.FC = () => {
); }; - -const ComposeQuotedStatus: React.FC<{ id: string }> = ({ id }) => { - return
Quoting status {id}
; -}; diff --git a/app/javascript/mastodon/features/compose/redesign/quote.tsx b/app/javascript/mastodon/features/compose/redesign/quote.tsx new file mode 100644 index 00000000000..bc4f6065e96 --- /dev/null +++ b/app/javascript/mastodon/features/compose/redesign/quote.tsx @@ -0,0 +1,106 @@ +import type React from 'react'; +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { quoteComposeCancel } from '@/mastodon/actions/compose_typed'; +import { Avatar } from '@/mastodon/components/avatar'; +import { Blurhash } from '@/mastodon/components/blurhash'; +import { Card, CardBody, CardTitle } from '@/mastodon/components/card'; +import { LinkedDisplayName } from '@/mastodon/components/display_name'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { RelativeTimestamp } from '@/mastodon/components/relative_timestamp'; +import type { AccountStatusShape } from '@/mastodon/models/status'; +import { selectAccountStatus } from '@/mastodon/selectors/statuses'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import type { OnElementHandler } from '@/mastodon/utils/html'; + +import classes from './attachments.module.scss'; + +export const ComposeQuote: React.FC<{ id: string }> = ({ id }) => { + const status = useAppSelector((state) => selectAccountStatus(state, id)); + + const dispatch = useAppDispatch(); + const handleDelete = useCallback(() => { + dispatch(quoteComposeCancel()); + }, [dispatch]); + + if (!status) { + return null; + } + + let imageEle: React.ReactNode = null; + const image = status.media_attachments.find(({ type }) => type !== 'unknown'); + if (image) { + imageEle = !status.sensitive ? ( + {image.description} + ) : ( + + ); + } + + const statusTo = `/@${status.account.acct}/${status.id}`; + + return ( + + + } + afterContent={ + + + + } + > + + + + + {!status.spoiler_text ? ( + + ) : ( +
+ +   + {status.spoiler_text} +
+ )} +
+
+ ); +}; + +const ComposeQuoteBody: React.FC<{ + status: AccountStatusShape; +}> = ({ status }) => { + return ( + + ); +}; + +const onStatusLinks: OnElementHandler = (element, { key }, children) => { + if (element instanceof HTMLAnchorElement) { + return {children}; + } + return undefined; +}; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 84535e117d0..8f0f8664015 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -526,6 +526,7 @@ "compose.published.body": "Post published.", "compose.published.open": "Open", "compose.quotable": "Allow others to quote", + "compose.quote.spoiler": "Content:", "compose.rearrange_modal.cancel": "Cancel", "compose.rearrange_modal.drag_cancel": "Dragging was cancelled. Attachment index {item, number} was dropped.", "compose.rearrange_modal.drag_end": "Attachment index {item, number} was moved to index {over, number}.", diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts index 3cafde1d0d7..fd6154424b2 100644 --- a/app/javascript/mastodon/models/status.ts +++ b/app/javascript/mastodon/models/status.ts @@ -86,8 +86,10 @@ export interface StatusShape { replies_count: number; visibility: StatusVisibility; } -export type ExpandedStatusShape = Omit & { +export type AccountStatusShape = Omit & { account: AccountShapeFull; +}; +export type ExpandedStatusShape = Omit & { reblog?: Omit; };