add status reply hover
This commit is contained in:
parent
f34a5b81f8
commit
cc7058349f
9 changed files with 229 additions and 9 deletions
BIN
app/soapbox/actions/status_hover_card.js
Normal file
BIN
app/soapbox/actions/status_hover_card.js
Normal file
Binary file not shown.
57
app/soapbox/components/hover_status_wrapper.tsx
Normal file
57
app/soapbox/components/hover_status_wrapper.tsx
Normal file
|
@ -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<IHoverStatusWrapper> = ({ statusId, children, inline = false, className }) => {
|
||||
const dispatch = useDispatch();
|
||||
const ref = useRef<HTMLDivElement>(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 (
|
||||
<Elem
|
||||
ref={ref}
|
||||
className={classNames('hover-status-wrapper', className)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
</Elem>
|
||||
);
|
||||
};
|
||||
|
||||
export { HoverStatusWrapper as default, showStatusHoverCard };
|
102
app/soapbox/components/status-hover-card.tsx
Normal file
102
app/soapbox/components/status-hover-card.tsx
Normal file
|
@ -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<IStatusHoverCard> = ({ visible = true }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const [popperElement, setPopperElement] = useState<HTMLElement | null>(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
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
hideActionBar
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
'absolute transition-opacity w-[500px] z-50 top-0 left-0': true,
|
||||
'opacity-100': visible,
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
})}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
onMouseEnter={handleMouseEnter(dispatch)}
|
||||
onMouseLeave={handleMouseLeave(dispatch)}
|
||||
>
|
||||
<Card className='relative'>
|
||||
<CardBody>
|
||||
{renderStatus(statusId)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusHoverCard;
|
|
@ -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<IStatusReplyMentions> = ({ status }) => {
|
|||
<div className='reply-mentions'>
|
||||
<FormattedMessage
|
||||
id='reply_mentions.reply'
|
||||
defaultMessage='Replying to {accounts}'
|
||||
defaultMessage='<hover>Replying to</hover> {accounts}'
|
||||
values={{
|
||||
accounts: <FormattedList type='conjunction' value={accounts} />,
|
||||
hover: (children: any) => <HoverStatusWrapper statusId={status.in_reply_to_id} inline>
|
||||
<span
|
||||
key='hoverstatus'
|
||||
className='hover:underline cursor-pointer'
|
||||
role='presentation'
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</HoverStatusWrapper>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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<IStatus, IStatusState> {
|
|||
{poll}
|
||||
{quote}
|
||||
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
// @ts-ignore what?
|
||||
account={account}
|
||||
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||
{...other}
|
||||
/>
|
||||
{!this.props.hideActionBar && (
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
// @ts-ignore what?
|
||||
account={account}
|
||||
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||
{...other}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -102,6 +102,7 @@ import {
|
|||
SidebarMenu,
|
||||
UploadArea,
|
||||
ProfileHoverCard,
|
||||
StatusHoverCard,
|
||||
Share,
|
||||
NewStatus,
|
||||
IntentionalError,
|
||||
|
@ -693,6 +694,10 @@ const UI: React.FC = ({ children }) => {
|
|||
<BundleContainer fetchComponent={ProfileHoverCard}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
|
||||
<BundleContainer fetchComponent={StatusHoverCard}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
37
app/soapbox/reducers/status_hover_card.ts
Normal file
37
app/soapbox/reducers/status_hover_card.ts
Normal file
|
@ -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<HTMLDivElement> | null,
|
||||
statusId: '',
|
||||
hovered: false,
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue