diff --git a/app/soapbox/actions/modals.js b/app/soapbox/actions/modals.ts similarity index 59% rename from app/soapbox/actions/modals.js rename to app/soapbox/actions/modals.ts index 72604ecc6..9d6e85139 100644 --- a/app/soapbox/actions/modals.js +++ b/app/soapbox/actions/modals.ts @@ -1,7 +1,8 @@ export const MODAL_OPEN = 'MODAL_OPEN'; export const MODAL_CLOSE = 'MODAL_CLOSE'; -export function openModal(type, props) { +/** Open a modal of the given type */ +export function openModal(type: string, props?: any) { return { type: MODAL_OPEN, modalType: type, @@ -9,7 +10,8 @@ export function openModal(type, props) { }; } -export function closeModal(type) { +/** Close the modal */ +export function closeModal(type: string) { return { type: MODAL_CLOSE, modalType: type, diff --git a/app/soapbox/components/emoji-button-wrapper.tsx b/app/soapbox/components/emoji-button-wrapper.tsx new file mode 100644 index 000000000..eb2ec2f3b --- /dev/null +++ b/app/soapbox/components/emoji-button-wrapper.tsx @@ -0,0 +1,98 @@ +import classNames from 'classnames'; +import React, { useState, useRef } from 'react'; +import { usePopper } from 'react-popper'; +import { useDispatch } from 'react-redux'; + +import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; +import { openModal } from 'soapbox/actions/modals'; +import EmojiSelector from 'soapbox/components/ui/emoji-selector/emoji-selector'; +import { useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks'; + +interface IEmojiButtonWrapper { + statusId: string, + children: JSX.Element, +} + +/** Provides emoji reaction functionality to the underlying button component */ +const EmojiButtonWrapper: React.FC = ({ statusId, children }): JSX.Element | null => { + const dispatch = useDispatch(); + const ownAccount = useOwnAccount(); + const status = useAppSelector(state => state.statuses.get(statusId)); + const soapboxConfig = useSoapboxConfig(); + + const [visible, setVisible] = useState(false); + // const [focused, setFocused] = useState(false); + + const ref = useRef(null); + const popperRef = useRef(null); + + const { styles, attributes } = usePopper(ref.current, popperRef.current, { + placement: 'top-start', + strategy: 'fixed', + modifiers: [ + { + name: 'offset', + options: { + offset: [-10, 0], + }, + }, + ], + }); + + if (!status) return null; + + const handleMouseEnter = () => { + setVisible(true); + }; + + const handleMouseLeave = () => { + setVisible(false); + }; + + const handleReact = (emoji: string): void => { + if (ownAccount) { + dispatch(simpleEmojiReact(status, emoji)); + } else { + dispatch(openModal('UNAUTHORIZED', { + action: 'FAVOURITE', + ap_id: status.url, + })); + } + + setVisible(false); + }; + + // const handleUnfocus: React.EventHandler = () => { + // setFocused(false); + // }; + + const selector = ( +
+ +
+ ); + + return ( +
+ {React.cloneElement(children, { + ref, + })} + + {selector} +
+ ); +}; + +export default EmojiButtonWrapper; diff --git a/app/soapbox/components/hoverable.tsx b/app/soapbox/components/hoverable.tsx deleted file mode 100644 index 751c413c1..000000000 --- a/app/soapbox/components/hoverable.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; -import { usePopper } from 'react-popper'; - -interface IHoverable { - component: JSX.Element, -} - -/** Wrapper to render a given component when hovered */ -const Hoverable: React.FC = ({ - component, - children, -}): JSX.Element => { - - const [portalActive, setPortalActive] = useState(false); - - const ref = useRef(null); - const popperRef = useRef(null); - - const handleMouseEnter = () => { - setPortalActive(true); - }; - - const handleMouseLeave = () => { - setPortalActive(false); - }; - - const { styles, attributes } = usePopper(ref.current, popperRef.current, { - placement: 'top-start', - strategy: 'fixed', - modifiers: [ - { - name: 'offset', - options: { - offset: [-10, 0], - }, - }, - ], - }); - - return ( -
- {children} - -
- {component} -
-
- ); -}; - -export default Hoverable; diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index 9011bec71..dbafd64fd 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -6,8 +6,7 @@ import { connect } from 'react-redux'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts'; -import EmojiSelector from 'soapbox/components/emoji_selector'; -import Hoverable from 'soapbox/components/hoverable'; +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'; @@ -641,15 +640,7 @@ class StatusActionBar extends ImmutablePureComponent - )} - > + - + ): ( = ({ emoji, className, onClick, tabInd }; interface IEmojiSelector { - emojis: string[], + emojis: Iterable, onReact: (emoji: string) => void, visible?: boolean, focused?: boolean, @@ -40,7 +40,7 @@ const EmojiSelector: React.FC = ({ emojis, onReact, visible = fa space={2} className={classNames('bg-white dark:bg-slate-900 p-3 rounded-full shadow-md z-[999] w-max')} > - {emojis.map((emoji, i) => ( + {Array.from(emojis).map((emoji, i) => ( { {reblogButton} - + {features.emojiReacts ? ( + + + + ) : ( + + )} {canShare && (