import { useCallback, useMemo } from 'react'; import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler, SVGProps, } from 'react'; import type { MessageDescriptor } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl'; 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 type { ActionMenuItem } from '@/flavours/glitch/models/dropdown_menu'; import type { Status, StatusVisibility } from '@/flavours/glitch/models/status'; import { createAppSelector, useAppDispatch, useAppSelector, } from '@/flavours/glitch/store'; import { isFeatureEnabled } from '@/flavours/glitch/utils/environment'; import FormatQuote from '@/material-icons/400-24px/format_quote.svg?react'; import FormatQuoteOff from '@/material-icons/400-24px/format_quote_off.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu'; import { Dropdown } from '../dropdown_menu'; import { Icon } from '../icon'; import { IconButton } from '../icon_button'; const messages = defineMessages({ all_disabled: { id: 'status.all_disabled', defaultMessage: 'Boosts and quotes are disabled', }, quote: { id: 'status.quote', defaultMessage: 'Quote' }, quote_cannot: { id: 'status.cannot_quote', defaultMessage: 'Author has disabled quoting on this post', }, quote_private: { id: 'status.quote_private', defaultMessage: 'Private posts cannot be quoted', }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog_cancel: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost', }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility', }, reblog_cannot: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted', }, }); interface ReblogButtonProps { status: Status; counters?: boolean; } export const StatusReblogButton: FC = ({ status, counters, }) => { const intl = useIntl(); const statusState = useAppSelector((state) => selectStatusState(state, status), ); const { isLoggedIn, isReblogged, isReblogAllowed, isQuoteAllowed } = statusState; const { iconComponent } = useMemo( () => reblogIconText(statusState), [statusState], ); const disabled = !isQuoteAllowed && !isReblogAllowed; const dispatch = useAppDispatch(); const statusId = status.get('id') as string; const items: ActionMenuItem[] = useMemo( () => [ { text: 'reblog', action: (event) => { if (isLoggedIn) { dispatch(toggleReblog(statusId, event.shiftKey)); } }, }, { text: 'quote', action: () => { if (isLoggedIn) { dispatch(quoteComposeById(statusId)); } }, }, ], [dispatch, isLoggedIn, statusId], ); const handleDropdownOpen = useCallback( (event: MouseEvent | KeyboardEvent) => { if (!isLoggedIn) { dispatch( openModal({ modalType: 'INTERACTION', modalProps: { type: 'reblog', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, }), ); } else if (event.shiftKey) { dispatch(toggleReblog(status.get('id'), true)); return false; } return true; }, [dispatch, isLoggedIn, status], ); const renderMenuItem: RenderItemFn = useCallback( (item, index, handlers, focusRefCallback) => ( ), [status], ); return ( ); }; interface ReblogMenuItemProps { status: Status; item: ActionMenuItem; index: number; handlers: RenderItemFnHandlers; focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void; } const ReblogMenuItem: FC = ({ status, index, item: { text }, handlers, focusRefCallback, }) => { const intl = useIntl(); const statusState = useAppSelector((state) => selectStatusState(state, status), ); const { title, meta, iconComponent, disabled } = useMemo( () => text === 'quote' ? quoteIconText(statusState) : reblogIconText(statusState), [statusState, text], ); const active = useMemo( () => text === 'reblog' && !!status.get('reblogged'), [status, text], ); return (
  • ); }; // Legacy helpers // Switch between the legacy and new reblog button based on feature flag. export const ReblogButton: FC = (props) => { if (isFeatureEnabled('outgoing_quotes')) { return ; } return ; }; export const LegacyReblogButton: FC = ({ status, counters, }) => { const intl = useIntl(); const statusState = useAppSelector((state) => selectStatusState(state, status), ); const { title, meta, iconComponent, disabled } = useMemo( () => reblogIconText(statusState), [statusState], ); const dispatch = useAppDispatch(); const handleClick: MouseEventHandler = useCallback( (event) => { if (statusState.isLoggedIn) { dispatch(toggleReblog(status.get('id') as string, event.shiftKey)); } else { dispatch( openModal({ modalType: 'INTERACTION', modalProps: { type: 'reblog', accountId: status.getIn(['account', 'id']), url: status.get('uri'), }, }), ); } }, [dispatch, status, statusState.isLoggedIn], ); return ( ); }; // Helpers for copy and state for status. const selectStatusState = createAppSelector( [ (state) => state.meta.get('me') as string | undefined, (_, status: Status) => status, ], (userId, status) => { const isPublic = ['public', 'unlisted'].includes( status.get('visibility') as StatusVisibility, ); const isMineAndPrivate = userId === status.getIn(['account', 'id']) && status.get('visibility') === 'private'; return { isLoggedIn: !!userId, isPublic, isMine: userId === status.getIn(['account', 'id']), isPrivateReblog: userId === status.getIn(['account', 'id']) && status.get('visibility') === 'private', isReblogged: !!status.get('reblogged'), isReblogAllowed: isPublic || isMineAndPrivate, isQuoteAllowed: status.getIn(['quote_approval', 'current_user']) === 'automatic' && (isPublic || isMineAndPrivate), }; }, ); type StatusState = ReturnType; interface IconText { title: MessageDescriptor; meta?: MessageDescriptor; iconComponent: FC>; disabled?: boolean; } function reblogIconText({ isPublic, isPrivateReblog, isReblogged, }: StatusState): IconText { if (isReblogged) { return { title: messages.reblog_cancel, iconComponent: isPublic ? RepeatActiveIcon : RepeatPrivateActiveIcon, }; } const iconText: IconText = { title: messages.reblog, iconComponent: RepeatIcon, }; if (isPrivateReblog) { iconText.meta = messages.reblog_private; iconText.iconComponent = RepeatPrivateIcon; } else if (!isPublic) { iconText.meta = messages.reblog_cannot; iconText.iconComponent = RepeatDisabledIcon; iconText.disabled = true; } return iconText; } function quoteIconText({ isMine, isQuoteAllowed, isPublic, }: StatusState): IconText { const iconText: IconText = { title: messages.quote, iconComponent: FormatQuote, }; if (!isQuoteAllowed || (!isPublic && !isMine)) { iconText.meta = !isQuoteAllowed ? messages.quote_cannot : messages.quote_private; iconText.iconComponent = FormatQuoteOff; iconText.disabled = true; } return iconText; }