add status reply hover

merge-requests/1539/head
ewwwwwwww 2022-06-16 21:19:53 -07:00
rodzic f34a5b81f8
commit cc7058349f
9 zmienionych plików z 253 dodań i 9 usunięć

Wyświetl plik

@ -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,
};
}

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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');
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;
}
}