Change appearance of account cards in web UI (#17689)

* Change appearance of account cards in web UI

* Various fixes and improvements

* Various fixes and improvements
signup-info-prompt
Eugen Rochko 2022-03-07 11:38:52 +01:00 committed by GitHub
parent 292c75aa31
commit dba4be1038
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 178 additions and 347 deletions

View File

@ -7,31 +7,28 @@ import { makeGetAccount } from 'mastodon/selectors';
import Avatar from 'mastodon/components/avatar'; import Avatar from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name'; import DisplayName from 'mastodon/components/display_name';
import Permalink from 'mastodon/components/permalink'; import Permalink from 'mastodon/components/permalink';
import RelativeTimestamp from 'mastodon/components/relative_timestamp'; import Button from 'mastodon/components/button';
import IconButton from 'mastodon/components/icon_button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state'; import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
import ShortNumber from 'mastodon/components/short_number'; import ShortNumber from 'mastodon/components/short_number';
import { import {
followAccount, followAccount,
unfollowAccount, unfollowAccount,
blockAccount,
unblockAccount, unblockAccount,
unmuteAccount, unmuteAccount,
} from 'mastodon/actions/accounts'; } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes'; import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unfollowConfirm: { unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
id: 'confirmations.unfollow.confirm', unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
defaultMessage: 'Unfollow', unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
}, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onBlock(account) { onBlock(account) {
if (account.getIn(['relationship', 'blocking'])) { if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id'))); dispatch(unblockAccount(account.get('id')));
} else {
dispatch(blockAccount(account.get('id')));
} }
}, },
onMute(account) { onMute(account) {
if (account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id'))); dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
} }
}, },
}); });
export default export default
@ -138,130 +132,92 @@ class AccountCard extends ImmutablePureComponent {
handleMute = () => { handleMute = () => {
this.props.onMute(this.props.account); this.props.onMute(this.props.account);
}; }
handleEditProfile = () => {
window.open('/settings/profile', '_blank');
}
render() { render() {
const { account, intl } = this.props; const { account, intl } = this.props;
let buttons; let actionBtn;
if ( if (me !== account.get('id')) {
account.get('id') !== me && if (!account.get('relationship')) { // Wait until the relationship is loaded
account.get('relationship', null) !== null actionBtn = '';
) { } else if (account.getIn(['relationship', 'requested'])) {
const following = account.getIn(['relationship', 'following']); actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
const requested = account.getIn(['relationship', 'requested']); } else if (account.getIn(['relationship', 'muting'])) {
const blocking = account.getIn(['relationship', 'blocking']); actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
const muting = account.getIn(['relationship', 'muting']); } 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} />;
if (requested) { } else if (account.getIn(['relationship', 'blocking'])) {
buttons = ( actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
<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}
/>
);
} }
} else {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
} }
return ( return (
<div className='directory__card'> <div className='account-card'>
<div className='directory__card__img'> <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
<img <div className='account-card__header'>
src={ <img
autoPlayGif ? account.get('header') : account.get('header_static') src={
} autoPlayGif ? account.get('header') : account.get('header_static')
alt='' }
/> 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} />
<DisplayName account={account} />
</Permalink>
<div className='directory__card__bar__relationship account__relationship'>
{buttons}
</div> </div>
</div>
<div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='account-card__title'>
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
<DisplayName account={account} />
</div>
</Permalink>
{account.get('note').length > 0 && (
<div <div
className='account__header__content translate' className='account-card__bio translate'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/> />
</div> )}
<div className='directory__card__extra'> <div className='account-card__actions'>
<div className='accounts-table__count'> <div className='account-card__counters'>
<ShortNumber value={account.get('statuses_count')} /> <div className='account-card__counters__item'>
<small> <ShortNumber value={account.get('statuses_count')} />
<FormattedMessage id='account.posts' defaultMessage='Toots' /> <small>
</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>
<div className='accounts-table__count'>
<ShortNumber value={account.get('followers_count')} />{' '} <div className='account-card__actions__button'>
<small> {actionBtn}
<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> </div>
</div> </div>
</div> </div>

View File

@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import AccountCard from './components/account_card'; import AccountCard from './components/account_card';
import RadioButton from 'mastodon/components/radio_button'; import RadioButton from 'mastodon/components/radio_button';
import classNames from 'classnames';
import LoadMore from 'mastodon/components/load_more'; import LoadMore from 'mastodon/components/load_more';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadingIndicator from 'mastodon/components/loading_indicator';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@ -129,7 +129,7 @@ class Directory extends React.PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
const scrollableArea = ( const scrollableArea = (
<div className='scrollable' style={{ background: 'transparent' }}> <div className='scrollable'>
<div className='filter-form'> <div className='filter-form'>
<div className='filter-form__column' role='group'> <div className='filter-form__column' role='group'>
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} /> <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
@ -142,8 +142,10 @@ class Directory extends React.PureComponent {
</div> </div>
</div> </div>
<div className={classNames('directory__list', { loading: isLoading })}> <div className='directory__list'>
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)} {isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
<AccountCard id={accountId} key={accountId} />
))}
</div> </div>
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} /> <LoadMore onClick={this.handleLoadMore} visible={!isLoading} />

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; 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 LoadingIndicator from 'mastodon/components/loading_indicator';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions'; import { fetchSuggestions } from 'mastodon/actions/suggestions';
@ -29,9 +29,9 @@ class Suggestions extends React.PureComponent {
const { isLoading, suggestions } = this.props; const { isLoading, suggestions } = this.props;
return ( return (
<div className='explore__links'> <div className='explore__suggestions'>
{isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => ( {isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
<Account key={suggestion.get('account')} id={suggestion.get('account')} /> <AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
))} ))}
</div> </div>
); );

View File

@ -40,19 +40,11 @@ html {
background: lighten($ui-base-color, 12%); background: lighten($ui-base-color, 12%);
} }
.filter-form, .filter-form {
.directory__card__bar {
background: $white; background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%); 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 { .table-of-contents {
border: 1px solid lighten($ui-base-color, 8%); border: 1px solid lighten($ui-base-color, 8%);
} }
@ -75,8 +67,7 @@ html {
.column-header__back-button, .column-header__back-button,
.column-header__button, .column-header__button,
.column-header__button.active, .column-header__button.active,
.account__header__bar, .account__header__bar {
.directory__card__extra {
background: $white; background: $white;
} }

View File

@ -1220,6 +1220,11 @@ a.sparkline {
background: $ui-base-color; background: $ui-base-color;
border-radius: 4px; border-radius: 4px;
&__permalink {
color: inherit;
text-decoration: none;
}
&__header { &__header {
padding: 4px; padding: 4px;
border-radius: 4px; border-radius: 4px;
@ -1236,20 +1241,22 @@ a.sparkline {
} }
&__title { &__title {
margin-top: -25px; margin-top: -(15px + 8px);
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
&__avatar { &__avatar {
padding: 15px; padding: 14px;
img { img,
.account__avatar {
display: block; display: block;
margin: 0; margin: 0;
width: 56px; width: 56px;
height: 56px; height: 56px;
background: darken($ui-base-color, 8%); background-color: darken($ui-base-color, 8%);
border-radius: 8px; border-radius: 8px;
border: 1px solid $ui-base-color;
} }
} }
@ -1257,30 +1264,34 @@ a.sparkline {
color: $darker-text-color; color: $darker-text-color;
padding-bottom: 15px; padding-bottom: 15px;
font-size: 15px; font-size: 15px;
line-height: 20px;
bdi { bdi {
display: block; display: block;
color: $primary-text-color; color: $primary-text-color;
font-weight: 500; font-weight: 700;
} }
} }
} }
&__bio { &__bio {
padding: 0 15px; padding: 0 15px;
margin: 8px 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-wrap: break-word; word-wrap: break-word;
max-height: 18px * 2; max-height: 21px * 2;
position: relative; position: relative;
font-size: 15px;
line-height: 21px;
&::after { &::after {
display: block; display: block;
content: ""; content: "";
width: 50px; width: 50px;
height: 18px; height: 21px;
position: absolute; position: absolute;
bottom: 0; bottom: 8px;
right: 15px; right: 15px;
background: linear-gradient(to left, $ui-base-color, transparent); background: linear-gradient(to left, $ui-base-color, transparent);
pointer-events: none; pointer-events: none;
@ -1293,10 +1304,6 @@ a.sparkline {
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
.fa {
color: lighten($dark-text-color, 7%);
}
} }
&.mention { &.mention {
@ -1313,12 +1320,21 @@ a.sparkline {
&__actions { &__actions {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding-top: 10px;
&__button { &__button {
flex: 0 0 auto; flex-shrink: 1;
padding: 0 15px; padding: 0 15px;
overflow: hidden;
.button {
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
}
} }
} }
@ -1327,19 +1343,23 @@ a.sparkline {
display: grid; display: grid;
grid-auto-columns: minmax(0, 1fr); grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column; grid-auto-flow: column;
max-width: 340px;
min-width: 65px * 3;
&__item { &__item {
padding: 15px; padding: 15px 0;
text-align: center; text-align: center;
color: $primary-text-color; color: $primary-text-color;
font-weight: 600; font-weight: 600;
font-size: 15px; font-size: 15px;
line-height: 21px;
small { small {
display: block; display: block;
color: $darker-text-color; color: $darker-text-color;
font-weight: 400; font-weight: 400;
font-size: 13px; font-size: 13px;
line-height: 18px;
} }
} }
} }

View File

@ -50,7 +50,7 @@
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font-family: inherit; font-family: inherit;
font-size: 17px; font-size: 15px;
font-weight: 500; font-weight: 500;
letter-spacing: 0; letter-spacing: 0;
line-height: 22px; line-height: 22px;
@ -2333,17 +2333,7 @@ a.account__display-name {
padding: 0; padding: 0;
} }
.directory__list { .account-card {
display: grid;
grid-gap: 10px;
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
@media screen and (max-width: $no-gap-breakpoint) {
display: block;
}
}
.directory__card {
margin-bottom: 0; margin-bottom: 0;
} }
@ -6219,136 +6209,20 @@ a.status-card.compact:hover {
} }
} }
.directory { .scrollable .account-card {
&__list { margin: 10px;
width: 100%; background: lighten($ui-base-color, 8%);
margin: 10px 0; }
transition: opacity 100ms ease-in;
&.loading { .scrollable .account-card__title__avatar {
opacity: 0.7; img,
} .account__avatar {
border-color: lighten($ui-base-color, 8%);
@media screen and (max-width: $no-gap-breakpoint) {
margin: 0;
}
} }
}
&__card { .scrollable .account-card__bio::after {
box-sizing: border-box; background: linear-gradient(to left, lighten($ui-base-color, 8%), transparent);
margin-bottom: 10px;
&__img {
height: 125px;
position: relative;
background: darken($ui-base-color, 12%);
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
margin: 0;
object-fit: cover;
}
}
&__bar {
display: flex;
align-items: center;
background: lighten($ui-base-color, 4%);
padding: 10px;
&__name {
flex: 1 1 auto;
display: flex;
align-items: center;
text-decoration: none;
overflow: hidden;
}
&__relationship {
width: 23px;
min-height: 1px;
flex: 0 0 auto;
}
.avatar {
flex: 0 0 auto;
width: 48px;
height: 48px;
padding-top: 2px;
img {
width: 100%;
height: 100%;
display: block;
margin: 0;
border-radius: 4px;
background: darken($ui-base-color, 8%);
object-fit: cover;
}
}
.display-name {
margin-left: 15px;
text-align: left;
strong {
font-size: 15px;
color: $primary-text-color;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
span {
display: block;
font-size: 14px;
color: $darker-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
&__extra {
background: $ui-base-color;
display: flex;
align-items: center;
justify-content: center;
.accounts-table__count {
width: 33.33%;
flex: 0 0 auto;
padding: 15px 0;
}
.account__header__content {
box-sizing: border-box;
padding: 15px 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
width: 100%;
min-height: 18px + 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
p {
display: none;
&:first-child {
display: inline;
}
}
br {
display: none;
}
}
}
}
} }
.account-gallery__container { .account-gallery__container {
@ -6452,6 +6326,7 @@ a.status-card.compact:hover {
&__column { &__column {
padding: 10px 15px; padding: 10px 15px;
padding-bottom: 0;
} }
.radio-button { .radio-button {

View File

@ -409,14 +409,6 @@
} }
} }
.directory__card {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
}
.page-header { .page-header {
@media screen and (max-width: $no-gap-breakpoint) { @media screen and (max-width: $no-gap-breakpoint) {
border-bottom: 0; border-bottom: 0;
@ -835,19 +827,21 @@
grid-gap: 10px; grid-gap: 10px;
grid-template-columns: minmax(0, 50%) minmax(0, 50%); grid-template-columns: minmax(0, 50%) minmax(0, 50%);
.account-card {
display: flex;
flex-direction: column;
}
@media screen and (max-width: $no-gap-breakpoint) { @media screen and (max-width: $no-gap-breakpoint) {
display: block; display: block;
}
.icon-button { .account-card {
font-size: 18px; margin-bottom: 10px;
display: block;
}
} }
} }
.directory__card {
margin-bottom: 0;
}
.card-grid { .card-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -12,11 +12,6 @@ body.rtl {
margin-left: 10px; margin-left: 10px;
} }
.directory__card__bar .display-name {
margin-left: 0;
margin-right: 15px;
}
.display-name, .display-name,
.announcements__item { .announcements__item {
text-align: right; text-align: right;

View File

@ -19,37 +19,36 @@
- else - else
.directory__list .directory__list
- @accounts.each do |account| - @accounts.each do |account|
.directory__card .account-card
.directory__card__img = link_to TagManager.instance.url_for(account), class: 'account-card__permalink' do
= image_tag account.header.url, alt: '' .account-card__header
.directory__card__bar = image_tag account.header.url, alt: ''
= link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do .account-card__title
.avatar .account-card__title__avatar
= image_tag account.avatar.url, alt: '', class: 'u-photo' = image_tag account.avatar.url, alt: ''
.display-name .display-name
%bdi %bdi
%strong.emojify.p-name= display_name(account, custom_emojify: true) %strong.emojify.p-name= display_name(account, custom_emojify: true)
%span= acct(account) %span
.directory__card__bar__relationship.account__relationship = acct(account)
= minimal_account_action_button(account) = fa_icon('lock') if account.locked?
- if account.note.present?
.directory__card__extra .account-card__bio.emojify
.account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true) = Formatter.instance.simplified_format(account, custom_emojify: true)
- else
.directory__card__extra .flex-spacer
.accounts-table__count .account-card__actions
= friendly_number_to_human account.statuses_count .account-card__counters
%small= t('accounts.posts', count: account.statuses_count).downcase .account-card__counters__item
.accounts-table__count = friendly_number_to_human account.statuses_count
= friendly_number_to_human account.followers_count %small= t('accounts.posts', count: account.statuses_count).downcase
%small= t('accounts.followers', count: account.followers_count).downcase .account-card__counters__item
.accounts-table__count = friendly_number_to_human account.followers_count
- if account.last_status_at.present? %small= t('accounts.followers', count: account.followers_count).downcase
%time.time-ago{ datetime: account.last_status_at.to_date.iso8601, title: l(account.last_status_at.to_date) }= l account.last_status_at.to_date .account-card__counters__item
- else = friendly_number_to_human account.following_count
= t('accounts.never_active') %small= t('accounts.following', count: account.following_count).downcase
.account-card__actions__button
%small= t('accounts.last_active') = account_action_button(account)
= paginate @accounts = paginate @accounts

View File

@ -72,7 +72,6 @@ en:
media: Media media: Media
moved_html: "%{name} has moved to %{new_profile_link}:" moved_html: "%{name} has moved to %{new_profile_link}:"
network_hidden: This information is not available network_hidden: This information is not available
never_active: Never
nothing_here: There is nothing here! nothing_here: There is nothing here!
people_followed_by: People whom %{name} follows people_followed_by: People whom %{name} follows
people_who_follow: People who follow %{name} people_who_follow: People who follow %{name}