Merge branch 'hovercard-refactor' into 'develop'
Hovercard refactor, fixes #407 Closes #266 and #407 See merge request soapbox-pub/soapbox-fe!228
This commit is contained in:
commit
ed6b876e07
22 changed files with 275 additions and 200 deletions
24
app/soapbox/actions/profile_hover_card.js
Normal file
24
app/soapbox/actions/profile_hover_card.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN';
|
||||||
|
export const PROFILE_HOVER_CARD_UPDATE = 'PROFILE_HOVER_CARD_UPDATE';
|
||||||
|
export const PROFILE_HOVER_CARD_CLOSE = 'PROFILE_HOVER_CARD_CLOSE';
|
||||||
|
|
||||||
|
export function openProfileHoverCard(ref, accountId) {
|
||||||
|
return {
|
||||||
|
type: PROFILE_HOVER_CARD_OPEN,
|
||||||
|
ref,
|
||||||
|
accountId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateProfileHoverCard() {
|
||||||
|
return {
|
||||||
|
type: PROFILE_HOVER_CARD_UPDATE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeProfileHoverCard(force = false) {
|
||||||
|
return {
|
||||||
|
type: PROFILE_HOVER_CARD_CLOSE,
|
||||||
|
force,
|
||||||
|
};
|
||||||
|
}
|
|
@ -4,16 +4,22 @@ exports[`<DisplayName /> renders display name + account name 1`] = `
|
||||||
<span
|
<span
|
||||||
className="display-name"
|
className="display-name"
|
||||||
>
|
>
|
||||||
<bdi>
|
<span
|
||||||
<strong
|
className="hover-ref-wrapper"
|
||||||
className="display-name__html"
|
onMouseEnter={[Function]}
|
||||||
dangerouslySetInnerHTML={
|
onMouseLeave={[Function]}
|
||||||
Object {
|
>
|
||||||
"__html": "<p>Foo</p>",
|
<bdi>
|
||||||
|
<strong
|
||||||
|
className="display-name__html"
|
||||||
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "<p>Foo</p>",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</bdi>
|
||||||
</bdi>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="display-name__account"
|
className="display-name__account"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
import { fromJS } from 'immutable';
|
import { fromJS } from 'immutable';
|
||||||
import DisplayName from '../display_name';
|
import DisplayName from '../display_name';
|
||||||
|
import { createComponent } from 'soapbox/test_helpers';
|
||||||
|
|
||||||
describe('<DisplayName />', () => {
|
describe('<DisplayName />', () => {
|
||||||
it('renders display name + account name', () => {
|
it('renders display name + account name', () => {
|
||||||
|
@ -10,7 +10,7 @@ describe('<DisplayName />', () => {
|
||||||
acct: 'bar@baz',
|
acct: 'bar@baz',
|
||||||
display_name_html: '<p>Foo</p>',
|
display_name_html: '<p>Foo</p>',
|
||||||
});
|
});
|
||||||
const component = renderer.create(<DisplayName account={account} />);
|
const component = createComponent(<DisplayName account={account} />);
|
||||||
const tree = component.toJSON();
|
const tree = component.toJSON();
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import VerificationBadge from './verification_badge';
|
import VerificationBadge from './verification_badge';
|
||||||
import { acctFull } from '../utils/accounts';
|
import { acctFull } from '../utils/accounts';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||||
|
|
||||||
export default class DisplayName extends React.PureComponent {
|
export default class DisplayName extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -42,7 +43,9 @@ export default class DisplayName extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='display-name'>
|
<span className='display-name'>
|
||||||
{displayName}
|
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||||
|
{displayName}
|
||||||
|
</HoverRefWrapper>
|
||||||
{suffix}
|
{suffix}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
|
|
56
app/soapbox/components/hover_ref_wrapper.js
Normal file
56
app/soapbox/components/hover_ref_wrapper.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
openProfileHoverCard,
|
||||||
|
closeProfileHoverCard,
|
||||||
|
} from 'soapbox/actions/profile_hover_card';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { isMobile } from 'soapbox/is_mobile';
|
||||||
|
|
||||||
|
const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
|
||||||
|
dispatch(openProfileHoverCard(ref, accountId));
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
|
const handleMouseEnter = (dispatch, ref, accountId) => {
|
||||||
|
return e => {
|
||||||
|
if (!isMobile(window.innerWidth))
|
||||||
|
showProfileHoverCard(dispatch, ref, accountId);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (dispatch) => {
|
||||||
|
return e => {
|
||||||
|
showProfileHoverCard.cancel();
|
||||||
|
setTimeout(() => dispatch(closeProfileHoverCard()), 300);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HoverRefWrapper = ({ accountId, children, inline }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const ref = useRef();
|
||||||
|
const Elem = inline ? 'span' : 'div';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Elem
|
||||||
|
ref={ref}
|
||||||
|
className='hover-ref-wrapper'
|
||||||
|
onMouseEnter={handleMouseEnter(dispatch, ref, accountId)}
|
||||||
|
onMouseLeave={handleMouseLeave(dispatch)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Elem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
HoverRefWrapper.propTypes = {
|
||||||
|
accountId: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
inline: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
HoverRefWrapper.defaultProps = {
|
||||||
|
inline: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HoverRefWrapper;
|
92
app/soapbox/components/profile_hover_card.js
Normal file
92
app/soapbox/components/profile_hover_card.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import UserPanel from 'soapbox/features/ui/components/user_panel';
|
||||||
|
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||||
|
import { isAdmin, isModerator } from 'soapbox/utils/accounts';
|
||||||
|
import Badge from 'soapbox/components/badge';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||||
|
import { usePopper } from 'react-popper';
|
||||||
|
import {
|
||||||
|
closeProfileHoverCard,
|
||||||
|
updateProfileHoverCard,
|
||||||
|
} from 'soapbox/actions/profile_hover_card';
|
||||||
|
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const getBadges = (account) => {
|
||||||
|
let badges = [];
|
||||||
|
if (isAdmin(account)) badges.push(<Badge key='admin' slug='admin' title='Admin' />);
|
||||||
|
if (isModerator(account)) badges.push(<Badge key='moderator' slug='moderator' title='Moderator' />);
|
||||||
|
if (account.getIn(['patron', 'is_patron'])) badges.push(<Badge key='patron' slug='patron' title='Patron' />);
|
||||||
|
return badges;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = (dispatch) => {
|
||||||
|
return e => {
|
||||||
|
dispatch(updateProfileHoverCard());
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (dispatch) => {
|
||||||
|
return e => {
|
||||||
|
dispatch(closeProfileHoverCard(true));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProfileHoverCard = ({ visible }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [popperElement, setPopperElement] = useState(null);
|
||||||
|
|
||||||
|
const accountId = useSelector(state => state.getIn(['profile_hover_card', 'accountId']));
|
||||||
|
const account = useSelector(state => accountId && getAccount(state, accountId));
|
||||||
|
const targetRef = useSelector(state => state.getIn(['profile_hover_card', 'ref', 'current']));
|
||||||
|
const badges = account ? getBadges(account) : [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId) dispatch(fetchRelationships([accountId]));
|
||||||
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
|
const { styles, attributes } = usePopper(targetRef, popperElement);
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
const accountBio = { __html: account.get('note_emojified') };
|
||||||
|
const followedBy = account.getIn(['relationship', 'followed_by']);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('profile-hover-card', { 'profile-hover-card--visible': visible })} ref={setPopperElement} style={styles.popper} {...attributes.popper} onMouseEnter={handleMouseEnter(dispatch)} onMouseLeave={handleMouseLeave(dispatch)}>
|
||||||
|
<div className='profile-hover-card__container'>
|
||||||
|
{followedBy &&
|
||||||
|
<span className='relationship-tag'>
|
||||||
|
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||||
|
</span>}
|
||||||
|
<div className='profile-hover-card__action-button'><ActionButton account={account} small /></div>
|
||||||
|
<UserPanel className='profile-hover-card__user' accountId={account.get('id')} />
|
||||||
|
{badges.length > 0 &&
|
||||||
|
<div className='profile-hover-card__badges'>
|
||||||
|
{badges}
|
||||||
|
</div>}
|
||||||
|
{account.getIn(['source', 'note'], '').length > 0 &&
|
||||||
|
<div className='profile-hover-card__bio' dangerouslySetInnerHTML={accountBio} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileHoverCard.propTypes = {
|
||||||
|
visible: PropTypes.bool,
|
||||||
|
accountId: PropTypes.string,
|
||||||
|
account: ImmutablePropTypes.map,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileHoverCard.defaultProps = {
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ProfileHoverCard);
|
|
@ -18,10 +18,8 @@ import classNames from 'classnames';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import PollContainer from 'soapbox/containers/poll_container';
|
import PollContainer from 'soapbox/containers/poll_container';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import ProfileHoverCardContainer from '../features/profile_hover_card/profile_hover_card_container';
|
|
||||||
import { isMobile } from '../../../app/soapbox/is_mobile';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { getDomain } from 'soapbox/utils/accounts';
|
import { getDomain } from 'soapbox/utils/accounts';
|
||||||
|
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||||
|
|
||||||
// We use the component (and not the container) since we do not want
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
|
@ -82,6 +80,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onEmbed: PropTypes.func,
|
onEmbed: PropTypes.func,
|
||||||
onHeightChange: PropTypes.func,
|
onHeightChange: PropTypes.func,
|
||||||
onToggleHidden: PropTypes.func,
|
onToggleHidden: PropTypes.func,
|
||||||
|
onShowHoverProfileCard: PropTypes.func,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
|
@ -108,7 +107,6 @@ class Status extends ImmutablePureComponent {
|
||||||
state = {
|
state = {
|
||||||
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
||||||
statusId: undefined,
|
statusId: undefined,
|
||||||
profileCardVisible: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track height changes we know about to compensate scrolling
|
// Track height changes we know about to compensate scrolling
|
||||||
|
@ -256,19 +254,6 @@ class Status extends ImmutablePureComponent {
|
||||||
this.handleToggleMediaVisibility();
|
this.handleToggleMediaVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
showProfileCard = debounce(() => {
|
|
||||||
this.setState({ profileCardVisible: true });
|
|
||||||
}, 1200);
|
|
||||||
|
|
||||||
handleProfileHover = e => {
|
|
||||||
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleProfileLeave = e => {
|
|
||||||
this.showProfileCard.cancel();
|
|
||||||
this.setState({ profileCardVisible: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
_properStatus() {
|
_properStatus() {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
|
|
||||||
|
@ -457,7 +442,6 @@ class Status extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
||||||
const { profileCardVisible } = this.state;
|
|
||||||
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
||||||
const domain = getDomain(status.get('account'));
|
const domain = getDomain(status.get('account'));
|
||||||
|
|
||||||
|
@ -478,17 +462,17 @@ class Status extends ImmutablePureComponent {
|
||||||
<img src={favicon} alt='' title={domain} />
|
<img src={favicon} alt='' title={domain} />
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<div className='status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
<div className='status__profile'>
|
||||||
<div className='status__avatar'>
|
<div className='status__avatar'>
|
||||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||||
{statusAvatar}
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])}>
|
||||||
|
{statusAvatar}
|
||||||
|
</NavLink>
|
||||||
|
</HoverRefWrapper>
|
||||||
</div>
|
</div>
|
||||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{ profileCardVisible &&
|
|
||||||
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth)} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { makeGetAccount } from '../../selectors';
|
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import UserPanel from '../ui/components/user_panel';
|
|
||||||
import ActionButton from '../ui/components/action_button';
|
|
||||||
import { isAdmin, isModerator } from 'soapbox/utils/accounts';
|
|
||||||
import Badge from 'soapbox/components/badge';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
|
||||||
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { accountId }) => {
|
|
||||||
return {
|
|
||||||
account: getAccount(state, accountId),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
@injectIntl
|
|
||||||
class ProfileHoverCardContainer extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
visible: PropTypes.bool,
|
|
||||||
accountId: PropTypes.string,
|
|
||||||
account: ImmutablePropTypes.map,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
visible: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
getBadges = () => {
|
|
||||||
const { account } = this.props;
|
|
||||||
let badges = [];
|
|
||||||
if (isAdmin(account)) badges.push(<Badge key='admin' slug='admin' title='Admin' />);
|
|
||||||
if (isModerator(account)) badges.push(<Badge key='moderator' slug='moderator' title='Moderator' />);
|
|
||||||
if (account.getIn(['patron', 'is_patron'])) badges.push(<Badge key='patron' slug='patron' title='Patron' />);
|
|
||||||
return badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatch(fetchRelationships([this.props.accountId]));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { visible, accountId, account } = this.props;
|
|
||||||
if (!accountId) return null;
|
|
||||||
const accountBio = { __html: account.get('note_emojified') };
|
|
||||||
const followedBy = account.getIn(['relationship', 'followed_by']);
|
|
||||||
const badges = this.getBadges();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('profile-hover-card', { 'profile-hover-card--visible': visible })}>
|
|
||||||
<div className='profile-hover-card__container'>
|
|
||||||
{followedBy &&
|
|
||||||
<span className='relationship-tag'>
|
|
||||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
|
||||||
</span>}
|
|
||||||
<div className='profile-hover-card__action-button'><ActionButton account={account} small /></div>
|
|
||||||
<UserPanel className='profile-hover-card__user' accountId={accountId} />
|
|
||||||
{badges.length > 0 &&
|
|
||||||
<div className='profile-hover-card__badges'>
|
|
||||||
{badges}
|
|
||||||
</div>}
|
|
||||||
{account.getIn(['source', 'note'], '').length > 0 &&
|
|
||||||
<div className='profile-hover-card__bio' dangerouslySetInnerHTML={accountBio} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -16,10 +16,8 @@ import classNames from 'classnames';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import PollContainer from 'soapbox/containers/poll_container';
|
import PollContainer from 'soapbox/containers/poll_container';
|
||||||
import { StatusInteractionBar } from './status_interaction_bar';
|
import { StatusInteractionBar } from './status_interaction_bar';
|
||||||
import ProfileHoverCardContainer from 'soapbox/features/profile_hover_card/profile_hover_card_container';
|
|
||||||
import { isMobile } from 'soapbox/is_mobile';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { getDomain } from 'soapbox/utils/accounts';
|
import { getDomain } from 'soapbox/utils/accounts';
|
||||||
|
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||||
|
|
||||||
export default class DetailedStatus extends ImmutablePureComponent {
|
export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -42,7 +40,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
height: null,
|
height: null,
|
||||||
profileCardVisible: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOpenVideo = (media, startTime) => {
|
handleOpenVideo = (media, startTime) => {
|
||||||
|
@ -86,24 +83,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
showProfileCard = debounce(() => {
|
|
||||||
this.setState({ profileCardVisible: true });
|
|
||||||
}, 1200);
|
|
||||||
|
|
||||||
handleProfileHover = e => {
|
|
||||||
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleProfileLeave = e => {
|
|
||||||
this.showProfileCard.cancel();
|
|
||||||
this.setState({ profileCardVisible: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { compact } = this.props;
|
const { compact } = this.props;
|
||||||
const { profileCardVisible } = this.state;
|
|
||||||
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
||||||
const domain = getDomain(status.get('account'));
|
const domain = getDomain(status.get('account'));
|
||||||
|
|
||||||
|
@ -181,20 +164,21 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
||||||
<div className='detailed-status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
<div className='detailed-status__profile'>
|
||||||
<div className='detailed-status__display-name'>
|
<div className='detailed-status__display-name'>
|
||||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`}>
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`}>
|
||||||
<div className='detailed-status__display-avatar'>
|
<div className='detailed-status__display-avatar'>
|
||||||
<Avatar account={status.get('account')} size={48} />
|
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||||
|
<Avatar account={status.get('account')} size={48} />
|
||||||
|
</HoverRefWrapper>
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<DisplayName account={status.get('account')}>
|
<DisplayName account={status.get('account')}>
|
||||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||||
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} />
|
||||||
|
</HoverRefWrapper>
|
||||||
</DisplayName>
|
</DisplayName>
|
||||||
</div>
|
</div>
|
||||||
{ profileCardVisible &&
|
|
||||||
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth) && profileCardVisible} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status.get('group') && (
|
{status.get('group') && (
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { Redirect } from 'react-router-dom';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import { isStaff } from 'soapbox/utils/accounts';
|
import { isStaff } from 'soapbox/utils/accounts';
|
||||||
import ChatPanes from 'soapbox/features/chats/components/chat_panes';
|
import ChatPanes from 'soapbox/features/chats/components/chat_panes';
|
||||||
|
import ProfileHoverCard from 'soapbox/components/profile_hover_card';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Status,
|
Status,
|
||||||
|
@ -650,6 +651,7 @@ class UI extends React.PureComponent {
|
||||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||||
{me && <SidebarMenu />}
|
{me && <SidebarMenu />}
|
||||||
{me && !mobile && <ChatPanes />}
|
{me && !mobile && <ChatPanes />}
|
||||||
|
<ProfileHoverCard />
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
|
|
@ -46,6 +46,7 @@ import admin from './admin';
|
||||||
import chats from './chats';
|
import chats from './chats';
|
||||||
import chat_messages from './chat_messages';
|
import chat_messages from './chat_messages';
|
||||||
import chat_message_lists from './chat_message_lists';
|
import chat_message_lists from './chat_message_lists';
|
||||||
|
import profile_hover_card from './profile_hover_card';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
dropdown_menu,
|
dropdown_menu,
|
||||||
|
@ -95,6 +96,7 @@ const reducers = {
|
||||||
chats,
|
chats,
|
||||||
chat_messages,
|
chat_messages,
|
||||||
chat_message_lists,
|
chat_message_lists,
|
||||||
|
profile_hover_card,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export default combineReducers(reducers);
|
||||||
|
|
27
app/soapbox/reducers/profile_hover_card.js
Normal file
27
app/soapbox/reducers/profile_hover_card.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import {
|
||||||
|
PROFILE_HOVER_CARD_OPEN,
|
||||||
|
PROFILE_HOVER_CARD_CLOSE,
|
||||||
|
PROFILE_HOVER_CARD_UPDATE,
|
||||||
|
} from 'soapbox/actions/profile_hover_card';
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
|
export default function profileHoverCard(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case PROFILE_HOVER_CARD_OPEN:
|
||||||
|
return ImmutableMap({
|
||||||
|
ref: action.ref,
|
||||||
|
accountId: action.accountId,
|
||||||
|
});
|
||||||
|
case PROFILE_HOVER_CARD_UPDATE:
|
||||||
|
return state.set('hovered', true);
|
||||||
|
case PROFILE_HOVER_CARD_CLOSE:
|
||||||
|
if (state.get('hovered') === true && !action.force)
|
||||||
|
return state;
|
||||||
|
else
|
||||||
|
return ImmutableMap();
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,6 @@
|
||||||
@import 'dyslexic';
|
@import 'dyslexic';
|
||||||
@import 'demetricator';
|
@import 'demetricator';
|
||||||
@import 'pro';
|
@import 'pro';
|
||||||
@import 'overflow_hacks';
|
|
||||||
@import 'chats';
|
@import 'chats';
|
||||||
|
|
||||||
// COMPONENTS
|
// COMPONENTS
|
||||||
|
|
|
@ -322,7 +322,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--accent-color--faint);
|
background: var(--accent-color--faint);
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
|
|
||||||
.column-back-button {
|
.column-back-button {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
|
@ -212,7 +212,6 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
text-align: unset;
|
text-align: unset;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -62,7 +62,6 @@
|
||||||
border-bottom: 1px solid var(--brand-color--faint);
|
border-bottom: 1px solid var(--brand-color--faint);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status__link {
|
.detailed-status__link {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
.column,
|
.column,
|
||||||
.drawer {
|
.drawer {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer__pager {
|
.drawer__pager {
|
||||||
|
|
|
@ -15,19 +15,14 @@
|
||||||
transition-duration: 0.2s;
|
transition-duration: 0.2s;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
left: -10px;
|
top: 0;
|
||||||
padding: 20px;
|
left: 0;
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
&--visible {
|
&--visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(min-width: 750px) {
|
|
||||||
left: -100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-hover-card__container {
|
.profile-hover-card__container {
|
||||||
@include standard-panel;
|
@include standard-panel;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -114,18 +109,12 @@
|
||||||
.detailed-status {
|
.detailed-status {
|
||||||
.profile-hover-card {
|
.profile-hover-card {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 60px;
|
left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent floating avatars from intercepting with current card */
|
/* Hide the popper when the reference is hidden */
|
||||||
.status,
|
#popper[data-popper-reference-hidden] {
|
||||||
.detailed-status {
|
visibility: hidden;
|
||||||
.floating-link {
|
pointer-events: none;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .floating-link {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,7 +188,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
// This is a file dedicated to fixing the css we broke by introducing the hover
|
|
||||||
// card and `overflow:visible` on drawer.scss line 23. If we ever figure out how
|
|
||||||
// to pop the hover card out while keeping `overflow:hidden`, feel free to delete
|
|
||||||
// this entire file.
|
|
||||||
|
|
||||||
button.column-header__button.active {
|
|
||||||
border-radius: 0 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-back-button.column-back-button--slim-button {
|
|
||||||
border-radius: 0 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailed-status__wrapper .detailed-status__action-bar {
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slist .item-list .column-link {
|
|
||||||
background-color: transparent;
|
|
||||||
border-top: 1px solid var(--brand-color--med);
|
|
||||||
}
|
|
||||||
|
|
||||||
.focusable {
|
|
||||||
&:focus {
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.load-more:hover {
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this still looks like shit but at least it's better than it overflowing
|
|
||||||
|
|
||||||
.empty-column-indicator {
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
}
|
|
|
@ -46,6 +46,7 @@
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@babel/runtime": "^7.3.4",
|
"@babel/runtime": "^7.3.4",
|
||||||
"@clusterws/cws": "^0.16.0",
|
"@clusterws/cws": "^0.16.0",
|
||||||
|
"@popperjs/core": "^2.4.4",
|
||||||
"array-includes": "^3.0.3",
|
"array-includes": "^3.0.3",
|
||||||
"autoprefixer": "^9.5.1",
|
"autoprefixer": "^9.5.1",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
|
@ -114,6 +115,7 @@
|
||||||
"react-motion": "^0.5.2",
|
"react-motion": "^0.5.2",
|
||||||
"react-notification": "^6.8.4",
|
"react-notification": "^6.8.4",
|
||||||
"react-overlays": "^0.8.3",
|
"react-overlays": "^0.8.3",
|
||||||
|
"react-popper": "^2.2.3",
|
||||||
"react-redux": "^7.2.1",
|
"react-redux": "^7.2.1",
|
||||||
"react-redux-loading-bar": "^4.5.0",
|
"react-redux-loading-bar": "^4.5.0",
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -1489,6 +1489,11 @@
|
||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
|
|
||||||
|
"@popperjs/core@^2.4.4":
|
||||||
|
version "2.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398"
|
||||||
|
integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==
|
||||||
|
|
||||||
"@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^1.7.0":
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
|
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
|
||||||
|
@ -9490,6 +9495,11 @@ react-fast-compare@^2.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
|
react-fast-compare@^3.0.1:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||||
|
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||||
|
|
||||||
react-helmet@^6.0.0:
|
react-helmet@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.0.0.tgz#fcb93ebaca3ba562a686eb2f1f9d46093d83b5f8"
|
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.0.0.tgz#fcb93ebaca3ba562a686eb2f1f9d46093d83b5f8"
|
||||||
|
@ -9617,6 +9627,14 @@ react-overlays@^0.8.3:
|
||||||
react-transition-group "^2.2.0"
|
react-transition-group "^2.2.0"
|
||||||
warning "^3.0.0"
|
warning "^3.0.0"
|
||||||
|
|
||||||
|
react-popper@^2.2.3:
|
||||||
|
version "2.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.3.tgz#33d425fa6975d4bd54d9acd64897a89d904b9d97"
|
||||||
|
integrity sha512-mOEiMNT1249js0jJvkrOjyHsGvqcJd3aGW/agkiMoZk3bZ1fXN1wQszIQSjHIai48fE67+zwF8Cs+C4fWqlfjw==
|
||||||
|
dependencies:
|
||||||
|
react-fast-compare "^3.0.1"
|
||||||
|
warning "^4.0.2"
|
||||||
|
|
||||||
react-redux-loading-bar@^4.5.0:
|
react-redux-loading-bar@^4.5.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux-loading-bar/-/react-redux-loading-bar-4.5.0.tgz#96538d0ba041463d810e213fb54eadbce9628266"
|
resolved "https://registry.yarnpkg.com/react-redux-loading-bar/-/react-redux-loading-bar-4.5.0.tgz#96538d0ba041463d810e213fb54eadbce9628266"
|
||||||
|
@ -11878,6 +11896,13 @@ warning@^4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.0.0"
|
loose-envify "^1.0.0"
|
||||||
|
|
||||||
|
warning@^4.0.2:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||||
|
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.0.0"
|
||||||
|
|
||||||
watchpack@^1.6.0:
|
watchpack@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
|
||||||
|
|
Loading…
Reference in a new issue