From cc7058349f5c6f572e028573bf8be3d0e6dff876 Mon Sep 17 00:00:00 2001 From: ewwwwwwww Date: Thu, 16 Jun 2022 21:19:53 -0700 Subject: [PATCH] add status reply hover --- app/soapbox/actions/status_hover_card.js | Bin 0 -> 543 bytes .../components/hover_status_wrapper.tsx | 57 ++++++++++ app/soapbox/components/status-hover-card.tsx | 102 ++++++++++++++++++ .../components/status-reply-mentions.tsx | 12 ++- app/soapbox/components/status.tsx | 19 ++-- app/soapbox/features/ui/index.tsx | 5 + .../features/ui/util/async-components.ts | 4 + app/soapbox/reducers/index.ts | 2 + app/soapbox/reducers/status_hover_card.ts | 37 +++++++ 9 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 app/soapbox/actions/status_hover_card.js create mode 100644 app/soapbox/components/hover_status_wrapper.tsx create mode 100644 app/soapbox/components/status-hover-card.tsx create mode 100644 app/soapbox/reducers/status_hover_card.ts diff --git a/app/soapbox/actions/status_hover_card.js b/app/soapbox/actions/status_hover_card.js new file mode 100644 index 0000000000000000000000000000000000000000..85a946e1f94dcf93d284b93783673b71701bbe0f GIT binary patch literal 543 zcmb7=%?g7s5QOhO#olbeC#duwDp)90{OMIf+(1DRNn&X!zI!!d4^>;dZ8Dp0CWDVg zNDWkQr6J3`yvWKZzJ_V(dudR{NqB)H*vpFTTKM~Ykpx~IE)z!WKf^!A*$To)JzYA%mHOCPt_h?7Jx+EN^V8~{cP~C(t*=259@3kCdzQ? rT&tT0Xq$>?{Eo1}b!=zdfYw6I_)JKOecO!G3OAUU9>=d4#K-mxJtwLi literal 0 HcmV?d00001 diff --git a/app/soapbox/components/hover_status_wrapper.tsx b/app/soapbox/components/hover_status_wrapper.tsx new file mode 100644 index 0000000000..580fdaaf95 --- /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 0000000000..a68bfd9fa5 --- /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 9f8d890aea..f52c6c7fdd 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 4389b1fc19..73a59519c3 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 c1057950c5..bcc1153cce 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 4670a77c3f..2e3c7b8be6 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 a05ef47c48..7c907de75d 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 0000000000..80169ab857 --- /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; + } +} +