diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 006584e8d1..db8d4b3068 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -9,6 +9,7 @@ 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'; @@ -21,8 +22,6 @@ const InstanceFavicon: React.FC = ({ account }) => { const history = useHistory(); const handleClick: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const timelineUrl = `/timeline/${account.domain}`; if (!(e.ctrlKey || e.metaKey)) { history.push(timelineUrl); @@ -167,106 +166,100 @@ const Account = ({ const LinkEl: any = withLinkToProfile ? Link : 'div'; return ( -
- - - {children}} - > - event.stopPropagation()} - > - - {emoji && ( - - )} - - - -
+ +
+ + {children}} + wrapper={(children) => {children}} > - event.stopPropagation()} - > -
- + + {emoji && ( + - - {account.verified && } -
+ )}
- - - @{username} +
+ {children}} + > + +
+ - {account.favicon && ( - - )} + {account.verified && } +
+
+
- {(timestamp) ? ( - <> - · + + + @{username} - {timestampUrl ? ( - event.stopPropagation()}> + {account.favicon && ( + + )} + + {(timestamp) ? ( + <> + · + + {timestampUrl ? ( + + + + ) : ( - - ) : ( - - )} - - ) : null} + )} + + ) : null} - {showEdit ? ( - <> - · + {showEdit ? ( + <> + · - - - ) : null} + + + ) : null} - {actionType === 'muting' && account.mute_expires_at ? ( - <> - · + {actionType === 'muting' && account.mute_expires_at ? ( + <> + · - - - ) : null} - + + + ) : null} + - {withAccountNote && ( - - )} - + {withAccountNote && ( + + )} + +
+
+ +
+ {withRelationship ? renderAction() : null}
- -
- {withRelationship ? renderAction() : null} -
-
-
+
+ ); }; diff --git a/app/soapbox/components/polls/poll.tsx b/app/soapbox/components/polls/poll.tsx index 2b6f99392f..2df985b7e7 100644 --- a/app/soapbox/components/polls/poll.tsx +++ b/app/soapbox/components/polls/poll.tsx @@ -5,6 +5,7 @@ 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'; @@ -64,8 +65,7 @@ const Poll: React.FC = ({ id, status }): JSX.Element | null => { const showResults = poll.voted || poll.expired; return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
e.stopPropagation()}> + {!showResults && poll.multiple && ( {intl.formatMessage(messages.multiple)} @@ -93,7 +93,7 @@ const Poll: React.FC = ({ id, status }): JSX.Element | null => { selected={selected} /> -
+ ); }; diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index 2c976bf865..925f2115e3 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -13,6 +13,7 @@ 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'; @@ -91,58 +92,60 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => } return ( - - + - - - - - {(status.hidden) && ( - - )} + - - + - {(status.card || status.media_attachments.size > 0) && ( - + {(status.hidden) && ( + )} + + + + + {(status.card || status.media_attachments.size > 0) && ( + + )} + - - + + ); }; diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 011fc63a8f..54c181c71f 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -1,4 +1,3 @@ -import classNames from 'clsx'; import { List as ImmutableList } from 'immutable'; import React from 'react'; 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 EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper'; import StatusActionButton from 'soapbox/components/status-action-button'; +import { HStack } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks'; import { isLocal } from 'soapbox/utils/accounts'; @@ -127,8 +127,6 @@ const StatusActionBar: React.FC = ({ } else { onOpenUnauthorizedModal('REPLY'); } - - e.stopPropagation(); }; const handleShareClick = () => { @@ -146,18 +144,13 @@ const StatusActionBar: React.FC = ({ } else { onOpenUnauthorizedModal('FAVOURITE'); } - - e.stopPropagation(); }; const handleBookmarkClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(toggleBookmark(status)); }; const handleReblogClick: React.EventHandler = e => { - e.stopPropagation(); - if (me) { const modalReblog = () => dispatch(toggleReblog(status)); const boostModal = settings.get('boostModal'); @@ -172,8 +165,6 @@ const StatusActionBar: React.FC = ({ }; const handleQuoteClick: React.EventHandler = (e) => { - e.stopPropagation(); - if (me) { dispatch(quoteCompose(status)); } else { @@ -199,12 +190,10 @@ const StatusActionBar: React.FC = ({ }; const handleDeleteClick: React.EventHandler = (e) => { - e.stopPropagation(); doDeleteStatus(); }; const handleRedraftClick: React.EventHandler = (e) => { - e.stopPropagation(); doDeleteStatus(true); }; @@ -213,35 +202,29 @@ const StatusActionBar: React.FC = ({ }; const handlePinClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(togglePin(status)); }; const handleMentionClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(mentionCompose(status.account as Account)); }; const handleDirectClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(directCompose(status.account as Account)); }; const handleChatClick: React.EventHandler = (e) => { - e.stopPropagation(); const account = status.account as Account; dispatch(launchChat(account.id, history)); }; const handleMuteClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(initMuteModal(status.account as Account)); }; const handleBlockClick: React.EventHandler = (e) => { - e.stopPropagation(); - const account = status.get('account') as Account; + dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/ban.svg'), heading: , @@ -257,7 +240,6 @@ const StatusActionBar: React.FC = ({ }; const handleOpen: React.EventHandler = (e) => { - e.stopPropagation(); history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}`); }; @@ -269,12 +251,10 @@ const StatusActionBar: React.FC = ({ }; const handleReport: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(initReport(status.account as Account, status)); }; const handleConversationMuteClick: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(toggleMuteStatus(status)); }; @@ -282,8 +262,6 @@ const StatusActionBar: React.FC = ({ const { uri } = status; const textarea = document.createElement('textarea'); - e.stopPropagation(); - textarea.textContent = uri; textarea.style.position = 'fixed'; @@ -300,18 +278,15 @@ const StatusActionBar: React.FC = ({ }; const onModerate: React.MouseEventHandler = (e) => { - e.stopPropagation(); const account = status.account as Account; dispatch(openModal('ACCOUNT_MODERATION', { accountId: account.id })); }; const handleDeleteStatus: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(deleteStatusModal(intl, status.id)); }; const handleToggleStatusSensitivity: React.EventHandler = (e) => { - e.stopPropagation(); dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); }; @@ -550,74 +525,77 @@ const StatusActionBar: React.FC = ({ const canShare = ('share' in navigator) && status.visibility === 'public'; return ( -
- + + e.stopPropagation()} + onMouseDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()} + > + - {(features.quotePosts && me) ? ( - - {reblogButton} - - ) : ( - reblogButton - )} + {(features.quotePosts && me) ? ( + + {reblogButton} + + ) : ( + reblogButton + )} - {features.emojiReacts ? ( - + {features.emojiReacts ? ( + + + + ) : ( - - ) : ( - - )} + )} - {canShare && ( - - )} + {canShare && ( + + )} - - - -
+ + + +
+
); }; diff --git a/app/soapbox/components/status-media.tsx b/app/soapbox/components/status-media.tsx index 71177cb122..f3d418d30f 100644 --- a/app/soapbox/components/status-media.tsx +++ b/app/soapbox/components/status-media.tsx @@ -2,6 +2,7 @@ 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'; @@ -173,7 +174,15 @@ const StatusMedia: React.FC = ({ ); } - return media; + if (media) { + return ( + + {media} + + ); + } else { + return null; + } }; export default StatusMedia; diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 846735dc3c..31af2cbaab 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -235,7 +235,8 @@ const Status: React.FC = (props) => { reblogElement = ( 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' > @@ -258,7 +259,8 @@ const Status: React.FC = (props) => {
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' > diff --git a/app/soapbox/components/statuses/sensitive-content-overlay.tsx b/app/soapbox/components/statuses/sensitive-content-overlay.tsx index 59faef161a..21411bf5c3 100644 --- a/app/soapbox/components/statuses/sensitive-content-overlay.tsx +++ b/app/soapbox/components/statuses/sensitive-content-overlay.tsx @@ -5,6 +5,7 @@ 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'; @@ -38,9 +39,7 @@ const SensitiveContentOverlay = React.forwardRef(defaultMediaVisibility(status, displayMedia)); - const toggleVisibility = (event: React.MouseEvent) => { - event.stopPropagation(); - + const toggleVisibility = () => { if (onToggleVisibility) { onToggleVisibility(); } else { @@ -64,13 +63,15 @@ const SensitiveContentOverlay = React.forwardRef {visible ? ( - - - )} - - ) : null} + + {isUnderReview ? ( + <> + {links.get('support') && ( + + + + )} + + ) : null} - + +
)} diff --git a/app/soapbox/components/stop-propagation.tsx b/app/soapbox/components/stop-propagation.tsx new file mode 100644 index 0000000000..614fa129c6 --- /dev/null +++ b/app/soapbox/components/stop-propagation.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +interface IStopPropagation { + children: React.ReactNode, +} + +/** + * 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 = ({ children }) => { + + const handler: React.MouseEventHandler = (e) => { + e.stopPropagation(); + }; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ {children} +
+ ); +}; + +export default StopPropagation; \ No newline at end of file diff --git a/app/soapbox/components/ui/button/button.tsx b/app/soapbox/components/ui/button/button.tsx index 97de8862b8..1b0528efbc 100644 --- a/app/soapbox/components/ui/button/button.tsx +++ b/app/soapbox/components/ui/button/button.tsx @@ -8,7 +8,7 @@ import { useButtonStyles } from './useButtonStyles'; import type { ButtonSizes, ButtonThemes } from './useButtonStyles'; -interface IButton { +interface IButton extends Pick, 'onClick' | 'onMouseUp'> { /** Whether this button expands the width of its container. */ block?: boolean, /** Elements inside the