From 4970c6c3075ba27fbbe3dfd776cfd2502093a6ac Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 9 Aug 2022 13:46:11 -0500 Subject: [PATCH] StatusActionBar: convert to React.FC --- app/soapbox/components/status_action_bar.tsx | 690 ++++++++----------- 1 file changed, 276 insertions(+), 414 deletions(-) diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index 920929d78..6bd69d74e 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -1,25 +1,19 @@ import { List as ImmutableList } from 'immutable'; import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, IntlShape } from 'react-intl'; -import { connect } from 'react-redux'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { defineMessages, useIntl } from 'react-intl'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; -import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; import { openModal } from 'soapbox/actions/modals'; import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper'; import StatusActionButton from 'soapbox/components/status-action-button'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; -import { isUserTouching } from 'soapbox/is_mobile'; +import { useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks'; import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts'; -import { getFeatures } from 'soapbox/utils/features'; import type { History } from 'history'; -import type { AnyAction, Dispatch } from 'redux'; import type { Menu } from 'soapbox/components/dropdown_menu'; -import type { RootState } from 'soapbox/store'; import type { Status } from 'soapbox/types/entities'; -import type { Features } from 'soapbox/utils/features'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -67,10 +61,8 @@ const messages = defineMessages({ quotePost: { id: 'status.quote', defaultMessage: 'Quote post' }, }); -interface IStatusActionBar extends RouteComponentProps { +interface IStatusActionBar { status: Status, - onOpenUnauthorizedModal: (modalType?: string) => void, - onOpenReblogsModal: (acct: string, statusId: string) => void, onReply: (status: Status) => void, onFavourite: (status: Status) => void, onEmojiReact: (status: Status, emoji: string) => void, @@ -94,47 +86,56 @@ interface IStatusActionBar extends RouteComponentProps { onPin: (status: Status) => void, withDismiss?: boolean, withGroupAdmin?: boolean, - intl: IntlShape, - me: string | null | false | undefined, - isStaff: boolean, - isAdmin: boolean, allowedEmoji: ImmutableList, emojiSelectorFocused: boolean, handleEmojiSelectorUnfocus: () => void, handleEmojiSelectorExpand?: React.EventHandler, - features: Features, - history: History, - dispatch: Dispatch, } -interface IStatusActionBarState { - emojiSelectorVisible: boolean, -} +const StatusActionBar: React.FC = ({ + status, + onReply, + onFavourite, + allowedEmoji, + onBookmark, + onReblog, + onQuote, + onDelete, + onEdit, + onPin, + onMention, + onDirect, + onChat, + onMute, + onBlock, + onEmbed, + onReport, + onMuteConversation, + onDeactivateUser, + onDeleteUser, + onDeleteStatus, + onToggleStatusSensitivity, + withDismiss, +}) => { + const intl = useIntl(); + const history = useHistory(); + const dispatch = useDispatch(); -class StatusActionBar extends ImmutablePureComponent { + const me = useAppSelector(state => state.me); + const features = useFeatures(); - static defaultProps: Partial = { - isStaff: false, - } + const account = useOwnAccount(); + const isStaff = account ? account.staff : false; + const isAdmin = account ? account.admin : false; - node?: HTMLDivElement = undefined; - - state = { - emojiSelectorVisible: false, - } - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - // @ts-ignore: the type checker is wrong. - updateOnProps = [ - 'status', - 'withDismiss', - 'emojiSelectorFocused', - ] - - handleReplyClick: React.MouseEventHandler = (e) => { - const { me, onReply, onOpenUnauthorizedModal, status } = this.props; + const onOpenUnauthorizedModal = (action?: string) => { + dispatch(openModal('UNAUTHORIZED', { + action, + ap_id: status.url, + })); + }; + const handleReplyClick: React.MouseEventHandler = (e) => { if (me) { onReply(status); } else { @@ -142,70 +143,18 @@ class StatusActionBar extends ImmutablePureComponent { + const handleShareClick = () => { navigator.share({ - text: this.props.status.search_index, - url: this.props.status.uri, + text: status.search_index, + url: status.uri, }).catch((e) => { if (e.name !== 'AbortError') console.error(e); }); - } + }; - handleLikeButtonHover: React.EventHandler = () => { - const { features } = this.props; - - if (features.emojiReacts && !isUserTouching()) { - this.setState({ emojiSelectorVisible: true }); - } - } - - handleLikeButtonLeave: React.EventHandler = () => { - const { features } = this.props; - - if (features.emojiReacts && !isUserTouching()) { - this.setState({ emojiSelectorVisible: false }); - } - } - - handleLikeButtonClick: React.EventHandler = (e) => { - const { features } = this.props; - - const reactForStatus = getReactForStatus(this.props.status, this.props.allowedEmoji); - const meEmojiReact = typeof reactForStatus === 'string' ? reactForStatus : '👍'; - - if (features.emojiReacts && isUserTouching()) { - if (this.state.emojiSelectorVisible) { - this.handleReact(meEmojiReact); - } else { - this.setState({ emojiSelectorVisible: true }); - } - } else { - this.handleReact(meEmojiReact); - } - - e.stopPropagation(); - } - - handleReact = (emoji: string): void => { - const { me, dispatch, onOpenUnauthorizedModal, status } = this.props; - if (me) { - dispatch(simpleEmojiReact(status, emoji) as any); - } else { - onOpenUnauthorizedModal('FAVOURITE'); - } - this.setState({ emojiSelectorVisible: false }); - } - - handleReactClick = (emoji: string): React.EventHandler => { - return () => { - this.handleReact(emoji); - }; - } - - handleFavouriteClick: React.EventHandler = (e) => { - const { me, onFavourite, onOpenUnauthorizedModal, status } = this.props; + const handleFavouriteClick: React.EventHandler = (e) => { if (me) { onFavourite(status); } else { @@ -213,15 +162,14 @@ class StatusActionBar extends ImmutablePureComponent = (e) => { + const handleBookmarkClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onBookmark(this.props.status); - } + onBookmark(status); + }; - handleReblogClick: React.EventHandler = e => { - const { me, onReblog, onOpenUnauthorizedModal, status } = this.props; + const handleReblogClick: React.EventHandler = e => { e.stopPropagation(); if (me) { @@ -229,83 +177,83 @@ class StatusActionBar extends ImmutablePureComponent = (e) => { + const handleQuoteClick: React.EventHandler = (e) => { e.stopPropagation(); - const { me, onQuote, onOpenUnauthorizedModal, status } = this.props; + if (me) { onQuote(status); } else { onOpenUnauthorizedModal('REBLOG'); } - } + }; - handleDeleteClick: React.EventHandler = (e) => { + const handleDeleteClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onDelete(this.props.status); - } + onDelete(status); + }; - handleRedraftClick: React.EventHandler = (e) => { + const handleRedraftClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onDelete(this.props.status, true); - } + onDelete(status, true); + }; - handleEditClick: React.EventHandler = () => { - this.props.onEdit(this.props.status); - } + const handleEditClick: React.EventHandler = () => { + onEdit(status); + }; - handlePinClick: React.EventHandler = (e) => { + const handlePinClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onPin(this.props.status); - } + onPin(status); + }; - handleMentionClick: React.EventHandler = (e) => { + const handleMentionClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onMention(this.props.status.account); - } + onMention(status.account); + }; - handleDirectClick: React.EventHandler = (e) => { + const handleDirectClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onDirect(this.props.status.account); - } + onDirect(status.account); + }; - handleChatClick: React.EventHandler = (e) => { + const handleChatClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onChat(this.props.status.account, this.props.history); - } + onChat(status.account, history); + }; - handleMuteClick: React.EventHandler = (e) => { + const handleMuteClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onMute(this.props.status.account); - } + onMute(status.account); + }; - handleBlockClick: React.EventHandler = (e) => { + const handleBlockClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onBlock(this.props.status); - } + onBlock(status); + }; - handleOpen: React.EventHandler = (e) => { + const handleOpen: React.EventHandler = (e) => { e.stopPropagation(); - this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/posts/${this.props.status.id}`); - } + history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}`); + }; - handleEmbed = () => { - this.props.onEmbed(this.props.status); - } + const handleEmbed = () => { + onEmbed(status); + }; - handleReport: React.EventHandler = (e) => { + const handleReport: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onReport(this.props.status); - } + onReport(status); + }; - handleConversationMuteClick: React.EventHandler = (e) => { + const handleConversationMuteClick: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onMuteConversation(this.props.status); - } + onMuteConversation(status); + }; - handleCopy: React.EventHandler = (e) => { - const { url } = this.props.status; + const handleCopy: React.EventHandler = (e) => { + const { url } = status; const textarea = document.createElement('textarea'); e.stopPropagation(); @@ -323,53 +271,29 @@ class StatusActionBar extends ImmutablePureComponent = (e) => { - // const { status } = this.props; - // - // e.stopPropagation(); - // - // this.props.onGroupRemoveAccount(status.getIn(['group', 'id']), status.getIn(['account', 'id'])); - // } - // - // handleGroupRemovePost: React.EventHandler = (e) => { - // const { status } = this.props; - // - // e.stopPropagation(); - // - // this.props.onGroupRemoveStatus(status.getIn(['group', 'id']), status.id); - // } - - handleDeactivateUser: React.EventHandler = (e) => { + const handleDeactivateUser: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onDeactivateUser(this.props.status); - } + onDeactivateUser(status); + }; - handleDeleteUser: React.EventHandler = (e) => { + const handleDeleteUser: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onDeleteUser(this.props.status); - } + onDeleteUser(status); + }; - handleDeleteStatus: React.EventHandler = (e) => { + const handleDeleteStatus: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onDeleteStatus(this.props.status); - } + onDeleteStatus(status); + }; - handleToggleStatusSensitivity: React.EventHandler = (e) => { + const handleToggleStatusSensitivity: React.EventHandler = (e) => { e.stopPropagation(); - this.props.onToggleStatusSensitivity(this.props.status); - } + onToggleStatusSensitivity(status); + }; - handleOpenReblogsModal = () => { - const { me, status, onOpenUnauthorizedModal, onOpenReblogsModal } = this.props; - - if (!me) onOpenUnauthorizedModal(); - else onOpenReblogsModal(String(status.getIn(['account', 'acct'])), status.id); - } - - _makeMenu = (publicStatus: boolean) => { - const { status, intl, withDismiss, me, features, isStaff, isAdmin } = this.props; + const _makeMenu = (publicStatus: boolean) => { const mutingConversation = status.muted; const ownAccount = status.getIn(['account', 'id']) === me; const username = String(status.getIn(['account', 'username'])); @@ -378,21 +302,21 @@ class StatusActionBar extends ImmutablePureComponent { - this.node = c; - } - - componentDidMount() { - document.addEventListener('click', (e) => { - if (this.node && !this.node.contains(e.target as Node)) - this.setState({ emojiSelectorVisible: false }); - }); - } - - render() { - const { status, intl, allowedEmoji, features, me } = this.props; - - const publicStatus = ['public', 'unlisted'].includes(status.visibility); - - const replyCount = status.replies_count; - const reblogCount = status.reblogs_count; - const favouriteCount = status.favourites_count; - - const emojiReactCount = reduceEmoji( - (status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList, - favouriteCount, - status.favourited, - allowedEmoji, - ).reduce((acc, cur) => acc + cur.get('count'), 0); - - const meEmojiReact = getReactForStatus(status, allowedEmoji) as keyof typeof reactMessages | undefined; - - const reactMessages = { - '👍': messages.reactionLike, - '❤️': messages.reactionHeart, - '😆': messages.reactionLaughing, - '😮': messages.reactionOpenMouth, - '😢': messages.reactionCry, - '😩': messages.reactionWeary, - '': messages.favourite, - }; - - const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite); - - const menu = this._makeMenu(publicStatus); - let reblogIcon = require('@tabler/icons/repeat.svg'); - let replyTitle; - - if (status.visibility === 'direct') { - reblogIcon = require('@tabler/icons/mail.svg'); - } else if (status.visibility === 'private') { - reblogIcon = require('@tabler/icons/lock.svg'); - } - - const reblogMenu = [{ - text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), - action: this.handleReblogClick, - icon: require('@tabler/icons/repeat.svg'), - }, { - text: intl.formatMessage(messages.quotePost), - action: this.handleQuoteClick, - icon: require('@tabler/icons/quote.svg'), - }]; - - const reblogButton = ( - - ); - - if (!status.in_reply_to_id) { - replyTitle = intl.formatMessage(messages.reply); - } else { - replyTitle = intl.formatMessage(messages.replyAll); - } - - const canShare = ('share' in navigator) && status.visibility === 'public'; - - return ( -
- - - {(features.quotePosts && me) ? ( - - {reblogButton} - - ) : ( - reblogButton - )} - - {features.emojiReacts ? ( - - - - ) : ( - - )} - - {canShare && ( - - )} - - - - -
- ); - } - -} - -const mapStateToProps = (state: RootState) => { - const { me, instance } = state; - const account = state.accounts.get(me); - - return { - me, - isStaff: account ? account.staff : false, - isAdmin: account ? account.admin : false, - features: getFeatures(instance), }; + + const publicStatus = ['public', 'unlisted'].includes(status.visibility); + + const replyCount = status.replies_count; + const reblogCount = status.reblogs_count; + const favouriteCount = status.favourites_count; + + const emojiReactCount = reduceEmoji( + (status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList, + favouriteCount, + status.favourited, + allowedEmoji, + ).reduce((acc, cur) => acc + cur.get('count'), 0); + + const meEmojiReact = getReactForStatus(status, allowedEmoji) as keyof typeof reactMessages | undefined; + + const reactMessages = { + '👍': messages.reactionLike, + '❤️': messages.reactionHeart, + '😆': messages.reactionLaughing, + '😮': messages.reactionOpenMouth, + '😢': messages.reactionCry, + '😩': messages.reactionWeary, + '': messages.favourite, + }; + + const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite); + + const menu = _makeMenu(publicStatus); + let reblogIcon = require('@tabler/icons/repeat.svg'); + let replyTitle; + + if (status.visibility === 'direct') { + reblogIcon = require('@tabler/icons/mail.svg'); + } else if (status.visibility === 'private') { + reblogIcon = require('@tabler/icons/lock.svg'); + } + + const reblogMenu = [{ + text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), + action: handleReblogClick, + icon: require('@tabler/icons/repeat.svg'), + }, { + text: intl.formatMessage(messages.quotePost), + action: handleQuoteClick, + icon: require('@tabler/icons/quote.svg'), + }]; + + const reblogButton = ( + + ); + + if (!status.in_reply_to_id) { + replyTitle = intl.formatMessage(messages.reply); + } else { + replyTitle = intl.formatMessage(messages.replyAll); + } + + const canShare = ('share' in navigator) && status.visibility === 'public'; + + return ( +
+ + + {(features.quotePosts && me) ? ( + + {reblogButton} + + ) : ( + reblogButton + )} + + {features.emojiReacts ? ( + + + + ) : ( + + )} + + {canShare && ( + + )} + + + + +
+ ); }; -const mapDispatchToProps = (dispatch: Dispatch, { status }: { status: Status}) => ({ - dispatch, - onOpenUnauthorizedModal(action: AnyAction) { - dispatch(openModal('UNAUTHORIZED', { - action, - ap_id: status.url, - })); - }, - onOpenReblogsModal(username: string, statusId: string) { - dispatch(openModal('REBLOGS', { - username, - statusId, - })); - }, -}); - -const WrappedComponent = withRouter(injectIntl(StatusActionBar)); -// @ts-ignore -export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(WrappedComponent); +export default StatusActionBar;