Merge remote-tracking branch 'parent/main' into kbtopic-remove-quote
This commit is contained in:
commit
f3c3ea42c2
301 changed files with 6618 additions and 3070 deletions
|
@ -14,6 +14,8 @@ import {
|
|||
muteAccount,
|
||||
unmuteAccount,
|
||||
followAccountSuccess,
|
||||
unpinAccount,
|
||||
pinAccount,
|
||||
} from 'mastodon/actions/accounts';
|
||||
import { showAlertForError } from 'mastodon/actions/alerts';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
|
@ -64,7 +66,7 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
export const Account: React.FC<{
|
||||
interface AccountProps {
|
||||
size?: number;
|
||||
id: string;
|
||||
hidden?: boolean;
|
||||
|
@ -73,7 +75,10 @@ export const Account: React.FC<{
|
|||
withBio?: boolean;
|
||||
hideButtons?: boolean;
|
||||
children?: ReactNode;
|
||||
}> = ({
|
||||
withMenu?: boolean;
|
||||
}
|
||||
|
||||
export const Account: React.FC<AccountProps> = ({
|
||||
id,
|
||||
size = 46,
|
||||
hidden,
|
||||
|
@ -82,6 +87,7 @@ export const Account: React.FC<{
|
|||
withBio,
|
||||
hideButtons,
|
||||
children,
|
||||
withMenu = true,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { signedIn } = useIdentity();
|
||||
|
@ -132,8 +138,6 @@ export const Account: React.FC<{
|
|||
},
|
||||
];
|
||||
} else if (defaultAction !== 'block') {
|
||||
arr = [];
|
||||
|
||||
if (isRemote && accountUrl) {
|
||||
arr.push({
|
||||
text: intl.formatMessage(messages.openOriginalPage),
|
||||
|
@ -186,6 +190,25 @@ export const Account: React.FC<{
|
|||
text: intl.formatMessage(messages.addToLists),
|
||||
action: handleAddToLists,
|
||||
});
|
||||
|
||||
if (id !== me && (relationship?.following || relationship?.requested)) {
|
||||
const handleEndorseToggle = () => {
|
||||
if (relationship.endorsed) {
|
||||
dispatch(unpinAccount(id));
|
||||
} else {
|
||||
dispatch(pinAccount(id));
|
||||
}
|
||||
};
|
||||
arr.push({
|
||||
text: intl.formatMessage(
|
||||
// Defined in features/account_timeline/components/account_header.tsx
|
||||
relationship.endorsed
|
||||
? { id: 'account.unendorse' }
|
||||
: { id: 'account.endorse' },
|
||||
),
|
||||
action: handleEndorseToggle,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,9 +233,10 @@ export const Account: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
let button: React.ReactNode, dropdown: React.ReactNode;
|
||||
let button: React.ReactNode;
|
||||
let dropdown: React.ReactNode;
|
||||
|
||||
if (menu.length > 0) {
|
||||
if (menu.length > 0 && withMenu) {
|
||||
dropdown = (
|
||||
<Dropdown
|
||||
items={menu}
|
||||
|
@ -268,43 +292,69 @@ export const Account: React.FC<{
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||
<div className='account__wrapper'>
|
||||
<Link
|
||||
className='account__display-name'
|
||||
title={account?.acct}
|
||||
to={`/@${account?.acct}`}
|
||||
data-hover-card-account={id}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
{account ? (
|
||||
<Avatar account={account} size={size} />
|
||||
<div
|
||||
className={classNames('account', {
|
||||
'account--minimal': minimal,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames('account__wrapper', {
|
||||
'account__wrapper--with-bio': account && withBio,
|
||||
})}
|
||||
>
|
||||
<div className='account__info-wrapper'>
|
||||
<Link
|
||||
className='account__display-name'
|
||||
title={account?.acct}
|
||||
to={`/@${account?.acct}`}
|
||||
data-hover-card-account={id}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
{account ? (
|
||||
<Avatar account={account} size={size} />
|
||||
) : (
|
||||
<Skeleton width={size} height={size} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
|
||||
{!minimal && (
|
||||
<div className='account__details'>
|
||||
{account ? (
|
||||
<>
|
||||
<ShortNumber
|
||||
value={account.followers_count}
|
||||
renderer={FollowersCounter}
|
||||
isHide={account.other_settings.hide_followers_count}
|
||||
/>{' '}
|
||||
{verification} {muteTimeRemaining}
|
||||
</>
|
||||
) : (
|
||||
<Skeleton width='7ch' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{account &&
|
||||
withBio &&
|
||||
(account.note.length > 0 ? (
|
||||
<div
|
||||
className='account__note translate'
|
||||
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton width={size} height={size} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
|
||||
{!minimal && (
|
||||
<div className='account__details'>
|
||||
{account ? (
|
||||
<>
|
||||
<ShortNumber
|
||||
value={account.followers_count}
|
||||
renderer={FollowersCounter}
|
||||
isHide={account.other_settings.hide_followers_count}
|
||||
/>{' '}
|
||||
{verification} {muteTimeRemaining}
|
||||
</>
|
||||
) : (
|
||||
<Skeleton width='7ch' />
|
||||
)}
|
||||
<div className='account__note account__note--missing'>
|
||||
<FormattedMessage
|
||||
id='account.no_bio'
|
||||
defaultMessage='No description provided.'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!minimal && children && (
|
||||
<div>
|
||||
|
@ -322,22 +372,6 @@ export const Account: React.FC<{
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{account &&
|
||||
withBio &&
|
||||
(account.note.length > 0 ? (
|
||||
<div
|
||||
className='account__note translate'
|
||||
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
||||
/>
|
||||
) : (
|
||||
<div className='account__note account__note--missing'>
|
||||
<FormattedMessage
|
||||
id='account.no_bio'
|
||||
defaultMessage='No description provided.'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ interface Props {
|
|||
withLink?: boolean;
|
||||
counter?: number | string;
|
||||
counterBorderColor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Avatar: React.FC<Props> = ({
|
||||
|
@ -27,6 +28,7 @@ export const Avatar: React.FC<Props> = ({
|
|||
inline = false,
|
||||
withLink = false,
|
||||
style: styleFromParent,
|
||||
className,
|
||||
counter,
|
||||
counterBorderColor,
|
||||
}) => {
|
||||
|
@ -52,7 +54,7 @@ export const Avatar: React.FC<Props> = ({
|
|||
|
||||
const avatar = (
|
||||
<div
|
||||
className={classNames('account__avatar', {
|
||||
className={classNames(className, 'account__avatar', {
|
||||
'account__avatar--inline': inline,
|
||||
'account__avatar--loading': loading,
|
||||
})}
|
||||
|
|
97
app/javascript/mastodon/components/button/button.stories.tsx
Normal file
97
app/javascript/mastodon/components/button/button.stories.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn, expect } from 'storybook/test';
|
||||
|
||||
import { Button } from '.';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
args: {
|
||||
secondary: false,
|
||||
compact: false,
|
||||
dangerous: false,
|
||||
disabled: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
text: {
|
||||
control: 'text',
|
||||
type: 'string',
|
||||
description:
|
||||
'Alternative way of specifying the button label. Will override `children` if provided.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
control: 'text',
|
||||
table: {
|
||||
type: { summary: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['test'],
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const buttonTest: Story['play'] = async ({ args, canvas, userEvent }) => {
|
||||
await userEvent.click(canvas.getByRole('button'));
|
||||
await expect(args.onClick).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const disabledButtonTest: Story['play'] = async ({
|
||||
args,
|
||||
canvas,
|
||||
userEvent,
|
||||
}) => {
|
||||
await userEvent.click(canvas.getByRole('button'));
|
||||
await expect(args.onClick).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: 'Primary button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
secondary: true,
|
||||
children: 'Secondary button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Compact: Story = {
|
||||
args: {
|
||||
compact: true,
|
||||
children: 'Compact button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Dangerous: Story = {
|
||||
args: {
|
||||
dangerous: true,
|
||||
children: 'Dangerous button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const PrimaryDisabled: Story = {
|
||||
args: {
|
||||
...Primary.args,
|
||||
disabled: true,
|
||||
},
|
||||
play: disabledButtonTest,
|
||||
};
|
||||
|
||||
export const SecondaryDisabled: Story = {
|
||||
args: {
|
||||
...Secondary.args,
|
||||
disabled: true,
|
||||
},
|
||||
play: disabledButtonTest,
|
||||
};
|
|
@ -22,6 +22,10 @@ interface PropsWithText extends BaseProps {
|
|||
|
||||
type Props = PropsWithText | PropsChildren;
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction that doesn't result in navigation.
|
||||
*/
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
type = 'button',
|
||||
onClick,
|
|
@ -9,7 +9,8 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
|||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||
import UnfoldLessIcon from '@/material-icons/400-24px/unfold_less.svg?react';
|
||||
import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react';
|
||||
import type { IconProp } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
|
@ -238,7 +239,10 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||
onClick={handleToggleClick}
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id='sliders' icon={SettingsIcon} />
|
||||
<Icon
|
||||
id='sliders'
|
||||
icon={collapsed ? UnfoldMoreIcon : UnfoldLessIcon}
|
||||
/>
|
||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||
</i>
|
||||
</button>
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import type { ComponentPropsWithRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useId,
|
||||
} from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -11,11 +18,14 @@ import { animated, useSpring } from '@react-spring/web';
|
|||
import { useDrag } from '@use-gesture/react';
|
||||
|
||||
import { expandAccountFeaturedTimeline } from '@/mastodon/actions/timelines';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import StatusContainer from '@/mastodon/containers/status_container';
|
||||
import { usePrevious } from '@/mastodon/hooks/usePrevious';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
|
||||
|
||||
const messages = defineMessages({
|
||||
previous: { id: 'featured_carousel.previous', defaultMessage: 'Previous' },
|
||||
|
@ -31,6 +41,7 @@ export const FeaturedCarousel: React.FC<{
|
|||
tagged?: string;
|
||||
}> = ({ accountId, tagged }) => {
|
||||
const intl = useIntl();
|
||||
const accessibilityId = useId();
|
||||
|
||||
// Load pinned statuses
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -74,6 +85,7 @@ export const FeaturedCarousel: React.FC<{
|
|||
const [currentSlideHeight, setCurrentSlideHeight] = useState(
|
||||
wrapperRef.current?.scrollHeight ?? 0,
|
||||
);
|
||||
const previousSlideHeight = usePrevious(currentSlideHeight);
|
||||
const observerRef = useRef<ResizeObserver>(
|
||||
new ResizeObserver(() => {
|
||||
handleSlideChange(0);
|
||||
|
@ -82,8 +94,10 @@ export const FeaturedCarousel: React.FC<{
|
|||
const wrapperStyles = useSpring({
|
||||
x: `-${slideIndex * 100}%`,
|
||||
height: currentSlideHeight,
|
||||
// Don't animate from zero to the height of the initial slide
|
||||
immediate: !previousSlideHeight,
|
||||
});
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
// Update slide height when the component mounts
|
||||
if (currentSlideHeight === 0) {
|
||||
handleSlideChange(0);
|
||||
|
@ -110,11 +124,15 @@ export const FeaturedCarousel: React.FC<{
|
|||
className='featured-carousel'
|
||||
{...bind()}
|
||||
aria-roledescription='carousel'
|
||||
aria-labelledby='featured-carousel-title'
|
||||
aria-labelledby={`${accessibilityId}-title`}
|
||||
role='region'
|
||||
>
|
||||
<div className='featured-carousel__header'>
|
||||
<h4 className='featured-carousel__title' id='featured-carousel-title'>
|
||||
<h4
|
||||
className='featured-carousel__title'
|
||||
id={`${accessibilityId}-title`}
|
||||
>
|
||||
<Icon id='thumb-tack' icon={PushPinIcon} />
|
||||
<FormattedMessage
|
||||
id='featured_carousel.header'
|
||||
defaultMessage='{count, plural, one {Pinned Post} other {Pinned Posts}}'
|
||||
|
|
|
@ -45,6 +45,19 @@ export const HoverCardAccount = forwardRef<
|
|||
|
||||
const { familiarFollowers } = useFetchFamiliarFollowers({ accountId });
|
||||
|
||||
const relationship = useAppSelector((state) =>
|
||||
accountId ? state.relationships.get(accountId) : undefined,
|
||||
);
|
||||
const isMutual = relationship?.followed_by && relationship.following;
|
||||
const isFollower = relationship?.followed_by;
|
||||
const hasRelationshipLoaded = !!relationship;
|
||||
|
||||
const shouldDisplayFamiliarFollowers =
|
||||
familiarFollowers.length > 0 &&
|
||||
hasRelationshipLoaded &&
|
||||
!isMutual &&
|
||||
!isFollower;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
@ -86,7 +99,7 @@ export const HoverCardAccount = forwardRef<
|
|||
renderer={FollowersCounter}
|
||||
isHide={account.other_settings.hide_followers_count}
|
||||
/>
|
||||
{familiarFollowers.length > 0 && (
|
||||
{shouldDisplayFamiliarFollowers && (
|
||||
<>
|
||||
·
|
||||
<div className='hover-card__familiar-followers'>
|
||||
|
@ -102,6 +115,22 @@ export const HoverCardAccount = forwardRef<
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
{(isMutual || isFollower) && (
|
||||
<>
|
||||
·
|
||||
{isMutual ? (
|
||||
<FormattedMessage
|
||||
id='account.mutual'
|
||||
defaultMessage='You follow each other'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='account.follows_you'
|
||||
defaultMessage='Follows you'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FollowButton accountId={accountId} />
|
||||
|
|
|
@ -28,6 +28,7 @@ interface Props {
|
|||
href?: string;
|
||||
ariaHidden?: boolean;
|
||||
data_id?: string;
|
||||
ariaControls?: string;
|
||||
}
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
||||
|
@ -54,6 +55,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
|||
tabIndex = 0,
|
||||
ariaHidden = false,
|
||||
data_id = undefined,
|
||||
ariaControls,
|
||||
},
|
||||
buttonRef,
|
||||
) => {
|
||||
|
@ -155,6 +157,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
|
|||
aria-label={title}
|
||||
aria-expanded={expanded}
|
||||
aria-hidden={ariaHidden}
|
||||
aria-controls={ariaControls}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -7,7 +7,7 @@ interface Props {
|
|||
id: string;
|
||||
icon: IconProp;
|
||||
count: number;
|
||||
issueBadge: boolean;
|
||||
issueBadge?: boolean;
|
||||
className: string;
|
||||
}
|
||||
export const IconWithBadge: React.FC<Props> = ({
|
||||
|
|
|
@ -14,7 +14,6 @@ import { fetchPoll, vote } from 'mastodon/actions/polls';
|
|||
import { Icon } from 'mastodon/components/icon';
|
||||
import emojify from 'mastodon/features/emoji/emoji';
|
||||
import { useIdentity } from 'mastodon/identity_context';
|
||||
import { reduceMotion } from 'mastodon/initial_state';
|
||||
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
||||
import type * as Model from 'mastodon/models/poll';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
|
@ -265,7 +264,6 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
|
|||
to: {
|
||||
width: `${percent}%`,
|
||||
},
|
||||
immediate: reduceMotion,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -624,11 +624,11 @@ class Status extends ImmutablePureComponent {
|
|||
{...statusContentProps}
|
||||
/>
|
||||
|
||||
{children}
|
||||
|
||||
{media}
|
||||
{hashtagBar}
|
||||
{emojiReactionsBar}
|
||||
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue