add status reply hover
This commit is contained in:
parent
f34a5b81f8
commit
cc7058349f
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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 };
|
|
@ -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 { openModal } from 'soapbox/actions/modals';
|
||||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||||
|
import HoverStatusWrapper from 'soapbox/components/hover_status_wrapper';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { Account, Status } from 'soapbox/types/entities';
|
import type { Account, Status } from 'soapbox/types/entities';
|
||||||
|
@ -64,9 +65,18 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status }) => {
|
||||||
<div className='reply-mentions'>
|
<div className='reply-mentions'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='reply_mentions.reply'
|
id='reply_mentions.reply'
|
||||||
defaultMessage='Replying to {accounts}'
|
defaultMessage='<hover>Replying to</hover> {accounts}'
|
||||||
values={{
|
values={{
|
||||||
accounts: <FormattedList type='conjunction' value={accounts} />,
|
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>
|
</div>
|
||||||
|
|
|
@ -93,6 +93,7 @@ interface IStatus extends RouteComponentProps {
|
||||||
history: History,
|
history: History,
|
||||||
featured?: boolean,
|
featured?: boolean,
|
||||||
withDismiss?: boolean,
|
withDismiss?: boolean,
|
||||||
|
hideActionBar?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IStatusState {
|
interface IStatusState {
|
||||||
|
@ -512,14 +513,16 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
||||||
{poll}
|
{poll}
|
||||||
{quote}
|
{quote}
|
||||||
|
|
||||||
<StatusActionBar
|
{!this.props.hideActionBar && (
|
||||||
status={status}
|
<StatusActionBar
|
||||||
// @ts-ignore what?
|
status={status}
|
||||||
account={account}
|
// @ts-ignore what?
|
||||||
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
account={account}
|
||||||
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||||
{...other}
|
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||||
/>
|
{...other}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -102,6 +102,7 @@ import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
UploadArea,
|
UploadArea,
|
||||||
ProfileHoverCard,
|
ProfileHoverCard,
|
||||||
|
StatusHoverCard,
|
||||||
Share,
|
Share,
|
||||||
NewStatus,
|
NewStatus,
|
||||||
IntentionalError,
|
IntentionalError,
|
||||||
|
@ -693,6 +694,10 @@ const UI: React.FC = ({ children }) => {
|
||||||
<BundleContainer fetchComponent={ProfileHoverCard}>
|
<BundleContainer fetchComponent={ProfileHoverCard}>
|
||||||
{Component => <Component />}
|
{Component => <Component />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
|
|
||||||
|
<BundleContainer fetchComponent={StatusHoverCard}>
|
||||||
|
{Component => <Component />}
|
||||||
|
</BundleContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
|
|
@ -406,6 +406,10 @@ export function ProfileHoverCard() {
|
||||||
return import(/* webpackChunkName: "features/ui" */'soapbox/components/profile-hover-card');
|
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() {
|
export function CryptoDonate() {
|
||||||
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate');
|
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate');
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ import security from './security';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
import sidebar from './sidebar';
|
import sidebar from './sidebar';
|
||||||
import soapbox from './soapbox';
|
import soapbox from './soapbox';
|
||||||
|
import status_hover_card from './status_hover_card';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import statuses from './statuses';
|
import statuses from './statuses';
|
||||||
import suggestions from './suggestions';
|
import suggestions from './suggestions';
|
||||||
|
@ -108,6 +109,7 @@ const reducers = {
|
||||||
chat_messages,
|
chat_messages,
|
||||||
chat_message_lists,
|
chat_message_lists,
|
||||||
profile_hover_card,
|
profile_hover_card,
|
||||||
|
status_hover_card,
|
||||||
backups,
|
backups,
|
||||||
admin_log,
|
admin_log,
|
||||||
security,
|
security,
|
||||||
|
|
|
@ -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 New Issue