Revert "Merge branch 'mouseup' into 'develop'"

This reverts commit 39b4ee9f09, reversing
changes made to a0597a6445.
environments/review-actually-f-5kuyh7/deployments/1474
Alex Gleason 2 years ago
parent 6444632324
commit 8061c8d782
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -9,7 +9,6 @@ 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';
@ -22,6 +21,8 @@ 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);
@ -166,100 +167,106 @@ const Account = ({
const LinkEl: any = withLinkToProfile ? Link : 'div'; const LinkEl: any = withLinkToProfile ? Link : 'div';
return ( return (
<StopPropagation> <div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}> <HStack alignItems={actionAlignment} justifyContent='between'>
<HStack alignItems={actionAlignment} justifyContent='between'> <HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}>
<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 <ProfilePopper
condition={showProfileHoverCard} 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}> <LinkEl
<Avatar src={account.avatar} size={avatarSize} /> to={`/@${account.acct}`}
{emoji && ( title={account.acct}
<Emoji onClick={(event: React.MouseEvent) => event.stopPropagation()}
className='w-5 h-5 absolute -bottom-1.5 -right-1.5' >
emoji={emoji} <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> </LinkEl>
</ProfilePopper> </ProfilePopper>
<div className='flex-grow'> <Stack space={withAccountNote ? 1 : 0}>
<ProfilePopper <HStack alignItems='center' space={1} style={style}>
condition={showProfileHoverCard} <Text theme='muted' size='sm' truncate>@{username}</Text>
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.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')} /> {account.favicon && (
</> <InstanceFavicon account={account} />
) : 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 ref={actionRef}> {(timestamp) ? (
{withRelationship ? renderAction() : null} <>
<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> </div>
</HStack> </HStack>
</div>
</StopPropagation> <div ref={actionRef}>
{withRelationship ? renderAction() : null}
</div>
</HStack>
</div>
); );
}; };

@ -5,7 +5,6 @@ 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';
@ -65,7 +64,8 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
const showResults = poll.voted || poll.expired; const showResults = poll.voted || poll.expired;
return ( return (
<StopPropagation> // eslint-disable-next-line jsx-a11y/no-static-element-interactions
<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>
</StopPropagation> </div>
); );
}; };

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

@ -1,3 +1,4 @@
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';
@ -15,7 +16,6 @@ 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,6 +127,8 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
} else { } else {
onOpenUnauthorizedModal('REPLY'); onOpenUnauthorizedModal('REPLY');
} }
e.stopPropagation();
}; };
const handleShareClick = () => { const handleShareClick = () => {
@ -144,13 +146,18 @@ 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');
@ -165,6 +172,8 @@ 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 {
@ -190,10 +199,12 @@ 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);
}; };
@ -202,29 +213,35 @@ 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) => {
const account = status.get('account') as Account; e.stopPropagation();
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') }} />,
@ -240,6 +257,7 @@ 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}`);
}; };
@ -251,10 +269,12 @@ 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));
}; };
@ -262,6 +282,8 @@ 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';
@ -278,15 +300,18 @@ 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));
}; };
@ -525,77 +550,74 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const canShare = ('share' in navigator) && status.visibility === 'public'; const canShare = ('share' in navigator) && status.visibility === 'public';
return ( return (
<HStack data-testid='status-action-bar'> <div
<HStack data-testid='status-action-bar'
justifyContent={space === 'expand' ? 'between' : undefined} className={classNames('flex flex-row', {
space={space === 'compact' ? 2 : undefined} 'justify-between': space === 'expand',
grow={space === 'expand'} 'space-x-2': space === 'compact',
onMouseUp={e => e.stopPropagation()} })}
onMouseDown={e => e.stopPropagation()} >
onClick={e => e.stopPropagation()} <StatusActionButton
> title={replyTitle}
<StatusActionButton icon={require('@tabler/icons/message-circle-2.svg')}
title={replyTitle} onClick={handleReplyClick}
icon={require('@tabler/icons/message-circle-2.svg')} count={replyCount}
onClick={handleReplyClick} text={withLabels ? intl.formatMessage(messages.reply) : undefined}
count={replyCount} />
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
/> {(features.quotePosts && me) ? (
<DropdownMenuContainer
items={reblogMenu}
disabled={!publicStatus}
onShiftClick={handleReblogClick}
>
{reblogButton}
</DropdownMenuContainer>
) : (
reblogButton
)}
{(features.quotePosts && me) ? ( {features.emojiReacts ? (
<DropdownMenuContainer <EmojiButtonWrapper statusId={status.id}>
items={reblogMenu}
disabled={!publicStatus}
onShiftClick={handleReblogClick}
>
{reblogButton}
</DropdownMenuContainer>
) : (
reblogButton
)}
{features.emojiReacts ? (
<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={intl.formatMessage(messages.favourite)} title={meEmojiTitle}
icon={require('@tabler/icons/heart.svg')} icon={require('@tabler/icons/heart.svg')}
color='accent'
filled filled
onClick={handleFavouriteClick} color='accent'
active={Boolean(meEmojiReact)} active={Boolean(meEmojiReact)}
count={favouriteCount} count={emojiReactCount}
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>
</HStack> </div>
</HStack>
); );
}; };

@ -12,7 +12,6 @@ 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';
@ -30,12 +29,10 @@ 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 }) => (
<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}>
<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' />
<FormattedMessage id='status.read_more' defaultMessage='Read more' /> <Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} fixedWidth />
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} fixedWidth /> </button>
</button>
</StopPropagation>
); );
interface IStatusContent { interface IStatusContent {
@ -106,10 +103,6 @@ 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,7 +2,6 @@ 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';
@ -174,15 +173,7 @@ const StatusMedia: React.FC<IStatusMedia> = ({
); );
} }
if (media) { return media;
return (
<StopPropagation>
{media}
</StopPropagation>
);
} else {
return null;
}
}; };
export default StatusMedia; export default StatusMedia;

@ -5,7 +5,6 @@ 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';
@ -19,6 +18,8 @@ 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', {
@ -49,7 +50,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'>@{account.username}</Link> <Link to={`/@${account.acct}`} className='reply-mentions__account' onClick={(e) => e.stopPropagation()}>@{account.username}</Link>
); );
if (hoverable) { if (hoverable) {
@ -72,34 +73,32 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
} }
return ( return (
<StopPropagation> <div className='reply-mentions'>
<div className='reply-mentions'> <FormattedMessage
<FormattedMessage id='reply_mentions.reply.hoverable'
id='reply_mentions.reply.hoverable' defaultMessage='<hover>Replying to</hover> {accounts}'
defaultMessage='<hover>Replying to</hover> {accounts}' values={{
values={{ accounts: <FormattedList type='conjunction' value={accounts} />,
accounts: <FormattedList type='conjunction' value={accounts} />, hover: (children: React.ReactNode) => {
hover: (children: React.ReactNode) => { if (hoverable) {
if (hoverable) { return (
return ( <HoverStatusWrapper statusId={status.in_reply_to_id} inline>
<HoverStatusWrapper statusId={status.in_reply_to_id} inline> <span
<span key='hoverstatus'
key='hoverstatus' className='hover:underline cursor-pointer'
className='hover:underline cursor-pointer' role='presentation'
role='presentation' >
> {children}
{children} </span>
</span> </HoverStatusWrapper>
</HoverStatusWrapper> );
); } else {
} else { return children;
return children; }
} },
}, }}
}} />
/> </div>
</div>
</StopPropagation>
); );
}; };

@ -235,8 +235,7 @@ const Status: React.FC<IStatus> = (props) => {
reblogElement = ( reblogElement = (
<NavLink <NavLink
to={`/@${status.getIn(['account', 'acct'])}`} to={`/@${status.getIn(['account', 'acct'])}`}
onClick={e => e.stopPropagation()} onClick={(event) => event.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' />
@ -259,8 +258,7 @@ 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={e => e.stopPropagation()} onClick={(event) => event.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,7 +5,6 @@ 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';
@ -39,7 +38,9 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
const [visible, setVisible] = useState<boolean>(defaultMediaVisibility(status, displayMedia)); const [visible, setVisible] = useState<boolean>(defaultMediaVisibility(status, displayMedia));
const toggleVisibility = () => { const toggleVisibility = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
if (onToggleVisibility) { if (onToggleVisibility) {
onToggleVisibility(); onToggleVisibility();
} else { } else {
@ -63,15 +64,13 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
data-testid='sensitive-overlay' data-testid='sensitive-overlay'
> >
{visible ? ( {visible ? (
<StopPropagation> <Button
<Button text={intl.formatMessage(messages.hide)}
text={intl.formatMessage(messages.hide)} icon={require('@tabler/icons/eye-off.svg')}
icon={require('@tabler/icons/eye-off.svg')} onClick={toggleVisibility}
onClick={toggleVisibility} theme='primary'
theme='primary' size='sm'
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'>
@ -93,34 +92,36 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
</div> </div>
<HStack alignItems='center' justifyContent='center' space={2}> <HStack alignItems='center' justifyContent='center' space={2}>
<StopPropagation> {isUnderReview ? (
{isUnderReview ? ( <>
<> {links.get('support') && (
{links.get('support') && ( <a
<a href={links.get('support')} target='_blank'> href={links.get('support')}
<Button target='_blank'
type='button' onClick={(event) => event.stopPropagation()}
theme='outline' >
size='sm' <Button
icon={require('@tabler/icons/headset.svg')} type='button'
> theme='outline'
{intl.formatMessage(messages.contact)} size='sm'
</Button> icon={require('@tabler/icons/headset.svg')}
</a> >
)} {intl.formatMessage(messages.contact)}
</> </Button>
) : null} </a>
)}
<Button </>
type='button' ) : null}
theme='outline'
size='sm' <Button
icon={require('@tabler/icons/eye.svg')} type='button'
onClick={toggleVisibility} theme='outline'
> size='sm'
{intl.formatMessage(messages.show)} icon={require('@tabler/icons/eye.svg')}
</Button> onClick={toggleVisibility}
</StopPropagation> >
{intl.formatMessage(messages.show)}
</Button>
</HStack> </HStack>
</div> </div>
)} )}

@ -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;

@ -4,7 +4,6 @@ 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';
@ -43,21 +42,17 @@ 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 }} />
<StopPropagation> <button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
<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' />
<FormattedMessage id='status.show_original' defaultMessage='Show original' /> </button>
</button>
</StopPropagation>
</Stack> </Stack>
); );
} }
return ( 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}>
<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' />
<FormattedMessage id='status.translate' defaultMessage='Translate' /> </button>
</button>
</StopPropagation>
); );
}; };

@ -8,7 +8,7 @@ import { useButtonStyles } from './useButtonStyles';
import type { ButtonSizes, ButtonThemes } 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. */ /** Whether this button expands the width of its container. */
block?: boolean, block?: boolean,
/** Elements inside the <button> */ /** Elements inside the <button> */
@ -19,6 +19,8 @@ interface IButton extends Pick<React.HTMLAttributes<HTMLButtonElement>, 'onClick
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 extends Pick<React.HTMLAttributes<HTMLDivElement>, 'onClick' | 'onMouseUp' | 'onMouseDown'> { interface IHStack {
/** 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,6 +449,7 @@ 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,12 +58,7 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
const renderAccount = (account: AccountEntity) => { const renderAccount = (account: AccountEntity) => {
return ( return (
<div className='relative'> <Account account={account} showProfileHoverCard={false} withLinkToProfile={false} hideActions />
{/* 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,6 +183,8 @@ const Video: React.FC<IVideo> = ({
} }
}, [video.current]); }, [video.current]);
const handleClickRoot: React.MouseEventHandler = e => e.stopPropagation();
const handlePlay = () => { const handlePlay = () => {
setPaused(false); setPaused(false);
}; };
@ -505,6 +507,7 @@ 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…
Cancel
Save