Skip to content

Commit

Permalink
Change appearance of account cards in web UI (#17689)
Browse files Browse the repository at this point in the history
* Change appearance of account cards in web UI

* Various fixes and improvements

* Various fixes and improvements
  • Loading branch information
Gargron authored Mar 7, 2022
1 parent 292c75a commit dba4be1
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 347 deletions.
204 changes: 80 additions & 124 deletions app/javascript/mastodon/features/directory/components/account_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,28 @@ import { makeGetAccount } from 'mastodon/selectors';
import Avatar from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import Permalink from 'mastodon/components/permalink';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import IconButton from 'mastodon/components/icon_button';
import Button from 'mastodon/components/button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
import ShortNumber from 'mastodon/components/short_number';
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
import classNames from 'classnames';

const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unfollowConfirm: {
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
},
follow: { id: 'account.follow', defaultMessage: 'Follow' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});

const makeMapStateToProps = () => {
Expand Down Expand Up @@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onBlock(account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(blockAccount(account.get('id')));
}
},

onMute(account) {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},

});

export default
Expand Down Expand Up @@ -138,130 +132,92 @@ class AccountCard extends ImmutablePureComponent {

handleMute = () => {
this.props.onMute(this.props.account);
};
}

handleEditProfile = () => {
window.open('/settings/profile', '_blank');
}

render() {
const { account, intl } = this.props;

let buttons;

if (
account.get('id') !== me &&
account.get('relationship', null) !== null
) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);

if (requested) {
buttons = (
<IconButton
disabled
icon='hourglass'
title={intl.formatMessage(messages.requested)}
/>
);
} else if (blocking) {
buttons = (
<IconButton
active
icon='unlock'
title={intl.formatMessage(messages.unblock, {
name: account.get('username'),
})}
onClick={this.handleBlock}
/>
);
} else if (muting) {
buttons = (
<IconButton
active
icon='volume-up'
title={intl.formatMessage(messages.unmute, {
name: account.get('username'),
})}
onClick={this.handleMute}
/>
);
} else if (!account.get('moved') || following) {
buttons = (
<IconButton
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow,
)}
onClick={this.handleFollow}
active={following}
/>
);
let actionBtn;

if (me !== account.get('id')) {
if (!account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
} else if (account.getIn(['relationship', 'muting'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
}
} else {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
}

return (
<div className='directory__card'>
<div className='directory__card__img'>
<img
src={
autoPlayGif ? account.get('header') : account.get('header_static')
}
alt=''
/>
</div>
<div className='account-card'>
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
<div className='account-card__header'>
<img
src={
autoPlayGif ? account.get('header') : account.get('header_static')
}
alt=''
/>
</div>

<div className='directory__card__bar'>
<Permalink
className='directory__card__bar__name'
href={account.get('url')}
to={`/@${account.get('acct')}`}
>
<Avatar account={account} size={48} />
<div className='account-card__title'>
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
<DisplayName account={account} />
</Permalink>

<div className='directory__card__bar__relationship account__relationship'>
{buttons}
</div>
</div>
</Permalink>

<div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{account.get('note').length > 0 && (
<div
className='account__header__content translate'
className='account-card__bio translate'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
</div>

<div className='directory__card__extra'>
<div className='accounts-table__count'>
<ShortNumber value={account.get('statuses_count')} />
<small>
<FormattedMessage id='account.posts' defaultMessage='Toots' />
</small>
)}

<div className='account-card__actions'>
<div className='account-card__counters'>
<div className='account-card__counters__item'>
<ShortNumber value={account.get('statuses_count')} />
<small>
<FormattedMessage id='account.posts' defaultMessage='Toots' />
</small>
</div>

<div className='account-card__counters__item'>
<ShortNumber value={account.get('followers_count')} />{' '}
<small>
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
</small>
</div>

<div className='account-card__counters__item'>
<ShortNumber value={account.get('following_count')} />{' '}
<small>
<FormattedMessage
id='account.following'
defaultMessage='Following'
/>
</small>
</div>
</div>
<div className='accounts-table__count'>
<ShortNumber value={account.get('followers_count')} />{' '}
<small>
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
</small>
</div>
<div className='accounts-table__count'>
{account.get('last_status_at') === null ? (
<FormattedMessage
id='account.never_active'
defaultMessage='Never'
/>
) : (
<RelativeTimestamp timestamp={account.get('last_status_at')} />
)}{' '}
<small>
<FormattedMessage
id='account.last_status'
defaultMessage='Last active'
/>
</small>

<div className='account-card__actions__button'>
{actionBtn}
</div>
</div>
</div>
Expand Down
10 changes: 6 additions & 4 deletions app/javascript/mastodon/features/directory/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
import { List as ImmutableList } from 'immutable';
import AccountCard from './components/account_card';
import RadioButton from 'mastodon/components/radio_button';
import classNames from 'classnames';
import LoadMore from 'mastodon/components/load_more';
import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadingIndicator from 'mastodon/components/loading_indicator';

const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
Expand Down Expand Up @@ -129,7 +129,7 @@ class Directory extends React.PureComponent {
const pinned = !!columnId;

const scrollableArea = (
<div className='scrollable' style={{ background: 'transparent' }}>
<div className='scrollable'>
<div className='filter-form'>
<div className='filter-form__column' role='group'>
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
Expand All @@ -142,8 +142,10 @@ class Directory extends React.PureComponent {
</div>
</div>

<div className={classNames('directory__list', { loading: isLoading })}>
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
<div className='directory__list'>
{isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
<AccountCard id={accountId} key={accountId} />
))}
</div>

<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
Expand Down
8 changes: 4 additions & 4 deletions app/javascript/mastodon/features/explore/suggestions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Account from 'mastodon/containers/account_container';
import AccountCard from 'mastodon/features/directory/components/account_card';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
Expand Down Expand Up @@ -29,9 +29,9 @@ class Suggestions extends React.PureComponent {
const { isLoading, suggestions } = this.props;

return (
<div className='explore__links'>
{isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => (
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
<div className='explore__suggestions'>
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
))}
</div>
);
Expand Down
13 changes: 2 additions & 11 deletions app/javascript/styles/mastodon-light/diff.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,11 @@ html {
background: lighten($ui-base-color, 12%);
}

.filter-form,
.directory__card__bar {
.filter-form {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}

.scrollable .directory__list {
width: calc(100% + 2px);
margin-left: -1px;
margin-right: -1px;
}

.directory__card,
.table-of-contents {
border: 1px solid lighten($ui-base-color, 8%);
}
Expand All @@ -75,8 +67,7 @@ html {
.column-header__back-button,
.column-header__button,
.column-header__button.active,
.account__header__bar,
.directory__card__extra {
.account__header__bar {
background: $white;
}

Expand Down
Loading

0 comments on commit dba4be1

Please sign in to comment.