diff --git a/app/soapbox/actions/status_hover_card.js b/app/soapbox/actions/status_hover_card.js new file mode 100644 index 000000000..85a946e1f --- /dev/null +++ b/app/soapbox/actions/status_hover_card.js @@ -0,0 +1,24 @@ +export const STATUS_HOVER_CARD_OPEN = 'STATUS_HOVER_CARD_OPEN'; +export const STATUS_HOVER_CARD_UPDATE = 'STATUS_HOVER_CARD_UPDATE'; +export const STATUS_HOVER_CARD_CLOSE = 'STATUS_HOVER_CARD_CLOSE'; + +export function openStatusHoverCard(ref, statusId) { + return { + type: STATUS_HOVER_CARD_OPEN, + ref, + statusId, + }; +} + +export function updateStatusHoverCard() { + return { + type: STATUS_HOVER_CARD_UPDATE, + }; +} + +export function closeStatusHoverCard(force = false) { + return { + type: STATUS_HOVER_CARD_CLOSE, + force, + }; +} diff --git a/app/soapbox/components/hover_status_wrapper.tsx b/app/soapbox/components/hover_status_wrapper.tsx new file mode 100644 index 000000000..580fdaaf9 --- /dev/null +++ b/app/soapbox/components/hover_status_wrapper.tsx @@ -0,0 +1,57 @@ +import classNames from 'classnames'; +import { debounce } from 'lodash'; +import React, { useRef } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + openStatusHoverCard, + closeStatusHoverCard, +} from 'soapbox/actions/status_hover_card'; +import { isMobile } from 'soapbox/is_mobile'; + +const showStatusHoverCard = debounce((dispatch, ref, statusId) => { + dispatch(openStatusHoverCard(ref, statusId)); +}, 300); + +interface IHoverStatusWrapper { + statusId: any, + inline: boolean, + className?: string, +} + +/** Makes a status hover card appear when the wrapped element is hovered. */ +export const HoverStatusWrapper: React.FC = ({ statusId, children, inline = false, className }) => { + const dispatch = useDispatch(); + const ref = useRef(null); + const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div'; + + const handleMouseEnter = () => { + if (!isMobile(window.innerWidth)) { + showStatusHoverCard(dispatch, ref, statusId); + } + }; + + const handleMouseLeave = () => { + showStatusHoverCard.cancel(); + setTimeout(() => dispatch(closeStatusHoverCard()), 200); + }; + + const handleClick = () => { + showStatusHoverCard.cancel(); + dispatch(closeStatusHoverCard(true)); + }; + + return ( + + {children} + + ); +}; + +export { HoverStatusWrapper as default, showStatusHoverCard }; diff --git a/app/soapbox/components/status-hover-card.tsx b/app/soapbox/components/status-hover-card.tsx new file mode 100644 index 000000000..a68bfd9fa --- /dev/null +++ b/app/soapbox/components/status-hover-card.tsx @@ -0,0 +1,102 @@ +import classNames from 'classnames'; +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { usePopper } from 'react-popper'; +import { useHistory } from 'react-router-dom'; + +import { fetchRelationships } from 'soapbox/actions/accounts'; +import { + closeStatusHoverCard, + updateStatusHoverCard, +} from 'soapbox/actions/status_hover_card'; +import ActionButton from 'soapbox/features/ui/components/action-button'; +import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; +import { UserPanel } from 'soapbox/features/ui/util/async-components'; +import StatusContainer from 'soapbox/containers/status_container'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; +import { makeGetStatus } from 'soapbox/selectors'; + +import { showStatusHoverCard } from './hover_status_wrapper'; +import { Card, CardBody, Stack, Text } from './ui'; + +import type { AppDispatch } from 'soapbox/store'; + +const getStatus = makeGetStatus(); + +const handleMouseEnter = (dispatch: AppDispatch): React.MouseEventHandler => { + return () => { + dispatch(updateStatusHoverCard()); + }; +}; + +const handleMouseLeave = (dispatch: AppDispatch): React.MouseEventHandler => { + return () => { + dispatch(closeStatusHoverCard(true)); + }; +}; + +interface IStatusHoverCard { + visible: boolean, +} + +/** Popup status preview that appears when hovering reply to */ +export const StatusHoverCard: React.FC = ({ visible = true }) => { + const dispatch = useAppDispatch(); + const history = useHistory(); + + const [popperElement, setPopperElement] = useState(null); + + const statusId: string | undefined = useAppSelector(state => state.status_hover_card.statusId || undefined); + const targetRef = useAppSelector(state => state.status_hover_card.ref?.current); + + useEffect(() => { + const unlisten = history.listen(() => { + showStatusHoverCard.cancel(); + dispatch(closeStatusHoverCard()); + }); + + return () => { + unlisten(); + }; + }, []); + + const { styles, attributes } = usePopper(targetRef, popperElement, { + placement: 'top' + }); + + if (!statusId) return null; + + const renderStatus = (statusId: string) => { + return ( + // @ts-ignore + + ); + }; + + return ( +
+ + + {renderStatus(statusId)} + + +
+ ); +}; + +export default StatusHoverCard; diff --git a/app/soapbox/components/status-reply-mentions.tsx b/app/soapbox/components/status-reply-mentions.tsx index 9f8d890ae..f52c6c7fd 100644 --- a/app/soapbox/components/status-reply-mentions.tsx +++ b/app/soapbox/components/status-reply-mentions.tsx @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper'; +import HoverStatusWrapper from 'soapbox/components/hover_status_wrapper'; import { useAppDispatch } from 'soapbox/hooks'; import type { Account, Status } from 'soapbox/types/entities'; @@ -64,9 +65,18 @@ const StatusReplyMentions: React.FC = ({ status }) => {
, + hover: (children: any) => + + {children} + + }} />
diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 4389b1fc1..73a59519c 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -93,6 +93,7 @@ interface IStatus extends RouteComponentProps { history: History, featured?: boolean, withDismiss?: boolean, + hideActionBar?: boolean, } interface IStatusState { @@ -512,14 +513,16 @@ class Status extends ImmutablePureComponent { {poll} {quote} - + {!this.props.hideActionBar && ( + + )} diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index c1057950c..bcc1153cc 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -102,6 +102,7 @@ import { SidebarMenu, UploadArea, ProfileHoverCard, + StatusHoverCard, Share, NewStatus, IntentionalError, @@ -693,6 +694,10 @@ const UI: React.FC = ({ children }) => { {Component => } + + + {Component => } + diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index 4670a77c3..2e3c7b8be 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -406,6 +406,10 @@ export function ProfileHoverCard() { return import(/* webpackChunkName: "features/ui" */'soapbox/components/profile-hover-card'); } +export function StatusHoverCard() { + return import(/* webpackChunkName: "features/ui" */'soapbox/components/status-hover-card'); +} + export function CryptoDonate() { return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate'); } diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index a05ef47c4..7c907de75 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -53,6 +53,7 @@ import security from './security'; import settings from './settings'; import sidebar from './sidebar'; import soapbox from './soapbox'; +import status_hover_card from './status_hover_card'; import status_lists from './status_lists'; import statuses from './statuses'; import suggestions from './suggestions'; @@ -108,6 +109,7 @@ const reducers = { chat_messages, chat_message_lists, profile_hover_card, + status_hover_card, backups, admin_log, security, diff --git a/app/soapbox/reducers/status_hover_card.ts b/app/soapbox/reducers/status_hover_card.ts new file mode 100644 index 000000000..80169ab85 --- /dev/null +++ b/app/soapbox/reducers/status_hover_card.ts @@ -0,0 +1,37 @@ +import { Record as ImmutableRecord } from 'immutable'; + +import { + STATUS_HOVER_CARD_OPEN, + STATUS_HOVER_CARD_CLOSE, + STATUS_HOVER_CARD_UPDATE, +} from 'soapbox/actions/status_hover_card'; + +import type { AnyAction } from 'redux'; + +const ReducerRecord = ImmutableRecord({ + ref: null as React.MutableRefObject | null, + statusId: '', + hovered: false, +}); + +type State = ReturnType; + +export default function statusHoverCard(state: State = ReducerRecord(), action: AnyAction) { + switch (action.type) { + case STATUS_HOVER_CARD_OPEN: + return state.withMutations((state) => { + state.set('ref', action.ref); + state.set('statusId', action.statusId); + }); + case STATUS_HOVER_CARD_UPDATE: + return state.set('hovered', true); + case STATUS_HOVER_CARD_CLOSE: + if (state.get('hovered') === true && !action.force) + return state; + else + return ReducerRecord(); + default: + return state; + } +} +