Merge branch 'mouseup' into 'develop'
Stop mouseUp propagation in statuses See merge request soapbox-pub/soapbox!1920
This commit is contained in:
commit
39b4ee9f09
16 changed files with 332 additions and 303 deletions
|
@ -9,6 +9,7 @@ import { getAcct } from 'soapbox/utils/accounts';
|
||||||
import { displayFqn } from 'soapbox/utils/state';
|
import { displayFqn } from 'soapbox/utils/state';
|
||||||
|
|
||||||
import RelativeTimestamp from './relative-timestamp';
|
import RelativeTimestamp from './relative-timestamp';
|
||||||
|
import StopPropagation from './stop-propagation';
|
||||||
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
|
||||||
|
|
||||||
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||||
|
@ -21,8 +22,6 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler = (e) => {
|
const handleClick: React.MouseEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const timelineUrl = `/timeline/${account.domain}`;
|
const timelineUrl = `/timeline/${account.domain}`;
|
||||||
if (!(e.ctrlKey || e.metaKey)) {
|
if (!(e.ctrlKey || e.metaKey)) {
|
||||||
history.push(timelineUrl);
|
history.push(timelineUrl);
|
||||||
|
@ -167,106 +166,100 @@ const Account = ({
|
||||||
const LinkEl: any = withLinkToProfile ? Link : 'div';
|
const LinkEl: any = withLinkToProfile ? Link : 'div';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
|
<StopPropagation>
|
||||||
<HStack alignItems={actionAlignment} justifyContent='between'>
|
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
|
||||||
<HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}>
|
<HStack alignItems={actionAlignment} justifyContent='between'>
|
||||||
<ProfilePopper
|
<HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}>
|
||||||
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
|
<ProfilePopper
|
||||||
condition={showProfileHoverCard}
|
condition={showProfileHoverCard}
|
||||||
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||||
>
|
>
|
||||||
<LinkEl
|
<LinkEl to={`/@${account.acct}`} title={account.acct}>
|
||||||
to={`/@${account.acct}`}
|
<Avatar src={account.avatar} size={avatarSize} />
|
||||||
title={account.acct}
|
{emoji && (
|
||||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
<Emoji
|
||||||
>
|
className='w-5 h-5 absolute -bottom-1.5 -right-1.5'
|
||||||
<div className='flex items-center space-x-1 flex-grow' style={style}>
|
emoji={emoji}
|
||||||
<Text
|
|
||||||
size='sm'
|
|
||||||
weight='semibold'
|
|
||||||
truncate
|
|
||||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{account.verified && <VerificationBadge />}
|
|
||||||
</div>
|
|
||||||
</LinkEl>
|
</LinkEl>
|
||||||
</ProfilePopper>
|
</ProfilePopper>
|
||||||
|
|
||||||
<Stack space={withAccountNote ? 1 : 0}>
|
<div className='flex-grow'>
|
||||||
<HStack alignItems='center' space={1} style={style}>
|
<ProfilePopper
|
||||||
<Text theme='muted' size='sm' truncate>@{username}</Text>
|
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 }}
|
||||||
|
/>
|
||||||
|
|
||||||
{account.favicon && (
|
{account.verified && <VerificationBadge />}
|
||||||
<InstanceFavicon account={account} />
|
</div>
|
||||||
)}
|
</LinkEl>
|
||||||
|
</ProfilePopper>
|
||||||
|
|
||||||
{(timestamp) ? (
|
<Stack space={withAccountNote ? 1 : 0}>
|
||||||
<>
|
<HStack alignItems='center' space={1} style={style}>
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<Text theme='muted' size='sm' truncate>@{username}</Text>
|
||||||
|
|
||||||
{timestampUrl ? (
|
{account.favicon && (
|
||||||
<Link to={timestampUrl} className='hover:underline' onClick={(event) => event.stopPropagation()}>
|
<InstanceFavicon account={account} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(timestamp) ? (
|
||||||
|
<>
|
||||||
|
<Text tag='span' theme='muted' size='sm'>·</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} />
|
<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}
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showEdit ? (
|
{showEdit ? (
|
||||||
<>
|
<>
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
||||||
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
|
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/pencil.svg')} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{actionType === 'muting' && account.mute_expires_at ? (
|
{actionType === 'muting' && account.mute_expires_at ? (
|
||||||
<>
|
<>
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
||||||
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
|
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{withAccountNote && (
|
{withAccountNote && (
|
||||||
<Text
|
<Text
|
||||||
size='sm'
|
size='sm'
|
||||||
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
||||||
className='mr-2'
|
className='mr-2'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<div ref={actionRef}>
|
||||||
|
{withRelationship ? renderAction() : null}
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
</div>
|
||||||
<div ref={actionRef}>
|
</StopPropagation>
|
||||||
{withRelationship ? renderAction() : null}
|
|
||||||
</div>
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { openModal } from 'soapbox/actions/modals';
|
||||||
import { vote } from 'soapbox/actions/polls';
|
import { vote } from 'soapbox/actions/polls';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import StopPropagation from '../stop-propagation';
|
||||||
import { Stack, Text } from '../ui';
|
import { Stack, Text } from '../ui';
|
||||||
|
|
||||||
import PollFooter from './poll-footer';
|
import PollFooter from './poll-footer';
|
||||||
|
@ -64,8 +65,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
|
||||||
const showResults = poll.voted || poll.expired;
|
const showResults = poll.voted || poll.expired;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
<StopPropagation>
|
||||||
<div onClick={e => e.stopPropagation()}>
|
|
||||||
{!showResults && poll.multiple && (
|
{!showResults && poll.multiple && (
|
||||||
<Text theme='muted' size='sm'>
|
<Text theme='muted' size='sm'>
|
||||||
{intl.formatMessage(messages.multiple)}
|
{intl.formatMessage(messages.multiple)}
|
||||||
|
@ -93,7 +93,7 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
|
||||||
selected={selected}
|
selected={selected}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</StopPropagation>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import OutlineBox from './outline-box';
|
||||||
import StatusContent from './status-content';
|
import StatusContent from './status-content';
|
||||||
import StatusReplyMentions from './status-reply-mentions';
|
import StatusReplyMentions from './status-reply-mentions';
|
||||||
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||||
|
import StopPropagation from './stop-propagation';
|
||||||
|
|
||||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
@ -91,58 +92,60 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OutlineBox
|
<StopPropagation>
|
||||||
data-testid='quoted-status'
|
<OutlineBox
|
||||||
className={classNames('cursor-pointer', {
|
data-testid='quoted-status'
|
||||||
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
|
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
|
<Stack
|
||||||
className='relative z-0'
|
space={2}
|
||||||
style={{ minHeight: status.hidden ? Math.max(minHeight, 208) + 12 : undefined }}
|
onClick={handleExpandClick}
|
||||||
>
|
>
|
||||||
{(status.hidden) && (
|
<AccountContainer
|
||||||
<SensitiveContentOverlay
|
{...actions}
|
||||||
status={status}
|
id={account.id}
|
||||||
visible={showMedia}
|
timestamp={status.created_at}
|
||||||
onToggleVisibility={handleToggleMediaVisibility}
|
withRelationship={false}
|
||||||
ref={overlay}
|
showProfileHoverCard={!compose}
|
||||||
/>
|
withLinkToProfile={!compose}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
<Stack space={4}>
|
<StatusReplyMentions status={status} hoverable={false} />
|
||||||
<StatusContent
|
|
||||||
status={status}
|
|
||||||
collapsable
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(status.card || status.media_attachments.size > 0) && (
|
<Stack
|
||||||
<StatusMedia
|
className='relative z-0'
|
||||||
|
style={{ minHeight: status.hidden ? Math.max(minHeight, 208) + 12 : undefined }}
|
||||||
|
>
|
||||||
|
{(status.hidden) && (
|
||||||
|
<SensitiveContentOverlay
|
||||||
status={status}
|
status={status}
|
||||||
muted={compose}
|
visible={showMedia}
|
||||||
showMedia={showMedia}
|
|
||||||
onToggleVisibility={handleToggleMediaVisibility}
|
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>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</OutlineBox>
|
||||||
</OutlineBox>
|
</StopPropagation>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import classNames from 'clsx';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
@ -16,6 +15,7 @@ import { initReport } from 'soapbox/actions/reports';
|
||||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||||
import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper';
|
import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper';
|
||||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||||
|
import { HStack } from 'soapbox/components/ui';
|
||||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
import { isLocal } from 'soapbox/utils/accounts';
|
import { isLocal } from 'soapbox/utils/accounts';
|
||||||
|
@ -127,8 +127,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
} else {
|
} else {
|
||||||
onOpenUnauthorizedModal('REPLY');
|
onOpenUnauthorizedModal('REPLY');
|
||||||
}
|
}
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShareClick = () => {
|
const handleShareClick = () => {
|
||||||
|
@ -146,18 +144,13 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
} else {
|
} else {
|
||||||
onOpenUnauthorizedModal('FAVOURITE');
|
onOpenUnauthorizedModal('FAVOURITE');
|
||||||
}
|
}
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(toggleBookmark(status));
|
dispatch(toggleBookmark(status));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
|
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
const modalReblog = () => dispatch(toggleReblog(status));
|
const modalReblog = () => dispatch(toggleReblog(status));
|
||||||
const boostModal = settings.get('boostModal');
|
const boostModal = settings.get('boostModal');
|
||||||
|
@ -172,8 +165,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuoteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleQuoteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
dispatch(quoteCompose(status));
|
dispatch(quoteCompose(status));
|
||||||
} else {
|
} else {
|
||||||
|
@ -199,12 +190,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleDeleteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
doDeleteStatus();
|
doDeleteStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRedraftClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleRedraftClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
doDeleteStatus(true);
|
doDeleteStatus(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -213,35 +202,29 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePinClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handlePinClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(togglePin(status));
|
dispatch(togglePin(status));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMentionClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleMentionClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(mentionCompose(status.account as Account));
|
dispatch(mentionCompose(status.account as Account));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDirectClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleDirectClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(directCompose(status.account as Account));
|
dispatch(directCompose(status.account as Account));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChatClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleChatClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
const account = status.account as Account;
|
const account = status.account as Account;
|
||||||
dispatch(launchChat(account.id, history));
|
dispatch(launchChat(account.id, history));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(initMuteModal(status.account as Account));
|
dispatch(initMuteModal(status.account as Account));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlockClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleBlockClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const account = status.get('account') as Account;
|
const account = status.get('account') as Account;
|
||||||
|
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
icon: require('@tabler/icons/ban.svg'),
|
icon: require('@tabler/icons/ban.svg'),
|
||||||
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.get('acct') }} />,
|
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.get('acct') }} />,
|
||||||
|
@ -257,7 +240,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpen: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleOpen: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}`);
|
history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -269,12 +251,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReport: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleReport: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(initReport(status.account as Account, status));
|
dispatch(initReport(status.account as Account, status));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConversationMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleConversationMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(toggleMuteStatus(status));
|
dispatch(toggleMuteStatus(status));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -282,8 +262,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
const { uri } = status;
|
const { uri } = status;
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
textarea.textContent = uri;
|
textarea.textContent = uri;
|
||||||
textarea.style.position = 'fixed';
|
textarea.style.position = 'fixed';
|
||||||
|
|
||||||
|
@ -300,18 +278,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const onModerate: React.MouseEventHandler = (e) => {
|
const onModerate: React.MouseEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
const account = status.account as Account;
|
const account = status.account as Account;
|
||||||
dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id }));
|
dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteStatus: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleDeleteStatus: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(deleteStatusModal(intl, status.id));
|
dispatch(deleteStatusModal(intl, status.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleStatusSensitivity: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleToggleStatusSensitivity: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive));
|
dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -550,74 +525,77 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
const canShare = ('share' in navigator) && status.visibility === 'public';
|
const canShare = ('share' in navigator) && status.visibility === 'public';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<HStack data-testid='status-action-bar'>
|
||||||
data-testid='status-action-bar'
|
<HStack
|
||||||
className={classNames('flex flex-row', {
|
justifyContent={space === 'expand' ? 'between' : undefined}
|
||||||
'justify-between': space === 'expand',
|
space={space === 'compact' ? 2 : undefined}
|
||||||
'space-x-2': space === 'compact',
|
grow={space === 'expand'}
|
||||||
})}
|
onMouseUp={e => e.stopPropagation()}
|
||||||
>
|
onMouseDown={e => e.stopPropagation()}
|
||||||
<StatusActionButton
|
onClick={e => e.stopPropagation()}
|
||||||
title={replyTitle}
|
>
|
||||||
icon={require('@tabler/icons/message-circle-2.svg')}
|
<StatusActionButton
|
||||||
onClick={handleReplyClick}
|
title={replyTitle}
|
||||||
count={replyCount}
|
icon={require('@tabler/icons/message-circle-2.svg')}
|
||||||
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
onClick={handleReplyClick}
|
||||||
/>
|
count={replyCount}
|
||||||
|
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
{(features.quotePosts && me) ? (
|
{(features.quotePosts && me) ? (
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
items={reblogMenu}
|
items={reblogMenu}
|
||||||
disabled={!publicStatus}
|
disabled={!publicStatus}
|
||||||
onShiftClick={handleReblogClick}
|
onShiftClick={handleReblogClick}
|
||||||
>
|
>
|
||||||
{reblogButton}
|
{reblogButton}
|
||||||
</DropdownMenuContainer>
|
</DropdownMenuContainer>
|
||||||
) : (
|
) : (
|
||||||
reblogButton
|
reblogButton
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{features.emojiReacts ? (
|
{features.emojiReacts ? (
|
||||||
<EmojiButtonWrapper statusId={status.id}>
|
<EmojiButtonWrapper statusId={status.id}>
|
||||||
|
<StatusActionButton
|
||||||
|
title={meEmojiTitle}
|
||||||
|
icon={require('@tabler/icons/heart.svg')}
|
||||||
|
filled
|
||||||
|
color='accent'
|
||||||
|
active={Boolean(meEmojiReact)}
|
||||||
|
count={emojiReactCount}
|
||||||
|
emoji={meEmojiReact}
|
||||||
|
text={withLabels ? meEmojiTitle : undefined}
|
||||||
|
/>
|
||||||
|
</EmojiButtonWrapper>
|
||||||
|
) : (
|
||||||
<StatusActionButton
|
<StatusActionButton
|
||||||
title={meEmojiTitle}
|
title={intl.formatMessage(messages.favourite)}
|
||||||
icon={require('@tabler/icons/heart.svg')}
|
icon={require('@tabler/icons/heart.svg')}
|
||||||
filled
|
|
||||||
color='accent'
|
color='accent'
|
||||||
|
filled
|
||||||
|
onClick={handleFavouriteClick}
|
||||||
active={Boolean(meEmojiReact)}
|
active={Boolean(meEmojiReact)}
|
||||||
count={emojiReactCount}
|
count={favouriteCount}
|
||||||
emoji={meEmojiReact}
|
|
||||||
text={withLabels ? meEmojiTitle : undefined}
|
text={withLabels ? meEmojiTitle : undefined}
|
||||||
/>
|
/>
|
||||||
</EmojiButtonWrapper>
|
)}
|
||||||
) : (
|
|
||||||
<StatusActionButton
|
|
||||||
title={intl.formatMessage(messages.favourite)}
|
|
||||||
icon={require('@tabler/icons/heart.svg')}
|
|
||||||
color='accent'
|
|
||||||
filled
|
|
||||||
onClick={handleFavouriteClick}
|
|
||||||
active={Boolean(meEmojiReact)}
|
|
||||||
count={favouriteCount}
|
|
||||||
text={withLabels ? meEmojiTitle : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canShare && (
|
{canShare && (
|
||||||
<StatusActionButton
|
<StatusActionButton
|
||||||
title={intl.formatMessage(messages.share)}
|
title={intl.formatMessage(messages.share)}
|
||||||
icon={require('@tabler/icons/upload.svg')}
|
icon={require('@tabler/icons/upload.svg')}
|
||||||
onClick={handleShareClick}
|
onClick={handleShareClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} status={status}>
|
<DropdownMenuContainer items={menu} status={status}>
|
||||||
<StatusActionButton
|
<StatusActionButton
|
||||||
title={intl.formatMessage(messages.more)}
|
title={intl.formatMessage(messages.more)}
|
||||||
icon={require('@tabler/icons/dots.svg')}
|
icon={require('@tabler/icons/dots.svg')}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuContainer>
|
</DropdownMenuContainer>
|
||||||
</div>
|
</HStack>
|
||||||
|
</HStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { isRtl } from '../rtl';
|
||||||
|
|
||||||
import Poll from './polls/poll';
|
import Poll from './polls/poll';
|
||||||
import './status-content.css';
|
import './status-content.css';
|
||||||
|
import StopPropagation from './stop-propagation';
|
||||||
|
|
||||||
import type { Status, Mention } from 'soapbox/types/entities';
|
import type { Status, Mention } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
@ -29,10 +30,12 @@ interface IReadMoreButton {
|
||||||
|
|
||||||
/** Button to expand a truncated status (due to too much content) */
|
/** Button to expand a truncated status (due to too much content) */
|
||||||
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
||||||
<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}>
|
<StopPropagation>
|
||||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
<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}>
|
||||||
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} fixedWidth />
|
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
||||||
</button>
|
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} fixedWidth />
|
||||||
|
</button>
|
||||||
|
</StopPropagation>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface IStatusContent {
|
interface IStatusContent {
|
||||||
|
@ -103,6 +106,10 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
||||||
link.setAttribute('title', link.href);
|
link.setAttribute('title', link.href);
|
||||||
link.addEventListener('click', onLinkClick.bind(link), false);
|
link.addEventListener('click', onLinkClick.bind(link), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent bubbling
|
||||||
|
link.addEventListener('mouseup', e => e.stopPropagation());
|
||||||
|
link.addEventListener('mousedown', e => e.stopPropagation());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||||
|
import StopPropagation from 'soapbox/components/stop-propagation';
|
||||||
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card';
|
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card';
|
||||||
import Card from 'soapbox/features/status/components/card';
|
import Card from 'soapbox/features/status/components/card';
|
||||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||||
|
@ -173,7 +174,15 @@ const StatusMedia: React.FC<IStatusMedia> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return media;
|
if (media) {
|
||||||
|
return (
|
||||||
|
<StopPropagation>
|
||||||
|
{media}
|
||||||
|
</StopPropagation>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatusMedia;
|
export default StatusMedia;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Link } from 'react-router-dom';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||||
import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper';
|
import HoverStatusWrapper from 'soapbox/components/hover-status-wrapper';
|
||||||
|
import StopPropagation from 'soapbox/components/stop-propagation';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { Account, Status } from 'soapbox/types/entities';
|
import type { Account, Status } from 'soapbox/types/entities';
|
||||||
|
@ -18,8 +19,6 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleOpenMentionsModal: React.MouseEventHandler<HTMLSpanElement> = (e) => {
|
const handleOpenMentionsModal: React.MouseEventHandler<HTMLSpanElement> = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const account = status.account as Account;
|
const account = status.account as Account;
|
||||||
|
|
||||||
dispatch(openModal('MENTIONS', {
|
dispatch(openModal('MENTIONS', {
|
||||||
|
@ -50,7 +49,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
||||||
// The typical case with a reply-to and a list of mentions.
|
// The typical case with a reply-to and a list of mentions.
|
||||||
const accounts = to.slice(0, 2).map(account => {
|
const accounts = to.slice(0, 2).map(account => {
|
||||||
const link = (
|
const link = (
|
||||||
<Link to={`/@${account.acct}`} className='reply-mentions__account' onClick={(e) => e.stopPropagation()}>@{account.username}</Link>
|
<Link to={`/@${account.acct}`} className='reply-mentions__account'>@{account.username}</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hoverable) {
|
if (hoverable) {
|
||||||
|
@ -73,32 +72,34 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='reply-mentions'>
|
<StopPropagation>
|
||||||
<FormattedMessage
|
<div className='reply-mentions'>
|
||||||
id='reply_mentions.reply.hoverable'
|
<FormattedMessage
|
||||||
defaultMessage='<hover>Replying to</hover> {accounts}'
|
id='reply_mentions.reply.hoverable'
|
||||||
values={{
|
defaultMessage='<hover>Replying to</hover> {accounts}'
|
||||||
accounts: <FormattedList type='conjunction' value={accounts} />,
|
values={{
|
||||||
hover: (children: React.ReactNode) => {
|
accounts: <FormattedList type='conjunction' value={accounts} />,
|
||||||
if (hoverable) {
|
hover: (children: React.ReactNode) => {
|
||||||
return (
|
if (hoverable) {
|
||||||
<HoverStatusWrapper statusId={status.in_reply_to_id} inline>
|
return (
|
||||||
<span
|
<HoverStatusWrapper statusId={status.in_reply_to_id} inline>
|
||||||
key='hoverstatus'
|
<span
|
||||||
className='hover:underline cursor-pointer'
|
key='hoverstatus'
|
||||||
role='presentation'
|
className='hover:underline cursor-pointer'
|
||||||
>
|
role='presentation'
|
||||||
{children}
|
>
|
||||||
</span>
|
{children}
|
||||||
</HoverStatusWrapper>
|
</span>
|
||||||
);
|
</HoverStatusWrapper>
|
||||||
} else {
|
);
|
||||||
return children;
|
} else {
|
||||||
}
|
return children;
|
||||||
},
|
}
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</StopPropagation>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -235,7 +235,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
reblogElement = (
|
reblogElement = (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/@${status.getIn(['account', 'acct'])}`}
|
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
|
onMouseUp={e => e.stopPropagation()}
|
||||||
className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline'
|
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' />
|
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
|
||||||
|
@ -258,7 +259,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
<div className='pb-5 -mt-2 sm:hidden truncate'>
|
<div className='pb-5 -mt-2 sm:hidden truncate'>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/@${status.getIn(['account', 'acct'])}`}
|
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
|
onMouseUp={e => e.stopPropagation()}
|
||||||
className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline'
|
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' />
|
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
import { defaultMediaVisibility } from 'soapbox/utils/status';
|
||||||
|
|
||||||
|
import StopPropagation from '../stop-propagation';
|
||||||
import { Button, HStack, Text } from '../ui';
|
import { Button, HStack, Text } from '../ui';
|
||||||
|
|
||||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
@ -38,9 +39,7 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
||||||
|
|
||||||
const [visible, setVisible] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
|
const [visible, setVisible] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
|
||||||
|
|
||||||
const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const toggleVisibility = () => {
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
if (onToggleVisibility) {
|
if (onToggleVisibility) {
|
||||||
onToggleVisibility();
|
onToggleVisibility();
|
||||||
} else {
|
} else {
|
||||||
|
@ -64,13 +63,15 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
||||||
data-testid='sensitive-overlay'
|
data-testid='sensitive-overlay'
|
||||||
>
|
>
|
||||||
{visible ? (
|
{visible ? (
|
||||||
<Button
|
<StopPropagation>
|
||||||
text={intl.formatMessage(messages.hide)}
|
<Button
|
||||||
icon={require('@tabler/icons/eye-off.svg')}
|
text={intl.formatMessage(messages.hide)}
|
||||||
onClick={toggleVisibility}
|
icon={require('@tabler/icons/eye-off.svg')}
|
||||||
theme='primary'
|
onClick={toggleVisibility}
|
||||||
size='sm'
|
theme='primary'
|
||||||
/>
|
size='sm'
|
||||||
|
/>
|
||||||
|
</StopPropagation>
|
||||||
) : (
|
) : (
|
||||||
<div className='text-center w-3/4 mx-auto space-y-4' ref={ref}>
|
<div className='text-center w-3/4 mx-auto space-y-4' ref={ref}>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
|
@ -92,36 +93,34 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HStack alignItems='center' justifyContent='center' space={2}>
|
<HStack alignItems='center' justifyContent='center' space={2}>
|
||||||
{isUnderReview ? (
|
<StopPropagation>
|
||||||
<>
|
{isUnderReview ? (
|
||||||
{links.get('support') && (
|
<>
|
||||||
<a
|
{links.get('support') && (
|
||||||
href={links.get('support')}
|
<a href={links.get('support')} target='_blank'>
|
||||||
target='_blank'
|
<Button
|
||||||
onClick={(event) => event.stopPropagation()}
|
type='button'
|
||||||
>
|
theme='outline'
|
||||||
<Button
|
size='sm'
|
||||||
type='button'
|
icon={require('@tabler/icons/headset.svg')}
|
||||||
theme='outline'
|
>
|
||||||
size='sm'
|
{intl.formatMessage(messages.contact)}
|
||||||
icon={require('@tabler/icons/headset.svg')}
|
</Button>
|
||||||
>
|
</a>
|
||||||
{intl.formatMessage(messages.contact)}
|
)}
|
||||||
</Button>
|
</>
|
||||||
</a>
|
) : null}
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
theme='outline'
|
theme='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
icon={require('@tabler/icons/eye.svg')}
|
icon={require('@tabler/icons/eye.svg')}
|
||||||
onClick={toggleVisibility}
|
onClick={toggleVisibility}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.show)}
|
{intl.formatMessage(messages.show)}
|
||||||
</Button>
|
</Button>
|
||||||
|
</StopPropagation>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
33
app/soapbox/components/stop-propagation.tsx
Normal file
33
app/soapbox/components/stop-propagation.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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;
|
|
@ -4,6 +4,7 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
|
import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import StopPropagation from './stop-propagation';
|
||||||
import { Stack } from './ui';
|
import { Stack } from './ui';
|
||||||
|
|
||||||
import type { Status } from 'soapbox/types/entities';
|
import type { Status } from 'soapbox/types/entities';
|
||||||
|
@ -42,17 +43,21 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||||
<Stack className='text-gray-700 dark:text-gray-600 text-sm' alignItems='start'>
|
<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 }} />
|
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||||
|
|
||||||
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
|
<StopPropagation>
|
||||||
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
|
||||||
</button>
|
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||||
|
</button>
|
||||||
|
</StopPropagation>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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}>
|
<StopPropagation>
|
||||||
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
<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}>
|
||||||
</button>
|
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||||
|
</button>
|
||||||
|
</StopPropagation>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useButtonStyles } from './useButtonStyles';
|
||||||
|
|
||||||
import type { ButtonSizes, ButtonThemes } from './useButtonStyles';
|
import type { ButtonSizes, ButtonThemes } from './useButtonStyles';
|
||||||
|
|
||||||
interface IButton {
|
interface IButton extends Pick<React.HTMLAttributes<HTMLButtonElement>, 'onClick' | 'onMouseUp'> {
|
||||||
/** Whether this button expands the width of its container. */
|
/** Whether this button expands the width of its container. */
|
||||||
block?: boolean,
|
block?: boolean,
|
||||||
/** Elements inside the <button> */
|
/** Elements inside the <button> */
|
||||||
|
@ -19,8 +19,6 @@ interface IButton {
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
/** URL to an SVG icon to render inside the button. */
|
/** URL to an SVG icon to render inside the button. */
|
||||||
icon?: string,
|
icon?: string,
|
||||||
/** Action when the button is clicked. */
|
|
||||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void,
|
|
||||||
/** A predefined button size. */
|
/** A predefined button size. */
|
||||||
size?: ButtonSizes,
|
size?: ButtonSizes,
|
||||||
/** Text inside the button. Takes precedence over `children`. */
|
/** Text inside the button. Takes precedence over `children`. */
|
||||||
|
|
|
@ -27,7 +27,7 @@ const spaces = {
|
||||||
8: 'space-x-8',
|
8: 'space-x-8',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IHStack {
|
interface IHStack extends Pick<React.HTMLAttributes<HTMLDivElement>, 'onClick' | 'onMouseUp' | 'onMouseDown'> {
|
||||||
/** Vertical alignment of children. */
|
/** Vertical alignment of children. */
|
||||||
alignItems?: keyof typeof alignItemsOptions
|
alignItems?: keyof typeof alignItemsOptions
|
||||||
/** Extra class names on the <div> element. */
|
/** Extra class names on the <div> element. */
|
||||||
|
|
|
@ -449,7 +449,6 @@ const Audio: React.FC<IAudio> = (props) => {
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<audio
|
<audio
|
||||||
src={src}
|
src={src}
|
||||||
|
|
|
@ -58,7 +58,12 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
||||||
|
|
||||||
const renderAccount = (account: AccountEntity) => {
|
const renderAccount = (account: AccountEntity) => {
|
||||||
return (
|
return (
|
||||||
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} hideActions />
|
<div className='relative'>
|
||||||
|
{/* HACK: The <Account> component stops click events, so insert this div as something to click. */}
|
||||||
|
<div className='absolute inset-0' />
|
||||||
|
|
||||||
|
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} hideActions />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -183,8 +183,6 @@ const Video: React.FC<IVideo> = ({
|
||||||
}
|
}
|
||||||
}, [video.current]);
|
}, [video.current]);
|
||||||
|
|
||||||
const handleClickRoot: React.MouseEventHandler = e => e.stopPropagation();
|
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
setPaused(false);
|
setPaused(false);
|
||||||
};
|
};
|
||||||
|
@ -507,7 +505,6 @@ const Video: React.FC<IVideo> = ({
|
||||||
ref={player}
|
ref={player}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
onClick={handleClickRoot}
|
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in a new issue