From 563e4e5bab8381f1a575f8ce5899892f3a8710ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1rbara=20de=20Castro=20Fernandes?= Date: Tue, 16 Jun 2020 09:06:44 -0300 Subject: [PATCH 01/16] Show profile preview on hover --- app/soapbox/components/status.js | 31 ++++++++++++++----- .../status/components/detailed_status.js | 26 +++++++++++++--- .../features/ui/components/user_panel.js | 26 ++++++++++------ app/soapbox/pages/home_page.js | 5 +-- app/styles/components/drawer.scss | 2 +- app/styles/components/status.scss | 22 ++++++++++++- app/styles/components/user-panel.scss | 8 ++++- 7 files changed, 95 insertions(+), 25 deletions(-) diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 889afc01e..41ccfc0ff 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -18,6 +18,7 @@ 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'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -104,6 +105,9 @@ class Status extends ImmutablePureComponent { state = { showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia), statusId: undefined, + profilePanelVisible: false, + profilePanelX: 0, + profilePanelY: 0, }; // Track height changes we know about to compensate scrolling @@ -249,6 +253,16 @@ 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 }); + } + + handleProfileLeave = e => { + if (!this.isMobile()) this.setState({ profilePanelVisible: false }); + } + _properStatus() { const { status } = this.props; @@ -435,6 +449,7 @@ class Status extends ImmutablePureComponent { }; const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`; + const { profilePanelVisible, profilePanelX, profilePanelY } = this.state; return ( @@ -448,13 +463,15 @@ class Status extends ImmutablePureComponent { - -
- {statusAvatar} -
- - -
+
+ +
+ {statusAvatar} +
+ +
+ +
{!group && status.get('group') && ( diff --git a/app/soapbox/features/status/components/detailed_status.js b/app/soapbox/features/status/components/detailed_status.js index a31d826be..a88395e2a 100644 --- a/app/soapbox/features/status/components/detailed_status.js +++ b/app/soapbox/features/status/components/detailed_status.js @@ -16,6 +16,7 @@ import classNames from 'classnames'; import Icon from 'soapbox/components/icon'; import PollContainer from 'soapbox/containers/poll_container'; import { StatusInteractionBar } from './status_interaction_bar'; +import UserPanel from '../../ui/components/user_panel'; export default class DetailedStatus extends ImmutablePureComponent { @@ -38,6 +39,9 @@ export default class DetailedStatus extends ImmutablePureComponent { state = { height: null, + profilePanelVisible: false, + profilePanelX: 0, + profilePanelY: 0, }; handleOpenVideo = (media, startTime) => { @@ -81,10 +85,21 @@ export default class DetailedStatus extends ImmutablePureComponent { window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); } + 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 }); + } + + handleProfileLeave = e => { + if (!this.isMobile()) this.setState({ profilePanelVisible: false }); + } + render() { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; + const { profilePanelVisible, profilePanelX, profilePanelY } = this.state; if (!status) { return null; @@ -158,10 +173,13 @@ export default class DetailedStatus extends ImmutablePureComponent { return (
- -
- -
+
+ +
+ + +
+
{status.get('group') && (
diff --git a/app/soapbox/features/ui/components/user_panel.js b/app/soapbox/features/ui/components/user_panel.js index 83dfd2099..da515ffed 100644 --- a/app/soapbox/features/ui/components/user_panel.js +++ b/app/soapbox/features/ui/components/user_panel.js @@ -10,6 +10,7 @@ import Avatar from 'soapbox/components/avatar'; import { shortNumberFormat } from 'soapbox/utils/numbers'; import { acctFull } from 'soapbox/utils/accounts'; import StillImage from 'soapbox/components/still_image'; +import classNames from 'classnames'; class UserPanel extends ImmutablePureComponent { @@ -17,16 +18,23 @@ class UserPanel extends ImmutablePureComponent { account: ImmutablePropTypes.map, intl: PropTypes.object.isRequired, domain: PropTypes.string, + style: PropTypes.object, + visible: PropTypes.bool, + } + + static defaultProps = { + style: {}, + visible: true, } render() { - const { account, intl, domain } = this.props; + const { account, intl, domain, style, visible } = this.props; if (!account) return null; const displayNameHtml = { __html: account.get('display_name_html') }; const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); return ( -
+
@@ -84,17 +92,17 @@ class UserPanel extends ImmutablePureComponent { }; - -const mapStateToProps = state => { - const me = state.get('me'); +const makeMapStateToProps = () => { const getAccount = makeGetAccount(); - return { - account: getAccount(state, me), - }; + const mapStateToProps = (state, { accountId }) => ({ + account: getAccount(state, accountId), + }); + + return mapStateToProps; }; export default injectIntl( - connect(mapStateToProps, null, null, { + connect(makeMapStateToProps, null, null, { forwardRef: true, })(UserPanel)); diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js index f06bd90eb..eeeafede9 100644 --- a/app/soapbox/pages/home_page.js +++ b/app/soapbox/pages/home_page.js @@ -15,6 +15,7 @@ import { getFeatures } from 'soapbox/utils/features'; const mapStateToProps = state => { const me = state.get('me'); return { + me, account: state.getIn(['accounts', me]), hasPatron: state.getIn(['soapbox', 'extensions', 'patron', 'enabled']), features: getFeatures(state.get('instance')), @@ -30,7 +31,7 @@ class HomePage extends ImmutablePureComponent { } render() { - const { children, account, hasPatron, features } = this.props; + const { me, children, account, hasPatron, features } = this.props; return (
@@ -39,7 +40,7 @@ class HomePage extends ImmutablePureComponent {
- + {hasPatron && } diff --git a/app/styles/components/drawer.scss b/app/styles/components/drawer.scss index 7dc5a1a10..39e936158 100644 --- a/app/styles/components/drawer.scss +++ b/app/styles/components/drawer.scss @@ -20,7 +20,7 @@ .column, .drawer { flex: 1 1 100%; - overflow: hidden; + overflow: visible; } .drawer__pager { diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index 0d349764b..f167cf2ac 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -152,7 +152,6 @@ .status__info .status__display-name { display: block; max-width: 100%; - padding-right: 25px; } .status__info { @@ -160,6 +159,27 @@ z-index: 4; } +.status__profile, +.detailed-status__profile { + display: inline-block; + + .user-panel { + position: absolute; + display: flex; + opacity: 0; + pointer-events: none; + transition-property: opacity; + transition-duration: 0.5s; + z-index: 999; + + &--visible { + opacity: 1; + transition-delay: 1s; + pointer-events: all; + } + } +} + .status-check-box { border-bottom: 1px solid var(--background-color); display: flex; diff --git a/app/styles/components/user-panel.scss b/app/styles/components/user-panel.scss index cf49a3d68..44ec3cd9b 100644 --- a/app/styles/components/user-panel.scss +++ b/app/styles/components/user-panel.scss @@ -3,7 +3,13 @@ display: flex; width: 265px; flex-direction: column; - overflow-y: hidden; + + &, + .user-panel__account__name, + .user-panel__account__username { + overflow: hidden; + text-overflow: ellipsis; + } &__header { display: block; From f6ebe5cbd76c36dfa145394421ef634cc189e1ed Mon Sep 17 00:00:00 2001 From: Mary Kate Date: Tue, 28 Jul 2020 20:01:16 -0500 Subject: [PATCH 02/16] 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 =