From f6ebe5cbd76c36dfa145394421ef634cc189e1ed Mon Sep 17 00:00:00 2001 From: Mary Kate Date: Tue, 28 Jul 2020 20:01:16 -0500 Subject: [PATCH] profile card basic functionality, needs some UI improvements --- app/soapbox/components/display_name.js | 5 +- app/soapbox/components/status.js | 20 ++-- app/soapbox/containers/account_container.js | 1 - .../profile_hover_card_container.js | 67 +++++++++++++ .../features/ui/components/action_button.js | 98 +++++++++++++++++++ .../features/ui/components/user_panel.js | 6 +- app/styles/application.scss | 1 + app/styles/components/profile_hover_card.scss | 54 ++++++++++ app/styles/components/status.scss | 16 --- 9 files changed, 235 insertions(+), 33 deletions(-) create mode 100644 app/soapbox/features/profile_hover_card/profile_hover_card_container.js create mode 100644 app/soapbox/features/ui/components/action_button.js create mode 100644 app/styles/components/profile_hover_card.scss diff --git a/app/soapbox/components/display_name.js b/app/soapbox/components/display_name.js index 1016c9f5a..30dbce9a2 100644 --- a/app/soapbox/components/display_name.js +++ b/app/soapbox/components/display_name.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import VerificationBadge from './verification_badge'; import { acctFull } from '../utils/accounts'; @@ -8,10 +9,11 @@ export default class DisplayName extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, others: ImmutablePropTypes.list, + children: PropTypes.node, }; render() { - const { account, others } = this.props; + const { account, others, children } = this.props; let displayName, suffix; @@ -40,6 +42,7 @@ export default class DisplayName extends React.PureComponent { {displayName} {suffix} + {children} ); } diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 41ccfc0ff..c3af6fd2d 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -18,7 +18,8 @@ import classNames from 'classnames'; import Icon from 'soapbox/components/icon'; import PollContainer from 'soapbox/containers/poll_container'; import { NavLink } from 'react-router-dom'; -import UserPanel from '../features/ui/components/user_panel'; +import ProfileHoverCardContainer from '../features/profile_hover_card/profile_hover_card_container'; +import { isMobile } from '../../../app/soapbox/is_mobile'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -105,9 +106,7 @@ class Status extends ImmutablePureComponent { state = { showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia), statusId: undefined, - profilePanelVisible: false, - profilePanelX: 0, - profilePanelY: 0, + profileCardVisible: false, }; // Track height changes we know about to compensate scrolling @@ -253,14 +252,12 @@ class Status extends ImmutablePureComponent { this.handleToggleMediaVisibility(); } - isMobile = () => window.matchMedia('only screen and (max-width: 895px)').matches; - handleProfileHover = e => { - if (!this.isMobile()) this.setState({ profilePanelVisible: true, profilePanelX: e.nativeEvent.offsetX, profilePanelY: e.nativeEvent.offsetY }); + if (!isMobile()) this.setState({ profileCardVisible: true }); } handleProfileLeave = e => { - if (!this.isMobile()) this.setState({ profilePanelVisible: false }); + if (!isMobile()) this.setState({ profileCardVisible: false }); } _properStatus() { @@ -449,7 +446,7 @@ class Status extends ImmutablePureComponent { }; const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`; - const { profilePanelVisible, profilePanelX, profilePanelY } = this.state; + const { profileCardVisible } = this.state; return ( @@ -468,9 +465,10 @@ class Status extends ImmutablePureComponent {
{statusAvatar}
- - + + + diff --git a/app/soapbox/containers/account_container.js b/app/soapbox/containers/account_container.js index 8e1f067de..1e890a7e2 100644 --- a/app/soapbox/containers/account_container.js +++ b/app/soapbox/containers/account_container.js @@ -66,7 +66,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onMuteNotifications(account, notifications) { dispatch(muteAccount(account.get('id'), notifications)); }, diff --git a/app/soapbox/features/profile_hover_card/profile_hover_card_container.js b/app/soapbox/features/profile_hover_card/profile_hover_card_container.js new file mode 100644 index 000000000..d9ae0986f --- /dev/null +++ b/app/soapbox/features/profile_hover_card/profile_hover_card_container.js @@ -0,0 +1,67 @@ +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'; + +const getAccount = makeGetAccount(); + +const mapStateToProps = (state, { accountId }) => { + return { + account: getAccount(state, accountId), + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class ProfileHoverCardContainer extends ImmutablePureComponent { + + static propTypes = { + visible: PropTypes.bool, + accountId: PropTypes.string, + account: ImmutablePropTypes.map, + intl: PropTypes.object.isRequired, + } + + static defaultProps = { + visible: true, + } + + render() { + const { visible, accountId, account } = this.props; + if (!accountId) return null; + const accountBio = { __html: account.get('note_emojified') }; + let followed_by = account.getIn(['relationship', 'followed_by']); + + return visible && ( +
+
+
+ +
+ {isAdmin(account) && } + {isModerator(account) && } + {account.getIn(['patron', 'is_patron']) && } + { followed_by ? + + + + : '' } +
+
+
+
+ ); + } + +}; diff --git a/app/soapbox/features/ui/components/action_button.js b/app/soapbox/features/ui/components/action_button.js new file mode 100644 index 000000000..e1c88d232 --- /dev/null +++ b/app/soapbox/features/ui/components/action_button.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; +import Button from 'soapbox/components/button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import classNames from 'classnames'; +import { + followAccount, + unfollowAccount, + blockAccount, + unblockAccount, +} from 'soapbox/actions/accounts'; + +const messages = defineMessages({ + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, +}); + +const mapStateToProps = state => { + const me = state.get('me'); + return { + me, + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + onFollow(account) { + if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + dispatch(unfollowAccount(account.get('id'))); + } else { + dispatch(followAccount(account.get('id'))); + } + }, + + onBlock(account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(blockAccount(account.get('id'))); + } + }, +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class ActionButton extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + componentDidMount() { + window.addEventListener('resize', this.handleResize, { passive: true }); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + + handleFollow = () => { + this.props.onFollow(this.props.account); + } + + handleBlock = () => { + this.props.onBlock(this.props.account); + } + + render() { + const { account, intl, me } = this.props; + let actionBtn = null; + + if (!account || !me) return actionBtn; + + if (me !== account.get('id')) { + if (!account.get('relationship')) { // Wait until the relationship is loaded + // + } else if (account.getIn(['relationship', 'requested'])) { + actionBtn =