kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'profile-directory' into 'develop'
Profile directory See merge request soapbox-pub/soapbox-fe!927strip-front-mentions
commit
a6dc420b70
|
@ -0,0 +1,61 @@
|
||||||
|
import api from '../api';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
|
|
||||||
|
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
|
||||||
|
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
|
||||||
|
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
|
||||||
|
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
|
||||||
|
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const fetchDirectory = params => (dispatch, getState) => {
|
||||||
|
dispatch(fetchDirectoryRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(fetchDirectorySuccess(data));
|
||||||
|
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||||
|
}).catch(error => dispatch(fetchDirectoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchDirectoryRequest = () => ({
|
||||||
|
type: DIRECTORY_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchDirectorySuccess = accounts => ({
|
||||||
|
type: DIRECTORY_FETCH_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchDirectoryFail = error => ({
|
||||||
|
type: DIRECTORY_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandDirectory = params => (dispatch, getState) => {
|
||||||
|
dispatch(expandDirectoryRequest());
|
||||||
|
|
||||||
|
const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(expandDirectorySuccess(data));
|
||||||
|
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||||
|
}).catch(error => dispatch(expandDirectoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandDirectoryRequest = () => ({
|
||||||
|
type: DIRECTORY_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandDirectorySuccess = accounts => ({
|
||||||
|
type: DIRECTORY_EXPAND_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandDirectoryFail = error => ({
|
||||||
|
type: DIRECTORY_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default class RadioButton extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
checked: PropTypes.bool,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
label: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, value, checked, onChange, label } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className='radio-button'>
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
type='radio'
|
||||||
|
value={value}
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={classNames('radio-button__input', { checked })} />
|
||||||
|
|
||||||
|
<span>{label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ const messages = defineMessages({
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
|
profileDirectory: { id: 'column.profile_directory', defaultMessage: 'Profile directory' },
|
||||||
header: { id: 'tabs_bar.header', defaultMessage: 'Account Info' },
|
header: { id: 'tabs_bar.header', defaultMessage: 'Account Info' },
|
||||||
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
||||||
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
|
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
|
||||||
|
@ -253,6 +254,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
||||||
<Icon src={require('@tabler/icons/icons/bookmarks.svg')} />
|
<Icon src={require('@tabler/icons/icons/bookmarks.svg')} />
|
||||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.bookmarks)}</span>
|
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.bookmarks)}</span>
|
||||||
</NavLink>}
|
</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>
|
||||||
|
|
||||||
<div className='sidebar-menu__section'>
|
<div className='sidebar-menu__section'>
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from '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 { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
account: getAccount(state, id),
|
||||||
|
autoPlayGif: getSettings(state).get('autoPlayGif'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
@connect(makeMapStateToProps)
|
||||||
|
class AccountCard extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
autoPlayGif: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
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={`/@${account.get('acct')}`}>
|
||||||
|
<Avatar account={account} size={48} />
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</Permalink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='directory__card__extra'>
|
||||||
|
<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>
|
||||||
|
<div className='accounts-table__count'>{shortNumberFormat(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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import React from 'react';
|
||||||
|
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/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 { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||||
|
recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
|
||||||
|
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
|
||||||
|
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
|
||||||
|
federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
|
||||||
|
isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
|
||||||
|
title: state.getIn(['instance', 'title']),
|
||||||
|
features: getFeatures(state.get('instance')),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Directory extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
accountIds: ImmutablePropTypes.list.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
order: PropTypes.string,
|
||||||
|
local: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
features: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
order: null,
|
||||||
|
local: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
getParams = (props, state) => ({
|
||||||
|
order: state.order === null ? (props.params.order || 'active') : state.order,
|
||||||
|
local: state.local === null ? (props.params.local || false) : state.local,
|
||||||
|
});
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchDirectory(this.getParams(this.props, this.state)));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const paramsOld = this.getParams(prevProps, prevState);
|
||||||
|
const paramsNew = this.getParams(this.props, this.state);
|
||||||
|
|
||||||
|
if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
|
||||||
|
dispatch(fetchDirectory(paramsNew));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangeOrder = e => {
|
||||||
|
this.setState({ order: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangeLocal = e => {
|
||||||
|
this.setState({ local: e.target.value === '1' });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(expandDirectory(this.getParams(this.props, this.state)));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isLoading, accountIds, intl, title, features } = this.props;
|
||||||
|
const { order, local } = this.getParams(this.props, this.state);
|
||||||
|
|
||||||
|
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='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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||||
|
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ const mapStateToProps = state => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account,
|
account,
|
||||||
|
profileDirectory: features.profileDirectory,
|
||||||
federating: features.federating,
|
federating: features.federating,
|
||||||
showAliases: features.accountAliasesAPI,
|
showAliases: features.accountAliasesAPI,
|
||||||
importAPI: features.importAPI,
|
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'>
|
<div className='getting-started__footer'>
|
||||||
<ul>
|
<ul>
|
||||||
{account && <>
|
{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='/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='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></Link></li>
|
||||||
<li><Link to='/filters'><FormattedMessage id='navigation_bar.filters' defaultMessage='Filters' /></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 = {
|
LinkFooter.propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
|
profileDirectory: PropTypes.bool,
|
||||||
federating: PropTypes.bool,
|
federating: PropTypes.bool,
|
||||||
showAliases: PropTypes.bool,
|
showAliases: PropTypes.bool,
|
||||||
importAPI: PropTypes.bool,
|
importAPI: PropTypes.bool,
|
||||||
|
|
|
@ -107,6 +107,7 @@ import {
|
||||||
FederationRestrictions,
|
FederationRestrictions,
|
||||||
Aliases,
|
Aliases,
|
||||||
FollowRecommendations,
|
FollowRecommendations,
|
||||||
|
Directory,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
UploadArea,
|
UploadArea,
|
||||||
NotificationsContainer,
|
NotificationsContainer,
|
||||||
|
@ -277,6 +278,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
|
|
||||||
<WrappedRoute path='/search' publicRoute page={DefaultPage} component={Search} content={children} />
|
<WrappedRoute path='/search' publicRoute page={DefaultPage} component={Search} content={children} />
|
||||||
<WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />
|
<WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />
|
||||||
|
<WrappedRoute path='/directory' publicRoute page={DefaultPage} component={Directory} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/chats' exact page={DefaultPage} component={ChatIndex} content={children} />
|
<WrappedRoute path='/chats' exact page={DefaultPage} component={ChatIndex} content={children} />
|
||||||
<WrappedRoute path='/chats/:chatId' page={DefaultPage} component={ChatRoom} content={children} />
|
<WrappedRoute path='/chats/:chatId' page={DefaultPage} component={ChatRoom} content={children} />
|
||||||
|
|
|
@ -410,6 +410,10 @@ export function FollowRecommendations() {
|
||||||
return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
|
return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Directory() {
|
||||||
|
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
||||||
|
}
|
||||||
|
|
||||||
export function RegisterInvite() {
|
export function RegisterInvite() {
|
||||||
return import(/* webpackChunkName: "features/register_invite" */'../../register_invite');
|
return import(/* webpackChunkName: "features/register_invite" */'../../register_invite');
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,14 @@ import {
|
||||||
MUTES_FETCH_SUCCESS,
|
MUTES_FETCH_SUCCESS,
|
||||||
MUTES_EXPAND_SUCCESS,
|
MUTES_EXPAND_SUCCESS,
|
||||||
} from '../actions/mutes';
|
} from '../actions/mutes';
|
||||||
|
import {
|
||||||
|
DIRECTORY_FETCH_REQUEST,
|
||||||
|
DIRECTORY_FETCH_SUCCESS,
|
||||||
|
DIRECTORY_FETCH_FAIL,
|
||||||
|
DIRECTORY_EXPAND_REQUEST,
|
||||||
|
DIRECTORY_EXPAND_SUCCESS,
|
||||||
|
DIRECTORY_EXPAND_FAIL,
|
||||||
|
} from '../actions/directory';
|
||||||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
import {
|
import {
|
||||||
GROUP_MEMBERS_FETCH_SUCCESS,
|
GROUP_MEMBERS_FETCH_SUCCESS,
|
||||||
|
@ -98,6 +106,16 @@ export default function userLists(state = initialState, action) {
|
||||||
return state.setIn(['mutes', 'items'], ImmutableOrderedSet(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
return state.setIn(['mutes', 'items'], ImmutableOrderedSet(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||||
case MUTES_EXPAND_SUCCESS:
|
case MUTES_EXPAND_SUCCESS:
|
||||||
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||||
|
case DIRECTORY_FETCH_SUCCESS:
|
||||||
|
return state.setIn(['directory', 'items'], ImmutableOrderedSet(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
|
||||||
|
case DIRECTORY_EXPAND_SUCCESS:
|
||||||
|
return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
|
||||||
|
case DIRECTORY_FETCH_REQUEST:
|
||||||
|
case DIRECTORY_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['directory', 'isLoading'], true);
|
||||||
|
case DIRECTORY_FETCH_FAIL:
|
||||||
|
case DIRECTORY_EXPAND_FAIL:
|
||||||
|
return state.setIn(['directory', 'isLoading'], false);
|
||||||
case GROUP_MEMBERS_FETCH_SUCCESS:
|
case GROUP_MEMBERS_FETCH_SUCCESS:
|
||||||
return normalizeList(state, 'groups', action.id, action.accounts, action.next);
|
return normalizeList(state, 'groups', action.id, action.accounts, action.next);
|
||||||
case GROUP_MEMBERS_EXPAND_SUCCESS:
|
case GROUP_MEMBERS_EXPAND_SUCCESS:
|
||||||
|
|
|
@ -66,6 +66,10 @@ export const getFeatures = createSelector([
|
||||||
accountSubscriptions: v.software === PLEROMA && gte(v.version, '1.0.0'),
|
accountSubscriptions: v.software === PLEROMA && gte(v.version, '1.0.0'),
|
||||||
unrestrictedLists: v.software === PLEROMA,
|
unrestrictedLists: v.software === PLEROMA,
|
||||||
accountByUsername: 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,8 @@
|
||||||
@import 'components/profile-stats';
|
@import 'components/profile-stats';
|
||||||
@import 'components/progress-circle';
|
@import 'components/progress-circle';
|
||||||
@import 'components/register-invite';
|
@import 'components/register-invite';
|
||||||
|
@import 'components/radio-button';
|
||||||
|
@import 'components/directory';
|
||||||
|
|
||||||
// Holiday
|
// Holiday
|
||||||
@import 'holiday/halloween';
|
@import 'holiday/halloween';
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
.directory {
|
||||||
|
&__filter-form {
|
||||||
|
display: flex;
|
||||||
|
background: var(--foreground-color);
|
||||||
|
|
||||||
|
&__column {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 10px;
|
||||||
|
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
transition: opacity 100ms ease-in;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 630px) {
|
||||||
|
grid-template-columns: minmax(0, 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__card {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 0;
|
||||||
|
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
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(--brand-color--med);
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--brand-color--med);
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account__avatar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 48px;
|
||||||
|
min-width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
padding-top: 2px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--brand-color--faint);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name {
|
||||||
|
margin-left: 15px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-weight: 400;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__extra {
|
||||||
|
background: var(--foreground-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.accounts-table__count {
|
||||||
|
padding: 15px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 33.33%;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
color: var(--primary-text-color--faint);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
br {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
> .fa {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
font-size: 20px;
|
font-size: 28px;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue