diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 92094a258..b4546336e 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -102,7 +102,6 @@ const Status: React.FC = (props) => { const node = useRef(null); const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); - const [emojiSelectorFocused, setEmojiSelectorFocused] = useState(false); const actualStatus = getActualStatus(status); @@ -192,12 +191,7 @@ const Status: React.FC = (props) => { _expandEmojiSelector(); }; - const handleEmojiSelectorUnfocus = (): void => { - setEmojiSelectorFocused(false); - }; - const _expandEmojiSelector = (): void => { - setEmojiSelectorFocused(true); const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); firstEmoji?.focus(); }; @@ -397,13 +391,7 @@ const Status: React.FC = (props) => { {quote} {!hideActionBar && ( - // @ts-ignore - + )} diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index 6bd69d74e..278bb3c3f 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -1,19 +1,26 @@ import { List as ImmutableList } from 'immutable'; import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; +import { blockAccount } from 'soapbox/actions/accounts'; +import { showAlertForError } from 'soapbox/actions/alerts'; +import { launchChat } from 'soapbox/actions/chats'; +import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose'; +import { bookmark, favourite, pin, reblog, unbookmark, unfavourite, unpin, unreblog } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; +import { deactivateUserModal, deleteStatusModal, deleteUserModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation'; +import { initMuteModal } from 'soapbox/actions/mutes'; +import { initReport } from 'soapbox/actions/reports'; +import { deleteStatus, editStatus, muteStatus, unmuteStatus } from 'soapbox/actions/statuses'; 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 { useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks'; import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts'; -import type { History } from 'history'; import type { Menu } from 'soapbox/components/dropdown_menu'; -import type { Status } from 'soapbox/types/entities'; +import type { Account, Status } from 'soapbox/types/entities'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -59,75 +66,43 @@ const messages = defineMessages({ reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' }, reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' }, quotePost: { id: 'status.quote', defaultMessage: 'Quote post' }, + deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, + deleteHeading: { id: 'confirmations.delete.heading', defaultMessage: 'Delete post' }, + deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this post?' }, + redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' }, + blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); interface IStatusActionBar { status: Status, - onReply: (status: Status) => void, - onFavourite: (status: Status) => void, - onEmojiReact: (status: Status, emoji: string) => void, - onBookmark: (status: Status) => void, - onReblog: (status: Status, e: React.MouseEvent) => void, - onQuote: (status: Status) => void, - onDelete: (status: Status, redraft?: boolean) => void, - onEdit: (status: Status) => void, - onDirect: (account: any) => void, - onChat: (account: any, history: History) => void, - onMention: (account: any) => void, - onMute: (account: any) => void, - onBlock: (status: Status) => void, - onReport: (status: Status) => void, - onEmbed: (status: Status) => void, - onDeactivateUser: (status: Status) => void, - onDeleteUser: (status: Status) => void, - onToggleStatusSensitivity: (status: Status) => void, - onDeleteStatus: (status: Status) => void, - onMuteConversation: (status: Status) => void, - onPin: (status: Status) => void, withDismiss?: boolean, - withGroupAdmin?: boolean, - allowedEmoji: ImmutableList, - emojiSelectorFocused: boolean, - handleEmojiSelectorUnfocus: () => void, - handleEmojiSelectorExpand?: React.EventHandler, } -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 StatusActionBar: React.FC = ({ status, withDismiss = false }) => { const intl = useIntl(); const history = useHistory(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const me = useAppSelector(state => state.me); const features = useFeatures(); + const settings = useSettings(); + const soapboxConfig = useSoapboxConfig(); + + const { allowedEmoji } = soapboxConfig; const account = useOwnAccount(); const isStaff = account ? account.staff : false; const isAdmin = account ? account.admin : false; + if (!status) { + return null; + } + const onOpenUnauthorizedModal = (action?: string) => { dispatch(openModal('UNAUTHORIZED', { action, @@ -137,7 +112,18 @@ const StatusActionBar: React.FC = ({ const handleReplyClick: React.MouseEventHandler = (e) => { if (me) { - onReply(status); + dispatch((_, getState) => { + const state = getState(); + if (state.compose.text.trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status)), + })); + } else { + dispatch(replyCompose(status)); + } + }); } else { onOpenUnauthorizedModal('REPLY'); } @@ -156,7 +142,11 @@ const StatusActionBar: React.FC = ({ const handleFavouriteClick: React.EventHandler = (e) => { if (me) { - onFavourite(status); + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } } else { onOpenUnauthorizedModal('FAVOURITE'); } @@ -166,14 +156,32 @@ const StatusActionBar: React.FC = ({ const handleBookmarkClick: React.EventHandler = (e) => { e.stopPropagation(); - onBookmark(status); + + if (status.get('bookmarked')) { + dispatch(unbookmark(status)); + } else { + dispatch(bookmark(status)); + } + }; + + const modalReblog = () => { + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else { + dispatch(reblog(status)); + } }; const handleReblogClick: React.EventHandler = e => { e.stopPropagation(); if (me) { - onReblog(status, e); + const boostModal = settings.get('boostModal'); + if ((e && e.shiftKey) || !boostModal) { + modalReblog(); + } else { + dispatch(openModal('BOOST', { status, onReblog: modalReblog })); + } } else { onOpenUnauthorizedModal('REBLOG'); } @@ -183,54 +191,101 @@ const StatusActionBar: React.FC = ({ e.stopPropagation(); if (me) { - onQuote(status); + dispatch((_, getState) => { + const state = getState(); + if (state.compose.text.trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(quoteCompose(status)), + })); + } else { + dispatch(quoteCompose(status)); + } + }); } else { onOpenUnauthorizedModal('REBLOG'); } }; + const doDeleteStatus = (withRedraft = false) => { + dispatch((_, getState) => { + const deleteModal = settings.get('deleteModal'); + if (!deleteModal) { + dispatch(deleteStatus(status.id, withRedraft)); + } else { + dispatch(openModal('CONFIRM', { + icon: withRedraft ? require('@tabler/icons/edit.svg') : require('@tabler/icons/trash.svg'), + heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading), + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.id, withRedraft)), + })); + } + }); + }; + const handleDeleteClick: React.EventHandler = (e) => { e.stopPropagation(); - onDelete(status); + doDeleteStatus(); }; const handleRedraftClick: React.EventHandler = (e) => { e.stopPropagation(); - onDelete(status, true); + doDeleteStatus(true); }; const handleEditClick: React.EventHandler = () => { - onEdit(status); + dispatch(editStatus(status.id)); }; const handlePinClick: React.EventHandler = (e) => { e.stopPropagation(); - onPin(status); + + if (status.get('pinned')) { + dispatch(unpin(status)); + } else { + dispatch(pin(status)); + } }; const handleMentionClick: React.EventHandler = (e) => { e.stopPropagation(); - onMention(status.account); + dispatch(mentionCompose(status.account as Account)); }; const handleDirectClick: React.EventHandler = (e) => { e.stopPropagation(); - onDirect(status.account); + dispatch(directCompose(status.account as Account)); }; const handleChatClick: React.EventHandler = (e) => { e.stopPropagation(); - onChat(status.account, history); + const account = status.account as Account; + dispatch(launchChat(account.id, history)); }; const handleMuteClick: React.EventHandler = (e) => { e.stopPropagation(); - onMute(status.account); + dispatch(initMuteModal(status.account as Account)); }; const handleBlockClick: React.EventHandler = (e) => { e.stopPropagation(); - onBlock(status); + + const account = status.get('account') as Account; + dispatch(openModal('CONFIRM', { + icon: require('@tabler/icons/ban.svg'), + heading: , + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.blockConfirm), + onConfirm: () => dispatch(blockAccount(account.id)), + secondary: intl.formatMessage(messages.blockAndReport), + onSecondary: () => { + dispatch(blockAccount(account.id)); + dispatch(initReport(account, status)); + }, + })); }; const handleOpen: React.EventHandler = (e) => { @@ -239,17 +294,25 @@ const StatusActionBar: React.FC = ({ }; const handleEmbed = () => { - onEmbed(status); + dispatch(openModal('EMBED', { + url: status.get('url'), + onError: (error: any) => dispatch(showAlertForError(error)), + })); }; const handleReport: React.EventHandler = (e) => { e.stopPropagation(); - onReport(status); + dispatch(initReport(status.account as Account, status)); }; const handleConversationMuteClick: React.EventHandler = (e) => { e.stopPropagation(); - onMuteConversation(status); + + if (status.get('muted')) { + dispatch(unmuteStatus(status.id)); + } else { + dispatch(muteStatus(status.id)); + } }; const handleCopy: React.EventHandler = (e) => { @@ -275,22 +338,22 @@ const StatusActionBar: React.FC = ({ const handleDeactivateUser: React.EventHandler = (e) => { e.stopPropagation(); - onDeactivateUser(status); + dispatch(deactivateUserModal(intl, status.getIn(['account', 'id']) as string)); }; const handleDeleteUser: React.EventHandler = (e) => { e.stopPropagation(); - onDeleteUser(status); + dispatch(deleteUserModal(intl, status.getIn(['account', 'id']) as string)); }; const handleDeleteStatus: React.EventHandler = (e) => { e.stopPropagation(); - onDeleteStatus(status); + dispatch(deleteStatusModal(intl, status.id)); }; const handleToggleStatusSensitivity: React.EventHandler = (e) => { e.stopPropagation(); - onToggleStatusSensitivity(status); + dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); }; const _makeMenu = (publicStatus: boolean) => { diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 1f8f3d4e0..42e4e82bd 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -3,46 +3,25 @@ import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immuta import { debounce } from 'lodash'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { HotKeys } from 'react-hotkeys'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { createSelector } from 'reselect'; -import { blockAccount } from 'soapbox/actions/accounts'; -import { launchChat } from 'soapbox/actions/chats'; import { replyCompose, mentionCompose, - directCompose, - quoteCompose, } from 'soapbox/actions/compose'; -import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; import { favourite, unfavourite, reblog, unreblog, - bookmark, - unbookmark, - pin, - unpin, } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; -import { - deactivateUserModal, - deleteUserModal, - deleteStatusModal, - toggleStatusSensitivityModal, -} from 'soapbox/actions/moderation'; -import { initMuteModal } from 'soapbox/actions/mutes'; -import { initReport } from 'soapbox/actions/reports'; import { getSettings } from 'soapbox/actions/settings'; import { - muteStatus, - unmuteStatus, - deleteStatus, hideStatus, revealStatus, - editStatus, fetchStatusWithContext, fetchNext, } from 'soapbox/actions/statuses'; @@ -55,7 +34,7 @@ import Tombstone from 'soapbox/components/tombstone'; import { Column, Stack } from 'soapbox/components/ui'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; import PendingStatus from 'soapbox/features/ui/components/pending_status'; -import { useAppDispatch, useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; import { makeGetStatus } from 'soapbox/selectors'; import { defaultMediaVisibility, textForScreenReader } from 'soapbox/utils/status'; @@ -63,7 +42,6 @@ import DetailedStatus from './components/detailed-status'; import ThreadLoginCta from './components/thread-login-cta'; import ThreadStatus from './components/thread-status'; -import type { History } from 'history'; import type { VirtuosoHandle } from 'react-virtuoso'; import type { RootState } from 'soapbox/store'; import type { @@ -153,12 +131,10 @@ const Thread: React.FC = (props) => { const dispatch = useAppDispatch(); const settings = useSettings(); - const soapboxConfig = useSoapboxConfig(); const me = useAppSelector(state => state.me); const status = useAppSelector(state => getStatus(state, { id: props.params.statusId })); const displayMedia = settings.get('displayMedia') as DisplayMedia; - const allowedEmoji = soapboxConfig.allowedEmoji; const askReplyConfirmation = useAppSelector(state => state.compose.text.trim().length !== 0); const { ancestorsIds, descendantsIds } = useAppSelector(state => { @@ -181,7 +157,6 @@ const Thread: React.FC = (props) => { }); const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); - const [emojiSelectorFocused, setEmojiSelectorFocused] = useState(false); const [isLoaded, setIsLoaded] = useState(!!status); const [next, setNext] = useState(); @@ -210,8 +185,11 @@ const Thread: React.FC = (props) => { setShowMedia(!showMedia); }; - const handleEmojiReactClick = (status: StatusEntity, emoji: string) => { - dispatch(simpleEmojiReact(status, emoji)); + const handleHotkeyReact = () => { + if (statusRef.current) { + const firstEmoji: HTMLButtonElement | null = statusRef.current.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); + firstEmoji?.focus(); + } }; const handleFavouriteClick = (status: StatusEntity) => { @@ -222,22 +200,6 @@ const Thread: React.FC = (props) => { } }; - const handlePin = (status: StatusEntity) => { - if (status.pinned) { - dispatch(unpin(status)); - } else { - dispatch(pin(status)); - } - }; - - const handleBookmark = (status: StatusEntity) => { - if (status.bookmarked) { - dispatch(unbookmark(status)); - } else { - dispatch(bookmark(status)); - } - }; - const handleReplyClick = (status: StatusEntity) => { if (askReplyConfirmation) { dispatch(openModal('CONFIRM', { @@ -269,47 +231,6 @@ const Thread: React.FC = (props) => { }); }; - const handleQuoteClick = (status: StatusEntity) => { - if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: () => dispatch(quoteCompose(status)), - })); - } else { - dispatch(quoteCompose(status)); - } - }; - - const handleDeleteClick = (status: StatusEntity, withRedraft = false) => { - dispatch((_, getState) => { - const deleteModal = getSettings(getState()).get('deleteModal'); - if (!deleteModal) { - dispatch(deleteStatus(status.id, withRedraft)); - } else { - dispatch(openModal('CONFIRM', { - icon: withRedraft ? require('@tabler/icons/edit.svg') : require('@tabler/icons/trash.svg'), - heading: intl.formatMessage(withRedraft ? messages.redraftHeading : messages.deleteHeading), - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.id, withRedraft)), - })); - } - }); - }; - - const handleEditClick = (status: StatusEntity) => { - dispatch(editStatus(status.id)); - }; - - const handleDirectClick = (account: AccountEntity) => { - dispatch(directCompose(account)); - }; - - const handleChatClick = (account: AccountEntity, router: History) => { - dispatch(launchChat(account.id, router)); - }; - const handleMentionClick = (account: AccountEntity) => { dispatch(mentionCompose(account)); }; @@ -337,18 +258,6 @@ const Thread: React.FC = (props) => { } }; - const handleMuteClick = (account: AccountEntity) => { - dispatch(initMuteModal(account)); - }; - - const handleConversationMuteClick = (status: StatusEntity) => { - if (status.muted) { - dispatch(unmuteStatus(status.id)); - } else { - dispatch(muteStatus(status.id)); - } - }; - const handleToggleHidden = (status: StatusEntity) => { if (status.hidden) { dispatch(revealStatus(status.id)); @@ -357,48 +266,6 @@ const Thread: React.FC = (props) => { } }; - const handleBlockClick = (status: StatusEntity) => { - const { account } = status; - if (!account || typeof account !== 'object') return; - - dispatch(openModal('CONFIRM', { - icon: require('@tabler/icons/ban.svg'), - heading: , - message: @{account.acct} }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.id)), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.id)); - dispatch(initReport(account, status)); - }, - })); - }; - - const handleReport = (status: StatusEntity) => { - dispatch(initReport(status.account as AccountEntity, status)); - }; - - const handleEmbed = (status: StatusEntity) => { - dispatch(openModal('EMBED', { url: status.url })); - }; - - const handleDeactivateUser = (status: StatusEntity) => { - dispatch(deactivateUserModal(intl, status.getIn(['account', 'id']) as string)); - }; - - const handleDeleteUser = (status: StatusEntity) => { - dispatch(deleteUserModal(intl, status.getIn(['account', 'id']) as string)); - }; - - const handleToggleStatusSensitivity = (status: StatusEntity) => { - dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); - }; - - const handleDeleteStatus = (status: StatusEntity) => { - dispatch(deleteStatusModal(intl, status.id)); - }; - const handleHotkeyMoveUp = () => { handleMoveUp(status!.id); }; @@ -439,10 +306,6 @@ const Thread: React.FC = (props) => { handleToggleMediaVisibility(); }; - const handleHotkeyReact = () => { - _expandEmojiSelector(); - }; - const handleMoveUp = (id: string) => { if (id === status?.id) { _selectChild(ancestorsIds.size - 1); @@ -473,25 +336,6 @@ const Thread: React.FC = (props) => { } }; - const handleEmojiSelectorExpand: React.EventHandler = e => { - if (e.key === 'Enter') { - _expandEmojiSelector(); - } - e.preventDefault(); - }; - - const handleEmojiSelectorUnfocus = () => { - setEmojiSelectorFocused(false); - }; - - const _expandEmojiSelector = () => { - if (statusRef.current) { - setEmojiSelectorFocused(true); - const firstEmoji: HTMLButtonElement | null = statusRef.current.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); - firstEmoji?.focus(); - } - }; - const _selectChild = (index: number) => { scroller.current?.scrollIntoView({ index, @@ -640,34 +484,7 @@ const Thread: React.FC = (props) => {
- +