diff --git a/app/soapbox/actions/profile_hover_card.js b/app/soapbox/actions/profile_hover_card.js new file mode 100644 index 000000000..7014858ae --- /dev/null +++ b/app/soapbox/actions/profile_hover_card.js @@ -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, + }; +} diff --git a/app/soapbox/components/profile_hover_card.js b/app/soapbox/components/profile_hover_card.js new file mode 100644 index 000000000..c6319d38b --- /dev/null +++ b/app/soapbox/components/profile_hover_card.js @@ -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(); + if (isModerator(account)) badges.push(); + if (account.getIn(['patron', 'is_patron'])) badges.push(); + 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 ( +
+
+ {followedBy && + + + } +
+ + {badges.length > 0 && +
+ {badges} +
} + {account.getIn(['source', 'note'], '').length > 0 && +
} +
+
+ ); +}; + +ProfileHoverCard.propTypes = { + visible: PropTypes.bool, + accountId: PropTypes.string, + account: ImmutablePropTypes.map, + intl: PropTypes.object.isRequired, +}; + +ProfileHoverCard.defaultProps = { + visible: true, +}; + +export default injectIntl(ProfileHoverCard); diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 8acb4e7f5..d0d182b7d 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -18,7 +18,6 @@ import classNames from 'classnames'; import Icon from 'soapbox/components/icon'; import PollContainer from 'soapbox/containers/poll_container'; 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'; @@ -82,6 +81,7 @@ class Status extends ImmutablePureComponent { onEmbed: PropTypes.func, onHeightChange: PropTypes.func, onToggleHidden: PropTypes.func, + onShowProfileCard: PropTypes.func, muted: PropTypes.bool, hidden: PropTypes.bool, unread: PropTypes.bool, @@ -257,7 +257,8 @@ class Status extends ImmutablePureComponent { } showProfileCard = debounce(() => { - this.setState({ profileCardVisible: true }); + const { onShowProfileCard, status } = this.props; + onShowProfileCard(this.profileNode, status.getIn(['account', 'id'])); }, 1200); handleProfileHover = e => { @@ -283,6 +284,10 @@ class Status extends ImmutablePureComponent { this.node = c; } + setProfileRef = c => { + this.profileNode = c; + } + render() { let media = null; let poll = null; @@ -457,7 +462,6 @@ class Status extends ImmutablePureComponent { }; const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`; - const { profileCardVisible } = this.state; const favicon = status.getIn(['account', 'pleroma', 'favicon']); const domain = getDomain(status.get('account')); @@ -478,7 +482,7 @@ class Status extends ImmutablePureComponent {
} -
+
{statusAvatar} @@ -486,9 +490,6 @@ class Status extends ImmutablePureComponent { - { profileCardVisible && - - }
diff --git a/app/soapbox/containers/status_container.js b/app/soapbox/containers/status_container.js index 02dc76c21..d2ae6f7e6 100644 --- a/app/soapbox/containers/status_container.js +++ b/app/soapbox/containers/status_container.js @@ -35,6 +35,7 @@ import { groupRemoveStatus, } from '../actions/groups'; import { getSettings } from '../actions/settings'; +import { openProfileHoverCard } from 'soapbox/actions/profile_hover_card'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -206,6 +207,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(groupRemoveStatus(groupId, statusId)); }, + onShowProfileCard(ref, accountId) { + dispatch(openProfileHoverCard(ref, accountId)); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index ff80086ee..3f37ffe40 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -38,6 +38,7 @@ import { Redirect } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import { isStaff } from 'soapbox/utils/accounts'; import ChatPanes from 'soapbox/features/chats/components/chat_panes'; +import ProfileHoverCard from 'soapbox/components/profile_hover_card'; import { Status, @@ -650,6 +651,7 @@ class UI extends React.PureComponent { {me && } {me && !mobile && } +
); diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index 2caec469a..6f4abdafe 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -46,6 +46,7 @@ import admin from './admin'; import chats from './chats'; import chat_messages from './chat_messages'; import chat_message_lists from './chat_message_lists'; +import profile_hover_card from './profile_hover_card'; const reducers = { dropdown_menu, @@ -95,6 +96,7 @@ const reducers = { chats, chat_messages, chat_message_lists, + profile_hover_card, }; export default combineReducers(reducers); diff --git a/app/soapbox/reducers/profile_hover_card.js b/app/soapbox/reducers/profile_hover_card.js new file mode 100644 index 000000000..907142656 --- /dev/null +++ b/app/soapbox/reducers/profile_hover_card.js @@ -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; + } +} diff --git a/app/styles/components/profile_hover_card.scss b/app/styles/components/profile_hover_card.scss index decb68711..6d2ead6e1 100644 --- a/app/styles/components/profile_hover_card.scss +++ b/app/styles/components/profile_hover_card.scss @@ -15,7 +15,8 @@ transition-duration: 0.2s; width: 320px; z-index: 200; - left: -10px; + top: 0; + left: 0; padding: 20px; margin-bottom: 10px; @@ -24,10 +25,6 @@ pointer-events: all; } - @media(min-width: 750px) { - left: -100px; - } - .profile-hover-card__container { @include standard-panel; position: relative; @@ -114,7 +111,7 @@ .detailed-status { .profile-hover-card { top: 0; - left: 60px; + left: 0; } }