From ca4821abf7c0db73b9596433453d16a7972aa222 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 9 Aug 2022 18:40:33 -0500 Subject: [PATCH] Nuke ActionBar component --- app/soapbox/components/status-action-bar.tsx | 25 +- .../components/status-action-button.tsx | 60 +- .../features/status/components/action-bar.tsx | 654 ------------------ app/soapbox/features/status/index.tsx | 2 +- 4 files changed, 60 insertions(+), 681 deletions(-) delete mode 100644 app/soapbox/features/status/components/action-bar.tsx diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index d8c27e3a3..76e4c5e83 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -81,9 +81,16 @@ const messages = defineMessages({ interface IStatusActionBar { status: Status, withDismiss?: boolean, + withLabels?: boolean, + expandable?: boolean } -const StatusActionBar: React.FC = ({ status, withDismiss = false }) => { +const StatusActionBar: React.FC = ({ + status, + withDismiss = false, + withLabels = false, + expandable = true, +}) => { const intl = useIntl(); const history = useHistory(); const dispatch = useAppDispatch(); @@ -337,11 +344,13 @@ const StatusActionBar: React.FC = ({ status, withDismiss = fal const menu: Menu = []; - menu.push({ - text: intl.formatMessage(messages.open), - action: handleOpen, - icon: require('@tabler/icons/arrows-vertical.svg'), - }); + if (expandable) { + menu.push({ + text: intl.formatMessage(messages.open), + action: handleOpen, + icon: require('@tabler/icons/arrows-vertical.svg'), + }); + } if (publicStatus) { menu.push({ @@ -562,6 +571,7 @@ const StatusActionBar: React.FC = ({ status, withDismiss = fal active={status.reblogged} onClick={handleReblogClick} count={reblogCount} + text={withLabels ? intl.formatMessage(messages.reblog) : undefined} /> ); @@ -580,6 +590,7 @@ const StatusActionBar: React.FC = ({ status, withDismiss = fal icon={require('@tabler/icons/message-circle-2.svg')} onClick={handleReplyClick} count={replyCount} + text={withLabels ? intl.formatMessage(messages.reply) : undefined} /> {(features.quotePosts && me) ? ( @@ -604,6 +615,7 @@ const StatusActionBar: React.FC = ({ status, withDismiss = fal active={Boolean(meEmojiReact)} count={emojiReactCount} emoji={meEmojiReact} + text={withLabels ? meEmojiTitle : undefined} /> ) : ( @@ -615,6 +627,7 @@ const StatusActionBar: React.FC = ({ status, withDismiss = fal onClick={handleFavouriteClick} active={Boolean(meEmojiReact)} count={favouriteCount} + text={withLabels ? meEmojiTitle : undefined} /> )} diff --git a/app/soapbox/components/status-action-button.tsx b/app/soapbox/components/status-action-button.tsx index 1a625b5a1..157271baf 100644 --- a/app/soapbox/components/status-action-button.tsx +++ b/app/soapbox/components/status-action-button.tsx @@ -32,10 +32,47 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes((props, ref): JSX.Element => { - const { icon, className, iconClassName, active, color, filled = false, count = 0, emoji, ...filteredProps } = props; + const { icon, className, iconClassName, active, color, filled = false, count = 0, emoji, text, ...filteredProps } = props; + + const renderIcon = () => { + if (emoji) { + return ( + + + + ); + } else { + return ( + + ); + } + }; + + const renderText = () => { + if (text) { + return ( + + {text} + + ); + } else if (count) { + return ( + + ); + } + }; return ( ); }); diff --git a/app/soapbox/features/status/components/action-bar.tsx b/app/soapbox/features/status/components/action-bar.tsx deleted file mode 100644 index b0560693c..000000000 --- a/app/soapbox/features/status/components/action-bar.tsx +++ /dev/null @@ -1,654 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import { defineMessages, injectIntl, WrappedComponentProps as IntlComponentProps } from 'react-intl'; -import { connect } from 'react-redux'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; - -import { openModal } from 'soapbox/actions/modals'; -import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper'; -import { HStack, IconButton, Emoji, Text } from 'soapbox/components/ui'; -import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; -import { isUserTouching } from 'soapbox/is_mobile'; -import { getReactForStatus } from 'soapbox/utils/emoji_reacts'; -import { getFeatures } from 'soapbox/utils/features'; - -import type { History } from 'history'; -import type { List as ImmutableList } from 'immutable'; -import type { AnyAction } from 'redux'; -import type { ThunkDispatch } from 'redux-thunk'; -import type { Menu } from 'soapbox/components/dropdown_menu'; -import type { RootState } from 'soapbox/store'; -import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; - -type Dispatch = ThunkDispatch; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, - edit: { id: 'status.edit', defaultMessage: 'Edit' }, - direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, - chat: { id: 'status.chat', defaultMessage: 'Chat with @{name}' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, - reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, - cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Like' }, - mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, - muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, - unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, - block: { id: 'status.block', defaultMessage: 'Block @{name}' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' }, - share: { id: 'status.share', defaultMessage: 'Share' }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Embed' }, - admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, - admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' }, - copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, - bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, - unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' }, - deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' }, - deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' }, - deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' }, - markStatusSensitive: { id: 'admin.statuses.actions.mark_status_sensitive', defaultMessage: 'Mark post sensitive' }, - markStatusNotSensitive: { id: 'admin.statuses.actions.mark_status_not_sensitive', defaultMessage: 'Mark post not sensitive' }, - reactionLike: { id: 'status.reactions.like', defaultMessage: 'Like' }, - reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' }, - reactionLaughing: { id: 'status.reactions.laughing', defaultMessage: 'Haha' }, - reactionOpenMouth: { id: 'status.reactions.open_mouth', defaultMessage: 'Wow' }, - reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' }, - reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' }, - emojiPickerExpand: { id: 'status.reactions_expand', defaultMessage: 'Select emoji' }, - more: { id: 'status.actions.more', defaultMessage: 'More' }, - quotePost: { id: 'status.quote', defaultMessage: 'Quote post' }, -}); - -const mapStateToProps = (state: RootState) => { - const me = state.me; - const account = state.accounts.get(me); - const instance = state.instance; - - return { - me, - isStaff: account ? account.staff : false, - isAdmin: account ? account.admin : false, - features: getFeatures(instance), - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch, { status }: OwnProps) => ({ - onOpenUnauthorizedModal(action: string) { - dispatch(openModal('UNAUTHORIZED', { - action, - ap_id: status.url, - })); - }, -}); - -interface OwnProps { - status: StatusEntity, - onReply: (status: StatusEntity) => void, - onReblog: (status: StatusEntity, e: React.MouseEvent) => void, - onQuote: (status: StatusEntity) => void, - onFavourite: (status: StatusEntity) => void, - onEmojiReact: (status: StatusEntity, emoji: string) => void, - onDelete: (status: StatusEntity, redraft?: boolean) => void, - onEdit: (status: StatusEntity) => void, - onBookmark: (status: StatusEntity) => void, - onDirect: (account: AccountEntity) => void, - onChat: (account: AccountEntity, history: History) => void, - onMention: (account: AccountEntity) => void, - onMute: (account: AccountEntity) => void, - onMuteConversation: (status: StatusEntity) => void, - onBlock: (status: StatusEntity) => void, - onReport: (status: StatusEntity) => void, - onPin: (status: StatusEntity) => void, - onEmbed: (status: StatusEntity) => void, - onDeactivateUser: (status: StatusEntity) => void, - onDeleteUser: (status: StatusEntity) => void, - onDeleteStatus: (status: StatusEntity) => void, - onToggleStatusSensitivity: (status: StatusEntity) => void, - allowedEmoji: ImmutableList, - emojiSelectorFocused: boolean, - handleEmojiSelectorExpand: React.EventHandler, - handleEmojiSelectorUnfocus: React.EventHandler, -} - -type StateProps = ReturnType; -type DispatchProps = ReturnType; - -type IActionBar = OwnProps & StateProps & DispatchProps & RouteComponentProps & IntlComponentProps; - -interface IActionBarState { - emojiSelectorVisible: boolean, - emojiSelectorFocused: boolean, -} - -class ActionBar extends React.PureComponent { - - static defaultProps: Partial = { - isStaff: false, - } - - state = { - emojiSelectorVisible: false, - emojiSelectorFocused: false, - } - - node: HTMLDivElement | null = null; - - handleReplyClick: React.EventHandler = (e) => { - const { me, onReply, onOpenUnauthorizedModal } = this.props; - e.preventDefault(); - - if (me) { - onReply(this.props.status); - } else { - onOpenUnauthorizedModal('REPLY'); - } - } - - handleReblogClick: React.EventHandler = (e) => { - const { me, onReblog, onOpenUnauthorizedModal, status } = this.props; - e.preventDefault(); - - if (me) { - onReblog(status, e); - } else { - onOpenUnauthorizedModal('REBLOG'); - } - } - - handleQuoteClick: React.EventHandler = () => { - const { me, onQuote, onOpenUnauthorizedModal, status } = this.props; - if (me) { - onQuote(status); - } else { - onOpenUnauthorizedModal('REBLOG'); - } - } - - handleBookmarkClick: React.EventHandler = () => { - this.props.onBookmark(this.props.status); - } - - handleFavouriteClick: React.EventHandler = (e) => { - const { me, onFavourite, onOpenUnauthorizedModal, status } = this.props; - - e.preventDefault(); - - if (me) { - onFavourite(status); - } else { - onOpenUnauthorizedModal('FAVOURITE'); - } - } - - 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 meEmojiReact = getReactForStatus(this.props.status, this.props.allowedEmoji) || '👍'; - - if (features.emojiReacts && isUserTouching()) { - if (this.state.emojiSelectorVisible) { - this.handleReactClick(meEmojiReact)(e); - } else { - this.setState({ emojiSelectorVisible: true }); - } - } else { - this.handleReactClick(meEmojiReact)(e); - } - } - - handleReactClick = (emoji: string): React.EventHandler => { - return () => { - const { me, onEmojiReact, onOpenUnauthorizedModal, status } = this.props; - if (me) { - onEmojiReact(status, emoji); - } else { - onOpenUnauthorizedModal('FAVOURITE'); - } - this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false }); - }; - } - - handleHotkeyEmoji = () => { - const { emojiSelectorVisible } = this.state; - - this.setState({ emojiSelectorVisible: !emojiSelectorVisible }); - } - - handleDeleteClick: React.EventHandler = () => { - this.props.onDelete(this.props.status); - } - - handleRedraftClick: React.EventHandler = () => { - this.props.onDelete(this.props.status, true); - } - - handleEditClick: React.EventHandler = () => { - this.props.onEdit(this.props.status); - } - - handleDirectClick: React.EventHandler = () => { - const { account } = this.props.status; - if (!account || typeof account !== 'object') return; - this.props.onDirect(account); - } - - handleChatClick: React.EventHandler = () => { - const { account } = this.props.status; - if (!account || typeof account !== 'object') return; - this.props.onChat(account, this.props.history); - } - - handleMentionClick: React.EventHandler = () => { - const { account } = this.props.status; - if (!account || typeof account !== 'object') return; - this.props.onMention(account); - } - - handleMuteClick: React.EventHandler = () => { - const { account } = this.props.status; - if (!account || typeof account !== 'object') return; - this.props.onMute(account); - } - - handleConversationMuteClick: React.EventHandler = () => { - this.props.onMuteConversation(this.props.status); - } - - handleBlockClick: React.EventHandler = () => { - this.props.onBlock(this.props.status); - } - - handleReport = () => { - this.props.onReport(this.props.status); - } - - handlePinClick: React.EventHandler = () => { - this.props.onPin(this.props.status); - } - - handleShare = () => { - navigator.share({ - text: this.props.status.search_index, - url: this.props.status.uri, - }); - } - - handleEmbed = () => { - this.props.onEmbed(this.props.status); - } - - handleCopy = () => { - const url = this.props.status.url; - const textarea = document.createElement('textarea'); - - textarea.textContent = url; - textarea.style.position = 'fixed'; - - document.body.appendChild(textarea); - - try { - textarea.select(); - document.execCommand('copy'); - } catch (e) { - // Do nothing - } finally { - document.body.removeChild(textarea); - } - } - - handleDeactivateUser = () => { - this.props.onDeactivateUser(this.props.status); - } - - handleDeleteUser = () => { - this.props.onDeleteUser(this.props.status); - } - - handleToggleStatusSensitivity = () => { - this.props.onToggleStatusSensitivity(this.props.status); - } - - handleDeleteStatus = () => { - this.props.onDeleteStatus(this.props.status); - } - - setRef: React.RefCallback = c => { - this.node = c; - } - - componentDidMount() { - document.addEventListener('click', e => { - if (this.node && !this.node.contains(e.target as Element)) - this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false }); - }); - } - - render() { - const { status, intl, me, isStaff, isAdmin, allowedEmoji, features } = this.props; - const ownAccount = status.getIn(['account', 'id']) === me; - const username = String(status.getIn(['account', 'acct'])); - - const publicStatus = ['public', 'unlisted'].includes(status.visibility); - const mutingConversation = status.muted; - - 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: Menu = []; - - if (publicStatus) { - menu.push({ - text: intl.formatMessage(messages.copy), - action: this.handleCopy, - icon: require('@tabler/icons/link.svg'), - }); - - if (features.embeds) { - menu.push({ - text: intl.formatMessage(messages.embed), - action: this.handleEmbed, - icon: require('@tabler/icons/share.svg'), - }); - } - } - - if (me) { - if (features.bookmarks) { - menu.push({ - text: intl.formatMessage(status.bookmarked ? messages.unbookmark : messages.bookmark), - action: this.handleBookmarkClick, - icon: status.bookmarked ? require('@tabler/icons/bookmark-off.svg') : require('@tabler/icons/bookmark.svg'), - }); - } - - menu.push(null); - - if (ownAccount) { - if (publicStatus) { - menu.push({ - text: intl.formatMessage(status.pinned ? messages.unpin : messages.pin), - action: this.handlePinClick, - icon: mutingConversation ? require('@tabler/icons/pinned-off.svg') : require('@tabler/icons/pin.svg'), - }); - - menu.push(null); - } else if (status.visibility === 'private') { - menu.push({ - text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog_private), - action: this.handleReblogClick, - icon: require('@tabler/icons/repeat.svg'), - }); - - menu.push(null); - } - - menu.push({ - text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), - action: this.handleConversationMuteClick, - icon: mutingConversation ? require('@tabler/icons/bell.svg') : require('@tabler/icons/bell-off.svg'), - }); - menu.push(null); - menu.push({ - text: intl.formatMessage(messages.delete), - action: this.handleDeleteClick, - icon: require('@tabler/icons/trash.svg'), - destructive: true, - }); - if (features.editStatuses) { - menu.push({ - text: intl.formatMessage(messages.edit), - action: this.handleEditClick, - icon: require('@tabler/icons/edit.svg'), - }); - } else { - menu.push({ - text: intl.formatMessage(messages.redraft), - action: this.handleRedraftClick, - icon: require('@tabler/icons/edit.svg'), - destructive: true, - }); - } - } else { - menu.push({ - text: intl.formatMessage(messages.mention, { name: username }), - action: this.handleMentionClick, - icon: require('@tabler/icons/at.svg'), - }); - - // if (status.getIn(['account', 'pleroma', 'accepts_chat_messages'], false) === true) { - // menu.push({ - // text: intl.formatMessage(messages.chat, { name: username }), - // action: this.handleChatClick, - // icon: require('@tabler/icons/messages.svg'), - // }); - // } else { - // menu.push({ - // text: intl.formatMessage(messages.direct, { name: username }), - // action: this.handleDirectClick, - // icon: require('@tabler/icons/mail.svg'), - // }); - // } - - menu.push(null); - menu.push({ - text: intl.formatMessage(messages.mute, { name: username }), - action: this.handleMuteClick, - icon: require('@tabler/icons/circle-x.svg'), - }); - menu.push({ - text: intl.formatMessage(messages.block, { name: username }), - action: this.handleBlockClick, - icon: require('@tabler/icons/ban.svg'), - }); - menu.push({ - text: intl.formatMessage(messages.report, { name: username }), - action: this.handleReport, - icon: require('@tabler/icons/flag.svg'), - }); - } - - if (isStaff) { - menu.push(null); - - if (isAdmin) { - menu.push({ - text: intl.formatMessage(messages.admin_account, { name: username }), - href: `/pleroma/admin/#/users/${status.getIn(['account', 'id'])}/`, - icon: require('@tabler/icons/gavel.svg'), - }); - menu.push({ - text: intl.formatMessage(messages.admin_status), - href: `/pleroma/admin/#/statuses/${status.id}/`, - icon: require('@tabler/icons/pencil.svg'), - }); - } - - menu.push({ - text: intl.formatMessage(status.sensitive === false ? messages.markStatusSensitive : messages.markStatusNotSensitive), - action: this.handleToggleStatusSensitivity, - icon: require('@tabler/icons/alert-triangle.svg'), - }); - - if (!ownAccount) { - menu.push({ - text: intl.formatMessage(messages.deactivateUser, { name: username }), - action: this.handleDeactivateUser, - icon: require('@tabler/icons/user-off.svg'), - }); - menu.push({ - text: intl.formatMessage(messages.deleteUser, { name: username }), - action: this.handleDeleteUser, - icon: require('@tabler/icons/user-minus.svg'), - destructive: true, - }); - menu.push({ - text: intl.formatMessage(messages.deleteStatus), - action: this.handleDeleteStatus, - icon: require('@tabler/icons/trash.svg'), - destructive: true, - }); - } - } - } - - const canShare = ('share' in navigator) && status.visibility === 'public'; - - let reblogIcon = require('@tabler/icons/repeat.svg'); - - if (status.visibility === 'direct') { - reblogIcon = require('@tabler/icons/mail.svg'); - } else if (status.visibility === 'private') { - reblogIcon = require('@tabler/icons/lock.svg'); - } - - const reblog_disabled = (status.visibility === 'direct' || status.visibility === 'private'); - - const reblogMenu: Menu = [{ - 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 = ( - - ); - - return ( - - - - {(features.quotePosts && me) ? ( - - {reblogButton} - - ) : ( - reblogButton - )} - - {features.emojiReacts ? ( - - {meEmojiReact ? ( - - ) : ( - - )} - - ) : ( - - )} - - {canShare && ( - - )} - - - - ); - } - -} - -const WrappedComponent = withRouter(injectIntl(ActionBar)); -export default connect(mapStateToProps, mapDispatchToProps)(WrappedComponent); diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 29bbcd842..cd38e56b8 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -484,7 +484,7 @@ const Thread: React.FC = (props) => {
- +