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