Hovercard: basic Redux+Popper prototype
This commit is contained in:
parent
3e473e0327
commit
0c4eae5f10
8 changed files with 121 additions and 13 deletions
9
app/soapbox/actions/profile_hover_card.js
Normal file
9
app/soapbox/actions/profile_hover_card.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN';
|
||||||
|
|
||||||
|
export function openProfileHoverCard(ref, accountId) {
|
||||||
|
return {
|
||||||
|
type: PROFILE_HOVER_CARD_OPEN,
|
||||||
|
ref,
|
||||||
|
accountId,
|
||||||
|
};
|
||||||
|
}
|
76
app/soapbox/components/profile_hover_card.js
Normal file
76
app/soapbox/components/profile_hover_card.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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']));
|
||||||
|
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}>
|
||||||
|
<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,7 +18,6 @@ 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 { isMobile } from '../../../app/soapbox/is_mobile';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { getDomain } from 'soapbox/utils/accounts';
|
import { getDomain } from 'soapbox/utils/accounts';
|
||||||
|
@ -82,6 +81,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onEmbed: PropTypes.func,
|
onEmbed: PropTypes.func,
|
||||||
onHeightChange: PropTypes.func,
|
onHeightChange: PropTypes.func,
|
||||||
onToggleHidden: PropTypes.func,
|
onToggleHidden: PropTypes.func,
|
||||||
|
onShowProfileCard: PropTypes.func,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
|
@ -257,7 +257,8 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
showProfileCard = debounce(() => {
|
showProfileCard = debounce(() => {
|
||||||
this.setState({ profileCardVisible: true });
|
const { onShowProfileCard, status } = this.props;
|
||||||
|
onShowProfileCard(this.profileNode, status.getIn(['account', 'id']));
|
||||||
}, 1200);
|
}, 1200);
|
||||||
|
|
||||||
handleProfileHover = e => {
|
handleProfileHover = e => {
|
||||||
|
@ -283,6 +284,10 @@ class Status extends ImmutablePureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setProfileRef = c => {
|
||||||
|
this.profileNode = c;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let media = null;
|
let media = null;
|
||||||
let poll = null;
|
let poll = null;
|
||||||
|
@ -457,7 +462,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,7 +482,7 @@ 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' ref={this.setProfileRef} onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
||||||
<div className='status__avatar'>
|
<div className='status__avatar'>
|
||||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
||||||
{statusAvatar}
|
{statusAvatar}
|
||||||
|
@ -486,9 +490,6 @@ class Status extends ImmutablePureComponent {
|
||||||
<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>
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
groupRemoveStatus,
|
groupRemoveStatus,
|
||||||
} from '../actions/groups';
|
} from '../actions/groups';
|
||||||
import { getSettings } from '../actions/settings';
|
import { getSettings } from '../actions/settings';
|
||||||
|
import { openProfileHoverCard } from 'soapbox/actions/profile_hover_card';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
|
@ -206,6 +207,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(groupRemoveStatus(groupId, statusId));
|
dispatch(groupRemoveStatus(groupId, statusId));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onShowProfileCard(ref, accountId) {
|
||||||
|
dispatch(openProfileHoverCard(ref, accountId));
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||||
|
|
|
@ -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);
|
||||||
|
|
16
app/soapbox/reducers/profile_hover_card.js
Normal file
16
app/soapbox/reducers/profile_hover_card.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { PROFILE_HOVER_CARD_OPEN } 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,
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,8 @@
|
||||||
transition-duration: 0.2s;
|
transition-duration: 0.2s;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
left: -10px;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
@ -24,10 +25,6 @@
|
||||||
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,7 +111,7 @@
|
||||||
.detailed-status {
|
.detailed-status {
|
||||||
.profile-hover-card {
|
.profile-hover-card {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 60px;
|
left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue