Profile directory: styles, cleanup
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
a829f429f7
commit
e3d2b44bdc
9 changed files with 127 additions and 205 deletions
|
@ -41,6 +41,7 @@ const messages = defineMessages({
|
|||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
profileDirectory: { id: 'column.profile_directory', defaultMessage: 'Profile directory' },
|
||||
header: { id: 'tabs_bar.header', defaultMessage: 'Account Info' },
|
||||
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
||||
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
|
||||
|
@ -253,6 +254,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon src={require('@tabler/icons/icons/bookmarks.svg')} />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.bookmarks)}</span>
|
||||
</NavLink>}
|
||||
{features.profileDirectory && <NavLink className='sidebar-menu-item' to='/directory' onClick={this.handleClose}>
|
||||
<Icon id='address-book' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.profileDirectory)}</span>
|
||||
</NavLink>}
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__section'>
|
||||
|
|
|
@ -2,29 +2,17 @@ import React from 'react';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||
|
||||
|
||||
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}' },
|
||||
});
|
||||
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
@ -32,121 +20,45 @@ const makeMapStateToProps = () => {
|
|||
const mapStateToProps = (state, { id }) => ({
|
||||
account: getAccount(state, id),
|
||||
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
||||
me: state.get('me'),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onFollow(account) {
|
||||
dispatch((_, getState) => {
|
||||
const unfollowModal = getSettings(getState()).get('unfollowModal');
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
if (unfollowModal) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||
}));
|
||||
} else {
|
||||
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')));
|
||||
}
|
||||
},
|
||||
|
||||
onMute(account) {
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(initMuteModal(account));
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@connect(makeMapStateToProps, mapDispatchToProps)
|
||||
@connect(makeMapStateToProps)
|
||||
class AccountCard extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
autoPlayGif: PropTypes.bool,
|
||||
me: SoapboxPropTypes.me,
|
||||
};
|
||||
|
||||
handleFollow = () => {
|
||||
this.props.onFollow(this.props.account);
|
||||
}
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
}
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, intl, me, autoPlayGif } = 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} />;
|
||||
}
|
||||
}
|
||||
const { account, autoPlayGif } = this.props;
|
||||
|
||||
return (
|
||||
<div className='directory__card'>
|
||||
<div className='directory__card__action-button'>
|
||||
<ActionButton account={account} small />
|
||||
</div>
|
||||
<div className='directory__card__img'>
|
||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||
</div>
|
||||
|
||||
<div className='directory__card__bar'>
|
||||
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<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>
|
||||
|
||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && (
|
||||
<div className='directory__card__extra'>
|
||||
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
|
||||
<div
|
||||
className={classNames('account__header__content', (account.get('note').length === 0 || account.get('note') === '<p></p>') && 'empty')}
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
|
||||
|
|
|
@ -3,15 +3,13 @@ import { connect } from 'react-redux';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from 'soapbox/components/column';
|
||||
import ColumnHeader from 'soapbox/components/column_header';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import AccountCard from './components/account_card';
|
||||
import RadioButton from 'soapbox/components/radio_button';
|
||||
import classNames from 'classnames';
|
||||
import LoadMore from 'soapbox/components/load_more';
|
||||
import { ScrollContainer } from 'react-router-scroll-4';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -33,18 +31,11 @@ export default @connect(mapStateToProps)
|
|||
@injectIntl
|
||||
class Directory extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
columnId: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
title: PropTypes.string.isRequired,
|
||||
params: PropTypes.shape({
|
||||
order: PropTypes.string,
|
||||
|
@ -63,10 +54,6 @@ class Directory extends React.PureComponent {
|
|||
local: state.local === null ? (props.params.local || false) : state.local,
|
||||
});
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchDirectory(this.getParams(this.props, this.state)));
|
||||
|
@ -82,10 +69,6 @@ class Directory extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleChangeOrder = e => {
|
||||
this.setState({ order: e.target.value });
|
||||
}
|
||||
|
@ -100,20 +83,19 @@ class Directory extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, accountIds, intl, columnId, multiColumn, title, shouldUpdateScroll, features } = this.props;
|
||||
const { isLoading, accountIds, intl, title, features } = this.props;
|
||||
const { order, local } = this.getParams(this.props, this.state);
|
||||
const pinned = !!columnId;
|
||||
|
||||
const scrollableArea = (
|
||||
<div className='scrollable' style={{ background: 'transparent' }}>
|
||||
<div className='filter-form'>
|
||||
<div className='filter-form__column' role='group'>
|
||||
return (
|
||||
<Column icon='address-book-o' heading={intl.formatMessage(messages.title)}>
|
||||
<div className='directory__filter-form'>
|
||||
<div className='directory__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='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
|
||||
</div>
|
||||
|
||||
{features.federating && (
|
||||
<div className='filter-form__column' role='group'>
|
||||
<div className='directory__filter-form__column' role='group'>
|
||||
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: title })} checked={local} onChange={this.handleChangeLocal} />
|
||||
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
|
||||
</div>
|
||||
|
@ -125,22 +107,6 @@ class Directory extends React.PureComponent {
|
|||
</div>
|
||||
|
||||
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='address-book-o'
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ const mapStateToProps = state => {
|
|||
|
||||
return {
|
||||
account,
|
||||
profileDirectory: features.profileDirectory,
|
||||
federating: features.federating,
|
||||
showAliases: features.accountAliasesAPI,
|
||||
importAPI: features.importAPI,
|
||||
|
@ -35,10 +36,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
},
|
||||
});
|
||||
|
||||
const LinkFooter = ({ onOpenHotkeys, account, federating, showAliases, importAPI, onClickLogOut, baseURL }) => (
|
||||
const LinkFooter = ({ onOpenHotkeys, account, profileDirectory, federating, showAliases, importAPI, onClickLogOut, baseURL }) => (
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{account && <>
|
||||
{profileDirectory && <li><Link to='/directory'><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></Link></li>}
|
||||
<li><Link to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></Link></li>
|
||||
<li><Link to='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></Link></li>
|
||||
<li><Link to='/filters'><FormattedMessage id='navigation_bar.filters' defaultMessage='Filters' /></Link></li>
|
||||
|
@ -75,6 +77,7 @@ const LinkFooter = ({ onOpenHotkeys, account, federating, showAliases, importAPI
|
|||
|
||||
LinkFooter.propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
profileDirectory: PropTypes.bool,
|
||||
federating: PropTypes.bool,
|
||||
showAliases: PropTypes.bool,
|
||||
importAPI: PropTypes.bool,
|
||||
|
|
|
@ -66,6 +66,10 @@ export const getFeatures = createSelector([
|
|||
accountSubscriptions: v.software === PLEROMA && gte(v.version, '1.0.0'),
|
||||
unrestrictedLists: v.software === PLEROMA,
|
||||
accountByUsername: v.software === PLEROMA,
|
||||
profileDirectory: any([
|
||||
v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
|
||||
features.includes('profile_directory'),
|
||||
]),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -91,6 +91,7 @@
|
|||
@import 'components/profile-stats';
|
||||
@import 'components/progress-circle';
|
||||
@import 'components/register-invite';
|
||||
@import 'components/radio-button';
|
||||
@import 'components/directory';
|
||||
|
||||
// Holiday
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
.directory {
|
||||
&__filter-form {
|
||||
display: flex;
|
||||
background: var(--foreground-color);
|
||||
|
||||
&__column {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
|
@ -24,11 +37,19 @@
|
|||
border-radius: 10px;
|
||||
background: var(--foreground-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&__action-button {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 78px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
&__img {
|
||||
height: 125px;
|
||||
position: relative;
|
||||
background: var(--foreground-color);
|
||||
background: var(--brand-color--med);
|
||||
|
||||
img {
|
||||
display: block;
|
||||
|
@ -42,7 +63,7 @@
|
|||
&__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--foreground-color);
|
||||
background: var(--brand-color--med);
|
||||
padding: 10px;
|
||||
|
||||
&__name {
|
||||
|
@ -50,17 +71,13 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__relationship {
|
||||
width: 23px;
|
||||
min-height: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
.account__avatar {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
padding-top: 2px;
|
||||
|
||||
|
@ -100,11 +117,12 @@
|
|||
|
||||
&__extra {
|
||||
background: var(--foreground-color);
|
||||
padding: 15px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.accounts-table__count {
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
|
@ -118,55 +136,33 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
background: var(--foreground-color);
|
||||
|
||||
&__column {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 6px 0;
|
||||
line-height: 18px;
|
||||
.account__header__content {
|
||||
box-sizing: border-box;
|
||||
padding: 15px 10px;
|
||||
border-bottom: 1px solid var(--brand-color--med);
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
input[type=radio],
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
&.empty {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&__input {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 1px solid var(--primary-text-color--faint);
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
top: -1px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
p {
|
||||
display: none;
|
||||
|
||||
&.checked {
|
||||
border-color: var(--brand-color);
|
||||
background: var(--brand-color);
|
||||
&:first-child {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
app/styles/components/radio-button.scss
Normal file
35
app/styles/components/radio-button.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
.radio-button {
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 6px 0;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
input[type=radio],
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__input {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 1px solid var(--primary-text-color--faint);
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
top: -1px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
|
||||
&.checked {
|
||||
border-color: var(--brand-color);
|
||||
background: var(--brand-color);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -155,7 +155,7 @@
|
|||
|
||||
> .fa {
|
||||
width: 24px;
|
||||
font-size: 20px;
|
||||
font-size: 28px;
|
||||
margin-right: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue