diff --git a/app/javascript/flavours/glitch/components/status/boost_button.tsx b/app/javascript/flavours/glitch/components/status/boost_button.tsx index 17ebcbc768..75d1e5bb75 100644 --- a/app/javascript/flavours/glitch/components/status/boost_button.tsx +++ b/app/javascript/flavours/glitch/components/status/boost_button.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import type { FC, KeyboardEvent, MouseEvent } from 'react'; +import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler } from 'react'; import { useIntl } from 'react-intl'; @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { quoteComposeById } from '@/flavours/glitch/actions/compose_typed'; import { toggleReblog } from '@/flavours/glitch/actions/interactions'; import { openModal } from '@/flavours/glitch/actions/modal'; +import { quickBoosting } from '@/flavours/glitch/initial_state'; import type { ActionMenuItem } from '@/flavours/glitch/models/dropdown_menu'; import type { Status } from '@/flavours/glitch/models/status'; import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; @@ -24,6 +25,55 @@ import { selectStatusState, } from './boost_button_utils'; +const StandaloneBoostButton: FC = ({ status, counters }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const statusState = useAppSelector((state) => + selectStatusState(state, status), + ); + const { title, meta, iconComponent, disabled } = useMemo( + () => boostItemState(statusState), + [statusState], + ); + + const handleClick: MouseEventHandler = useCallback( + (event) => { + if (statusState.isLoggedIn) { + dispatch(toggleReblog(status.get('id') as string, event.shiftKey)); + } else { + dispatch( + openModal({ + modalType: 'INTERACTION', + modalProps: { + accountId: status.getIn(['account', 'id']), + url: status.get('uri'), + }, + }), + ); + } + }, + [dispatch, status, statusState.isLoggedIn], + ); + + return ( + + ); +}; + const renderMenuItem: RenderItemFn = ( item, index, @@ -46,7 +96,7 @@ interface ReblogButtonProps { type ActionMenuItemWithIcon = SomeRequired; -export const BoostButton: FC = ({ status, counters }) => { +const BoostOrQuoteMenu: FC = ({ status, counters }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const statusState = useAppSelector((state) => @@ -188,3 +238,9 @@ const ReblogMenuItem: FC = ({ ); }; + +// Switch between the standalone boost button or the +// "Boost or quote" menu based on the quickBoosting preference +export const BoostButton = quickBoosting + ? StandaloneBoostButton + : BoostOrQuoteMenu; diff --git a/app/javascript/flavours/glitch/components/status_action_bar/index.jsx b/app/javascript/flavours/glitch/components/status_action_bar/index.jsx index 9284fb17f0..02505bd8fb 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar/index.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar/index.jsx @@ -22,12 +22,13 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import { Dropdown } from 'flavours/glitch/components/dropdown_menu'; -import { me } from '../../initial_state'; +import { me, quickBoosting } from '../../initial_state'; import { IconButton } from '../icon_button'; import { RelativeTimestamp } from '../relative_timestamp'; import { BoostButton } from '../status/boost_button'; import { RemoveQuoteHint } from './remove_quote_hint'; +import { quoteItemState, selectStatusState } from '../status/boost_button_utils'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -68,6 +69,7 @@ const mapStateToProps = (state, { status }) => { const quotedStatusId = status.getIn(['quote', 'quoted_status']); return ({ quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null, + statusQuoteState: selectStatusState(state, status), }); }; @@ -75,6 +77,7 @@ class StatusActionBar extends ImmutablePureComponent { static propTypes = { identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, + statusQuoteState: PropTypes.object, quotedAccountId: PropTypes.string, contextType: PropTypes.string, onReply: PropTypes.func, @@ -122,6 +125,10 @@ class StatusActionBar extends ImmutablePureComponent { } }; + handleQuoteClick = () => { + this.props.onQuote(this.props.status); + }; + handleShareClick = () => { navigator.share({ url: this.props.status.get('url'), @@ -214,7 +221,7 @@ class StatusActionBar extends ImmutablePureComponent { }; render () { - const { status, quotedAccountId, contextType, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props; + const { status, statusQuoteState, quotedAccountId, contextType, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props; const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -238,6 +245,20 @@ class StatusActionBar extends ImmutablePureComponent { if (publicStatus && 'share' in navigator) { menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick }); } + + if (quickBoosting && signedIn) { + const quoteItem = quoteItemState(statusQuoteState); + menu.push(null); + menu.push({ + text: intl.formatMessage(quoteItem.title), + description: quoteItem.meta + ? intl.formatMessage(quoteItem.meta) + : undefined, + disabled: quoteItem.disabled, + action: this.handleQuoteClick, + }); + menu.push(null); + } if (publicStatus && !isRemote) { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index 488916dc1e..256a269fd9 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -19,8 +19,9 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend import { IconButton } from '../../../components/icon_button'; import { Dropdown } from 'flavours/glitch/components/dropdown_menu'; -import { me } from '../../../initial_state'; +import { me, quickBoosting } from '../../../initial_state'; import { BoostButton } from '@/flavours/glitch/components/status/boost_button'; +import { quoteItemState, selectStatusState } from '@/flavours/glitch/components/status/boost_button_utils'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -56,6 +57,7 @@ const mapStateToProps = (state, { status }) => { const quotedStatusId = status.getIn(['quote', 'quoted_status']); return ({ quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null, + statusQuoteState: selectStatusState(state, status), }); }; @@ -63,6 +65,7 @@ class ActionBar extends PureComponent { static propTypes = { identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, + statusQuoteState: PropTypes.object, quotedAccountId: ImmutablePropTypes.string, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, @@ -107,6 +110,10 @@ class ActionBar extends PureComponent { this.props.onRevokeQuote(this.props.status); }; + handleQuoteClick = () => { + this.props.onQuote(this.props.status); + }; + handleQuotePolicyChange = () => { this.props.onQuotePolicyChange(this.props.status); }; @@ -163,7 +170,7 @@ class ActionBar extends PureComponent { }; render () { - const { status, quotedAccountId, intl } = this.props; + const { status, statusQuoteState, quotedAccountId, intl } = this.props; const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -184,6 +191,20 @@ class ActionBar extends PureComponent { menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare }); } + if (quickBoosting && signedIn) { + const quoteItem = quoteItemState(statusQuoteState); + menu.push(null); + menu.push({ + text: intl.formatMessage(quoteItem.title), + description: quoteItem.meta + ? intl.formatMessage(quoteItem.meta) + : undefined, + disabled: quoteItem.disabled, + action: this.handleQuoteClick, + }); + menu.push(null); + } + if (publicStatus && (signedIn || !isRemote)) { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } diff --git a/app/javascript/flavours/glitch/initial_state.ts b/app/javascript/flavours/glitch/initial_state.ts index 06d78125bb..359f0f6c01 100644 --- a/app/javascript/flavours/glitch/initial_state.ts +++ b/app/javascript/flavours/glitch/initial_state.ts @@ -9,6 +9,7 @@ interface InitialStateMeta { activity_api_enabled: boolean; admin: string; boost_modal?: boolean; + quick_boosting?: boolean; favourite_modal?: boolean; crop_images: boolean; delete_modal?: boolean; @@ -117,6 +118,7 @@ function getMeta( export const activityApiEnabled = getMeta('activity_api_enabled'); export const autoPlayGif = getMeta('auto_play_gif'); export const boostModal = getMeta('boost_modal'); +export const quickBoosting = getMeta('quick_boosting'); export const deleteModal = getMeta('delete_modal'); export const missingAltTextModal = getMeta('missing_alt_text_modal'); export const disableSwiping = getMeta('disable_swiping'); diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 3927f3f04b..060f54f607 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -225,6 +225,10 @@ code { list-style: disc; margin-inline-start: 18px; } + + .icon { + vertical-align: -3px; + } } ul.hint {