Merge remote-tracking branch 'origin/develop' into stillimage-tw

This commit is contained in:
Alex Gleason
2022-11-21 10:59:38 -06:00
30 changed files with 299 additions and 413 deletions

View File

@@ -9,7 +9,6 @@ import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
import RelativeTimestamp from './relative-timestamp';
import StopPropagation from './stop-propagation';
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
import type { Account as AccountEntity } from 'soapbox/types/entities';
@@ -22,6 +21,8 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account }) => {
const history = useHistory();
const handleClick: React.MouseEventHandler = (e) => {
e.stopPropagation();
const timelineUrl = `/timeline/${account.domain}`;
if (!(e.ctrlKey || e.metaKey)) {
history.push(timelineUrl);
@@ -166,100 +167,106 @@ const Account = ({
const LinkEl: any = withLinkToProfile ? Link : 'div';
return (
<StopPropagation>
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
<HStack alignItems={actionAlignment} justifyContent='between'>
<HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}>
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
<HStack alignItems={actionAlignment} justifyContent='between'>
<HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}>
<ProfilePopper
condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
>
<LinkEl
to={`/@${account.acct}`}
title={account.acct}
onClick={(event: React.MouseEvent) => event.stopPropagation()}
>
<Avatar src={account.avatar} size={avatarSize} />
{emoji && (
<Emoji
className='w-5 h-5 absolute -bottom-1.5 -right-1.5'
emoji={emoji}
/>
)}
</LinkEl>
</ProfilePopper>
<div className='flex-grow'>
<ProfilePopper
condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
>
<LinkEl to={`/@${account.acct}`} title={account.acct}>
<Avatar src={account.avatar} size={avatarSize} />
{emoji && (
<Emoji
className='w-5 h-5 absolute -bottom-1.5 -right-1.5'
emoji={emoji}
<LinkEl
to={`/@${account.acct}`}
title={account.acct}
onClick={(event: React.MouseEvent) => event.stopPropagation()}
>
<div className='flex items-center space-x-1 flex-grow' style={style}>
<Text
size='sm'
weight='semibold'
truncate
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
)}
{account.verified && <VerificationBadge />}
</div>
</LinkEl>
</ProfilePopper>
<div className='flex-grow'>
<ProfilePopper
condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
>
<LinkEl to={`/@${account.acct}`} title={account.acct}>
<div className='flex items-center space-x-1 flex-grow' style={style}>
<Text
size='sm'
weight='semibold'
truncate
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
<Stack space={withAccountNote ? 1 : 0}>
<HStack alignItems='center' space={1} style={style}>
<Text theme='muted' size='sm' truncate>@{username}</Text>
{account.verified && <VerificationBadge />}
</div>
</LinkEl>
</ProfilePopper>
<Stack space={withAccountNote ? 1 : 0}>
<HStack alignItems='center' space={1} style={style}>
<Text theme='muted' size='sm' truncate>@{username}</Text>
{account.favicon && (
<InstanceFavicon account={account} />
)}
{(timestamp) ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
{timestampUrl ? (
<Link to={timestampUrl} className='hover:underline'>
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
</Link>
) : (
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
)}
</>
) : null}
{showEdit ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
</>
) : null}
{actionType === 'muting' && account.mute_expires_at ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
</>
) : null}
</HStack>
{withAccountNote && (
<Text
size='sm'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
className='mr-2'
/>
{account.favicon && (
<InstanceFavicon account={account} />
)}
</Stack>
</div>
</HStack>
<div ref={actionRef}>
{withRelationship ? renderAction() : null}
{(timestamp) ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
{timestampUrl ? (
<Link to={timestampUrl} className='hover:underline' onClick={(event) => event.stopPropagation()}>
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
</Link>
) : (
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
)}
</>
) : null}
{showEdit ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
</>
) : null}
{actionType === 'muting' && account.mute_expires_at ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
</>
) : null}
</HStack>
{withAccountNote && (
<Text
size='sm'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
className='mr-2'
/>
)}
</Stack>
</div>
</HStack>
</div>
</StopPropagation>
<div ref={actionRef}>
{withRelationship ? renderAction() : null}
</div>
</HStack>
</div>
);
};

View File

@@ -1,77 +1,77 @@
.status-content p {
[data-markup] p {
@apply mb-4 whitespace-pre-wrap;
}
.status-content p:last-child {
[data-markup] p:last-child {
@apply mb-0;
}
.status-content a {
[data-markup] a {
@apply text-primary-600 dark:text-accent-blue hover:underline;
}
.status-content strong {
[data-markup] strong {
@apply font-bold;
}
.status-content em {
[data-markup] em {
@apply italic;
}
.status-content ul,
.status-content ol {
[data-markup] ul,
[data-markup] ol {
@apply pl-10 mb-4;
}
.status-content ul {
[data-markup] ul {
@apply list-disc list-outside;
}
.status-content ol {
[data-markup] ol {
@apply list-decimal list-outside;
}
.status-content blockquote {
[data-markup] blockquote {
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
}
.status-content code {
[data-markup] code {
@apply cursor-text font-mono;
}
.status-content p > code,
.status-content pre {
[data-markup] p > code,
[data-markup] pre {
@apply bg-gray-100 dark:bg-primary-800;
}
/* Inline code */
.status-content p > code {
[data-markup] p > code {
@apply py-0.5 px-1 rounded-sm;
}
/* Code block */
.status-content pre {
[data-markup] pre {
@apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all;
}
.status-content pre:last-child {
[data-markup] pre:last-child {
@apply mb-0;
}
/* Markdown images */
.status-content img:not(.emojione):not([width][height]) {
[data-markup] img:not(.emojione):not([width][height]) {
@apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block;
}
/* User setting to underline links */
body.underline-links .status-content a {
body.underline-links [data-markup] a {
@apply underline;
}
.status-content .big-emoji img.emojione {
[data-markup] .big-emoji img.emojione {
@apply inline w-9 h-9 p-1;
}
.status-content .status-link {
[data-markup] .status-link {
@apply hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue;
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
import Text, { IText } from './ui/text/text';
import './markup.css';
interface IMarkup extends IText {
}
/** Styles HTML markup returned by the API, such as in account bios and statuses. */
const Markup = React.forwardRef<any, IMarkup>((props, ref) => {
return (
<Text ref={ref} {...props} data-markup />
);
});
export default Markup;

View File

@@ -5,7 +5,6 @@ import { openModal } from 'soapbox/actions/modals';
import { vote } from 'soapbox/actions/polls';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import StopPropagation from '../stop-propagation';
import { Stack, Text } from '../ui';
import PollFooter from './poll-footer';
@@ -65,7 +64,8 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
const showResults = poll.voted || poll.expired;
return (
<StopPropagation>
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div onClick={e => e.stopPropagation()}>
{!showResults && poll.multiple && (
<Text theme='muted' size='sm'>
{intl.formatMessage(messages.multiple)}
@@ -93,7 +93,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
selected={selected}
/>
</Stack>
</StopPropagation>
</div>
);
};

View File

@@ -13,7 +13,6 @@ import OutlineBox from './outline-box';
import StatusContent from './status-content';
import StatusReplyMentions from './status-reply-mentions';
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
import StopPropagation from './stop-propagation';
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
@@ -92,60 +91,58 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
}
return (
<StopPropagation>
<OutlineBox
data-testid='quoted-status'
className={classNames('cursor-pointer', {
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
})}
<OutlineBox
data-testid='quoted-status'
className={classNames('cursor-pointer', {
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
})}
>
<Stack
space={2}
onClick={handleExpandClick}
>
<AccountContainer
{...actions}
id={account.id}
timestamp={status.created_at}
withRelationship={false}
showProfileHoverCard={!compose}
withLinkToProfile={!compose}
/>
<StatusReplyMentions status={status} hoverable={false} />
<Stack
space={2}
onClick={handleExpandClick}
className='relative z-0'
style={{ minHeight: status.hidden ? Math.max(minHeight, 208) + 12 : undefined }}
>
<AccountContainer
{...actions}
id={account.id}
timestamp={status.created_at}
withRelationship={false}
showProfileHoverCard={!compose}
withLinkToProfile={!compose}
/>
{(status.hidden) && (
<SensitiveContentOverlay
status={status}
visible={showMedia}
onToggleVisibility={handleToggleMediaVisibility}
ref={overlay}
/>
)}
<StatusReplyMentions status={status} hoverable={false} />
<Stack space={4}>
<StatusContent
status={status}
collapsable
/>
<Stack
className='relative z-0'
style={{ minHeight: status.hidden ? Math.max(minHeight, 208) + 12 : undefined }}
>
{(status.hidden) && (
<SensitiveContentOverlay
{(status.card || status.media_attachments.size > 0) && (
<StatusMedia
status={status}
visible={showMedia}
muted={compose}
showMedia={showMedia}
onToggleVisibility={handleToggleMediaVisibility}
ref={overlay}
/>
)}
<Stack space={4}>
<StatusContent
status={status}
collapsable
/>
{(status.card || status.media_attachments.size > 0) && (
<StatusMedia
status={status}
muted={compose}
showMedia={showMedia}
onToggleVisibility={handleToggleMediaVisibility}
/>
)}
</Stack>
</Stack>
</Stack>
</OutlineBox>
</StopPropagation>
</Stack>
</OutlineBox>
);
};

View File

@@ -530,8 +530,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
justifyContent={space === 'expand' ? 'between' : undefined}
space={space === 'compact' ? 2 : undefined}
grow={space === 'expand'}
onMouseUp={e => e.stopPropagation()}
onMouseDown={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
<StatusActionButton

View File

@@ -10,32 +10,24 @@ import { onlyEmoji as isOnlyEmoji } from 'soapbox/utils/rich-content';
import { isRtl } from '../rtl';
import Markup from './markup';
import Poll from './polls/poll';
import './status-content.css';
import StopPropagation from './stop-propagation';
import type { Status, Mention } from 'soapbox/types/entities';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
const BIG_EMOJI_LIMIT = 10;
type Point = [
x: number,
y: number,
]
interface IReadMoreButton {
onClick: React.MouseEventHandler,
}
/** Button to expand a truncated status (due to too much content) */
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
<StopPropagation>
<button className='flex items-center text-gray-900 dark:text-gray-300 border-0 bg-transparent p-0 pt-2 hover:underline active:underline' onClick={onClick}>
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} fixedWidth />
</button>
</StopPropagation>
<button className='flex items-center text-gray-900 dark:text-gray-300 border-0 bg-transparent p-0 pt-2 hover:underline active:underline' onClick={onClick}>
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} fixedWidth />
</button>
);
interface IStatusContent {
@@ -52,7 +44,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
const [collapsed, setCollapsed] = useState(false);
const [onlyEmoji, setOnlyEmoji] = useState(false);
const startXY = useRef<Point>();
const node = useRef<HTMLDivElement>(null);
const { greentext } = useSoapboxConfig();
@@ -106,10 +97,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
link.setAttribute('title', link.href);
link.addEventListener('click', onLinkClick.bind(link), false);
}
// Prevent bubbling
link.addEventListener('mouseup', e => e.stopPropagation());
link.addEventListener('mousedown', e => e.stopPropagation());
});
};
@@ -138,29 +125,6 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
updateStatusLinks();
});
const handleMouseDown: React.EventHandler<React.MouseEvent> = (e) => {
startXY.current = [e.clientX, e.clientY];
};
const handleMouseUp: React.EventHandler<React.MouseEvent> = (e) => {
if (!startXY.current) return;
const target = e.target as HTMLElement;
const parentNode = target.parentNode as HTMLElement;
const [startX, startY] = startXY.current;
const [deltaX, deltaY] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
if (target.localName === 'button' || target.localName === 'a' || (parentNode && (parentNode.localName === 'button' || parentNode.localName === 'a'))) {
return;
}
if (deltaX + deltaY < 5 && e.button === 0 && !(e.ctrlKey || e.metaKey) && onClick) {
onClick();
}
startXY.current = undefined;
};
const parsedHtml = useMemo((): string => {
const html = translatable && status.translation ? status.translation.get('content')! : status.contentHtml;
@@ -180,30 +144,24 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
const content = { __html: parsedHtml };
const directionStyle: React.CSSProperties = { direction: 'ltr' };
const className = classNames(baseClassName, 'status-content', {
const direction = isRtl(status.search_index) ? 'rtl' : 'ltr';
const className = classNames(baseClassName, {
'cursor-pointer': onClick,
'whitespace-normal': withSpoiler,
'max-h-[300px]': collapsed,
'leading-normal big-emoji': onlyEmoji,
});
if (isRtl(status.search_index)) {
directionStyle.direction = 'rtl';
}
if (onClick) {
const output = [
<div
<Markup
ref={node}
tabIndex={0}
key='content'
className={className}
style={directionStyle}
direction={direction}
dangerouslySetInnerHTML={content}
lang={status.language || undefined}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
/>,
];
@@ -219,14 +177,14 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
return <div className={classNames({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</div>;
} else {
const output = [
<div
<Markup
ref={node}
tabIndex={0}
key='content'
className={classNames(baseClassName, 'status-content', {
className={classNames(baseClassName, {
'leading-normal big-emoji': onlyEmoji,
})}
style={directionStyle}
direction={direction}
dangerouslySetInnerHTML={content}
lang={status.language || undefined}
/>,

View File

@@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { openModal } from 'soapbox/actions/modals';
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
import StopPropagation from 'soapbox/components/stop-propagation';
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card';
import Card from 'soapbox/features/status/components/card';
import Bundle from 'soapbox/features/ui/components/bundle';
@@ -176,9 +175,10 @@ const StatusMedia: React.FC<IStatusMedia> = ({
if (media) {
return (
<StopPropagation>
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div onClick={e => e.stopPropagation()}>
{media}
</StopPropagation>
</div>
);
} else {
return null;

View File

@@ -5,7 +5,6 @@ import { Link } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper';
import StopPropagation from 'soapbox/components/stop-propagation';
import { useAppDispatch } from 'soapbox/hooks';
import type { Account, Status } from 'soapbox/types/entities';
@@ -19,6 +18,8 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
const dispatch = useAppDispatch();
const handleOpenMentionsModal: React.MouseEventHandler<HTMLSpanElement> = (e) => {
e.stopPropagation();
const account = status.account as Account;
dispatch(openModal('MENTIONS', {
@@ -49,7 +50,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
// The typical case with a reply-to and a list of mentions.
const accounts = to.slice(0, 2).map(account => {
const link = (
<Link to={`/@${account.acct}`} className='reply-mentions__account'>@{account.username}</Link>
<Link to={`/@${account.acct}`} className='reply-mentions__account' onClick={(e) => e.stopPropagation()}>@{account.username}</Link>
);
if (hoverable) {
@@ -72,34 +73,32 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
}
return (
<StopPropagation>
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply.hoverable'
defaultMessage='<hover>Replying to</hover> {accounts}'
values={{
accounts: <FormattedList type='conjunction' value={accounts} />,
hover: (children: React.ReactNode) => {
if (hoverable) {
return (
<HoverStatusWrapper statusId={status.in_reply_to_id} inline>
<span
key='hoverstatus'
className='hover:underline cursor-pointer'
role='presentation'
>
{children}
</span>
</HoverStatusWrapper>
);
} else {
return children;
}
},
}}
/>
</div>
</StopPropagation>
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply.hoverable'
defaultMessage='<hover>Replying to</hover> {accounts}'
values={{
accounts: <FormattedList type='conjunction' value={accounts} />,
hover: (children: React.ReactNode) => {
if (hoverable) {
return (
<HoverStatusWrapper statusId={status.in_reply_to_id} inline>
<span
key='hoverstatus'
className='hover:underline cursor-pointer'
role='presentation'
>
{children}
</span>
</HoverStatusWrapper>
);
} else {
return children;
}
},
}}
/>
</div>
);
};

View File

@@ -235,8 +235,7 @@ const Status: React.FC<IStatus> = (props) => {
reblogElement = (
<NavLink
to={`/@${status.getIn(['account', 'acct'])}`}
onClick={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()}
onClick={(event) => event.stopPropagation()}
className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline'
>
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
@@ -259,8 +258,7 @@ const Status: React.FC<IStatus> = (props) => {
<div className='pb-5 -mt-2 sm:hidden truncate'>
<NavLink
to={`/@${status.getIn(['account', 'acct'])}`}
onClick={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()}
onClick={(event) => event.stopPropagation()}
className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline'
>
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
@@ -328,7 +326,7 @@ const Status: React.FC<IStatus> = (props) => {
data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, actualStatus, rebloggedByText)}
ref={node}
onMouseUp={handleClick}
onClick={handleClick}
role='link'
>
{featured && (

View File

@@ -5,7 +5,6 @@ import { defineMessages, useIntl } from 'react-intl';
import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
import { defaultMediaVisibility } from 'soapbox/utils/status';
import StopPropagation from '../stop-propagation';
import { Button, HStack, Text } from '../ui';
import type { Status as StatusEntity } from 'soapbox/types/entities';
@@ -39,7 +38,9 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
const [visible, setVisible] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
const toggleVisibility = () => {
const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
if (onToggleVisibility) {
onToggleVisibility();
} else {
@@ -63,15 +64,13 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
data-testid='sensitive-overlay'
>
{visible ? (
<StopPropagation>
<Button
text={intl.formatMessage(messages.hide)}
icon={require('@tabler/icons/eye-off.svg')}
onClick={toggleVisibility}
theme='primary'
size='sm'
/>
</StopPropagation>
<Button
text={intl.formatMessage(messages.hide)}
icon={require('@tabler/icons/eye-off.svg')}
onClick={toggleVisibility}
theme='primary'
size='sm'
/>
) : (
<div className='text-center w-3/4 mx-auto space-y-4' ref={ref}>
<div className='space-y-1'>
@@ -93,34 +92,36 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
</div>
<HStack alignItems='center' justifyContent='center' space={2}>
<StopPropagation>
{isUnderReview ? (
<>
{links.get('support') && (
<a href={links.get('support')} target='_blank'>
<Button
type='button'
theme='outline'
size='sm'
icon={require('@tabler/icons/headset.svg')}
>
{intl.formatMessage(messages.contact)}
</Button>
</a>
)}
</>
) : null}
{isUnderReview ? (
<>
{links.get('support') && (
<a
href={links.get('support')}
target='_blank'
onClick={(event) => event.stopPropagation()}
>
<Button
type='button'
theme='outline'
size='sm'
icon={require('@tabler/icons/headset.svg')}
>
{intl.formatMessage(messages.contact)}
</Button>
</a>
)}
</>
) : null}
<Button
type='button'
theme='outline'
size='sm'
icon={require('@tabler/icons/eye.svg')}
onClick={toggleVisibility}
>
{intl.formatMessage(messages.show)}
</Button>
</StopPropagation>
<Button
type='button'
theme='outline'
size='sm'
icon={require('@tabler/icons/eye.svg')}
onClick={toggleVisibility}
>
{intl.formatMessage(messages.show)}
</Button>
</HStack>
</div>
)}

View File

@@ -1,33 +0,0 @@
import React from 'react';
interface IStopPropagation {
/** Children to render within the bubble. */
children: React.ReactNode,
/** Whether to prevent mouse events from bubbling. (default: `true`) */
enabled?: boolean,
}
/**
* Prevent mouse events from bubbling up.
*
* Why is this needed? Because `onClick`, `onMouseDown`, and `onMouseUp` are 3 separate events.
* To prevent a lot of code duplication, this component can stop all mouse events.
* Plus, placing it in the component tree makes it more readable.
*/
const StopPropagation: React.FC<IStopPropagation> = ({ children, enabled = true }) => {
const handler: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (enabled) {
e.stopPropagation();
}
};
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div onClick={handler} onMouseDown={handler} onMouseUp={handler}>
{children}
</div>
);
};
export default StopPropagation;

View File

@@ -4,7 +4,6 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import StopPropagation from './stop-propagation';
import { Stack } from './ui';
import type { Status } from 'soapbox/types/entities';
@@ -43,21 +42,17 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
<Stack className='text-gray-700 dark:text-gray-600 text-sm' alignItems='start'>
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
<StopPropagation>
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
</button>
</StopPropagation>
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
</button>
</Stack>
);
}
return (
<StopPropagation>
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
<FormattedMessage id='status.translate' defaultMessage='Translate' />
</button>
</StopPropagation>
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
<FormattedMessage id='status.translate' defaultMessage='Translate' />
</button>
);
};

View File

@@ -8,7 +8,7 @@ import { useButtonStyles } from './useButtonStyles';
import type { ButtonSizes, ButtonThemes } from './useButtonStyles';
interface IButton extends Pick<React.HTMLAttributes<HTMLButtonElement>, 'onClick' | 'onMouseUp'> {
interface IButton {
/** Whether this button expands the width of its container. */
block?: boolean,
/** Elements inside the <button> */
@@ -19,6 +19,8 @@ interface IButton extends Pick<React.HTMLAttributes<HTMLButtonElement>, 'onClick
disabled?: boolean,
/** URL to an SVG icon to render inside the button. */
icon?: string,
/** Action when the button is clicked. */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void,
/** A predefined button size. */
size?: ButtonSizes,
/** Text inside the button. Takes precedence over `children`. */

View File

@@ -27,7 +27,7 @@ const spaces = {
8: 'space-x-8',
};
interface IHStack extends Pick<React.HTMLAttributes<HTMLDivElement>, 'onClick' | 'onMouseUp' | 'onMouseDown'> {
interface IHStack extends Pick<React.HTMLAttributes<HTMLDivElement>, 'onClick'> {
/** Vertical alignment of children. */
alignItems?: keyof typeof alignItemsOptions
/** Extra class names on the <div> element. */

View File

@@ -54,7 +54,9 @@ export type Sizes = keyof typeof sizes
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label'
type Directions = 'ltr' | 'rtl'
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML'> {
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML' | 'tabIndex' | 'lang'> {
/** Text content. */
children?: React.ReactNode,
/** How to align the text. */
align?: keyof typeof alignments,
/** Extra class names for the outer element. */
@@ -84,8 +86,8 @@ interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'danger
}
/** UI-friendly text container with dark mode support. */
const Text: React.FC<IText> = React.forwardRef(
(props: IText, ref: React.LegacyRef<any>) => {
const Text = React.forwardRef<any, IText>(
(props, ref) => {
const {
align,
className,