Add group directory
This commit is contained in:
parent
c6417be067
commit
d9649ad1f7
16 changed files with 580 additions and 5 deletions
|
@ -19,7 +19,7 @@ class Api::V1::DirectoriesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def accounts_scope
|
def accounts_scope
|
||||||
Account.discoverable.tap do |scope|
|
Account.discoverable.without_groups.tap do |scope|
|
||||||
scope.merge!(Account.local) if truthy_param?(:local)
|
scope.merge!(Account.local) if truthy_param?(:local)
|
||||||
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
|
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
|
||||||
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
|
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
|
||||||
|
|
24
app/controllers/api/v1/group_directories_controller.rb
Normal file
24
app/controllers/api/v1/group_directories_controller.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::GroupDirectoriesController < Api::BaseController
|
||||||
|
before_action :set_accounts
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_accounts
|
||||||
|
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||||
|
end
|
||||||
|
|
||||||
|
def accounts_scope
|
||||||
|
Account.discoverable.groups.tap do |scope|
|
||||||
|
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
|
||||||
|
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
|
||||||
|
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
||||||
|
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,7 +21,7 @@ class DirectoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_accounts
|
def set_accounts
|
||||||
@accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
|
@accounts = Account.local.discoverable.without_groups.by_recent_status.page(params[:page]).per(20).tap do |query|
|
||||||
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
query.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
61
app/javascript/mastodon/actions/group_directory.js
Normal file
61
app/javascript/mastodon/actions/group_directory.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import api from '../api';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
|
|
||||||
|
export const GROUP_DIRECTORY_FETCH_REQUEST = 'GROUP_DIRECTORY_FETCH_REQUEST';
|
||||||
|
export const GROUP_DIRECTORY_FETCH_SUCCESS = 'GROUP_DIRECTORY_FETCH_SUCCESS';
|
||||||
|
export const GROUP_DIRECTORY_FETCH_FAIL = 'GROUP_DIRECTORY_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const GROUP_DIRECTORY_EXPAND_REQUEST = 'GROUP_DIRECTORY_EXPAND_REQUEST';
|
||||||
|
export const GROUP_DIRECTORY_EXPAND_SUCCESS = 'GROUP_DIRECTORY_EXPAND_SUCCESS';
|
||||||
|
export const GROUP_DIRECTORY_EXPAND_FAIL = 'GROUP_DIRECTORY_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const fetchGroupDirectory = params => (dispatch, getState) => {
|
||||||
|
dispatch(fetchGroupDirectoryRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/group_directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(fetchGroupDirectorySuccess(data));
|
||||||
|
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||||
|
}).catch(error => dispatch(fetchGroupDirectoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchGroupDirectoryRequest = () => ({
|
||||||
|
type: GROUP_DIRECTORY_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchGroupDirectorySuccess = accounts => ({
|
||||||
|
type: GROUP_DIRECTORY_FETCH_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchGroupDirectoryFail = error => ({
|
||||||
|
type: GROUP_DIRECTORY_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandGroupDirectory = params => (dispatch, getState) => {
|
||||||
|
dispatch(expandGroupDirectoryRequest());
|
||||||
|
|
||||||
|
const loadedItems = getState().getIn(['user_lists', 'group_directory', 'items']).size;
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/group_directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(expandGroupDirectorySuccess(data));
|
||||||
|
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||||
|
}).catch(error => dispatch(expandGroupDirectoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandGroupDirectoryRequest = () => ({
|
||||||
|
type: GROUP_DIRECTORY_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandGroupDirectorySuccess = accounts => ({
|
||||||
|
type: GROUP_DIRECTORY_EXPAND_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandGroupDirectoryFail = error => ({
|
||||||
|
type: GROUP_DIRECTORY_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
|
@ -34,6 +34,7 @@ const messages = defineMessages({
|
||||||
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
|
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
|
||||||
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
|
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
|
||||||
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
|
group_directory: { id: 'getting_started.group_directory', defaultMessage: 'Group directory' },
|
||||||
profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
|
profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -102,6 +103,12 @@ class GettingStarted extends ImmutablePureComponent {
|
||||||
|
|
||||||
height += 34 + 48;
|
height += 34 + 48;
|
||||||
|
|
||||||
|
navItems.push(
|
||||||
|
<ColumnLink key='group_directory' icon='address-book' text={intl.formatMessage(messages.group_directory)} to='/group_directory' />,
|
||||||
|
);
|
||||||
|
|
||||||
|
height += 48;
|
||||||
|
|
||||||
if (profile_directory) {
|
if (profile_directory) {
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
||||||
|
@ -115,12 +122,20 @@ class GettingStarted extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 34;
|
height += 34;
|
||||||
} else if (profile_directory) {
|
} else {
|
||||||
navItems.push(
|
navItems.push(
|
||||||
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
<ColumnLink key='group_directory' icon='address-book' text={intl.formatMessage(messages.group_directory)} to='/group_directory' />,
|
||||||
);
|
);
|
||||||
|
|
||||||
height += 48;
|
height += 48;
|
||||||
|
|
||||||
|
if (profile_directory) {
|
||||||
|
navItems.push(
|
||||||
|
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
|
||||||
|
);
|
||||||
|
|
||||||
|
height += 48;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
|
if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
|
||||||
|
|
|
@ -0,0 +1,286 @@
|
||||||
|
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 { 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 { 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';
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
account: getAccount(state, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
onFollow(account) {
|
||||||
|
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)
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateEmojis() {
|
||||||
|
const node = this.node;
|
||||||
|
|
||||||
|
if (!node || autoPlayGif) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojis = node.querySelectorAll('.custom-emoji');
|
||||||
|
|
||||||
|
for (var i = 0; i < emojis.length; i++) {
|
||||||
|
let emoji = emojis[i];
|
||||||
|
if (emoji.classList.contains('status-emoji')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
emoji.classList.add('status-emoji');
|
||||||
|
|
||||||
|
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
|
||||||
|
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._updateEmojis();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this._updateEmojis();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmojiMouseEnter = ({ target }) => {
|
||||||
|
target.src = target.getAttribute('data-original');
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEmojiMouseLeave = ({ target }) => {
|
||||||
|
target.src = target.getAttribute('data-static');
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFollow = () => {
|
||||||
|
this.props.onFollow(this.props.account);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBlock = () => {
|
||||||
|
this.props.onBlock(this.props.account);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMute = () => {
|
||||||
|
this.props.onMute(this.props.account);
|
||||||
|
};
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.node = c;
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='directory__card'>
|
||||||
|
<div className='directory__card__img'>
|
||||||
|
<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={`/timelines/groups/${account.get('id')}`}
|
||||||
|
>
|
||||||
|
<Avatar account={account} size={48} />
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
<div className='directory__card__bar__relationship account__relationship'>
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='directory__card__extra' ref={this.setRef}>
|
||||||
|
<div
|
||||||
|
className='account__header__content'
|
||||||
|
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>
|
||||||
|
<div className='accounts-table__count'>
|
||||||
|
<ShortNumber value={account.get('followers_count')} />{' '}
|
||||||
|
<small>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.members'
|
||||||
|
defaultMessage='Members'
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
149
app/javascript/mastodon/features/group_directory/index.js
Normal file
149
app/javascript/mastodon/features/group_directory/index.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
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 'mastodon/components/column';
|
||||||
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
|
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodon/actions/columns';
|
||||||
|
import { fetchGroupDirectory, expandGroupDirectory } from 'mastodon/actions/group_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 'react-router-scroll-4';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.group_directory', defaultMessage: 'Browse groups' },
|
||||||
|
recentlyActive: { id: 'group_directory.recently_active', defaultMessage: 'Recently active' },
|
||||||
|
newArrivals: { id: 'group_directory.new_arrivals', defaultMessage: 'New arrivals' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
accountIds: state.getIn(['user_lists', 'group_directory', 'items'], ImmutableList()),
|
||||||
|
isLoading: state.getIn(['user_lists', 'group_directory', 'isLoading'], true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class GroupDirectory extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
accountIds: ImmutablePropTypes.list.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
columnId: PropTypes.string,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
order: PropTypes.string,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
order: null,
|
||||||
|
local: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePin = () => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(removeColumn(columnId));
|
||||||
|
} else {
|
||||||
|
dispatch(addColumn('GROUP_DIRECTORY', this.getParams(this.props, this.state)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getParams = (props, state) => ({
|
||||||
|
order: state.order === null ? (props.params.order || 'active') : state.order,
|
||||||
|
});
|
||||||
|
|
||||||
|
handleMove = dir => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
dispatch(moveColumn(columnId, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchGroupDirectory(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(fetchGroupDirectory(paramsNew));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangeOrder = e => {
|
||||||
|
const { dispatch, columnId } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(changeColumnParams(columnId, ['order'], e.target.value));
|
||||||
|
} else {
|
||||||
|
this.setState({ order: e.target.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(expandGroupDirectory(this.getParams(this.props, this.state)));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, accountIds, intl, columnId, multiColumn } = this.props;
|
||||||
|
const { order } = 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'>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||||
|
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
|
||||||
|
</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='group_directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import {
|
||||||
FavouritedStatuses,
|
FavouritedStatuses,
|
||||||
BookmarkedStatuses,
|
BookmarkedStatuses,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
|
GroupDirectory,
|
||||||
Directory,
|
Directory,
|
||||||
} from '../../ui/util/async-components';
|
} from '../../ui/util/async-components';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
@ -49,6 +50,7 @@ const componentMap = {
|
||||||
'FAVOURITES': FavouritedStatuses,
|
'FAVOURITES': FavouritedStatuses,
|
||||||
'BOOKMARKS': BookmarkedStatuses,
|
'BOOKMARKS': BookmarkedStatuses,
|
||||||
'LIST': ListTimeline,
|
'LIST': ListTimeline,
|
||||||
|
'GROUP_DIRECTORY': GroupDirectory,
|
||||||
'DIRECTORY': Directory,
|
'DIRECTORY': Directory,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ const NavigationPanel = () => (
|
||||||
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
||||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||||
|
<NavLink className='column-link column-link--transparent' to='/group_directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.group_directory' defaultMessage='Group directory' /></NavLink>
|
||||||
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
|
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
|
||||||
|
|
||||||
<ListPanel />
|
<ListPanel />
|
||||||
|
|
|
@ -52,6 +52,7 @@ import {
|
||||||
PinnedStatuses,
|
PinnedStatuses,
|
||||||
Lists,
|
Lists,
|
||||||
Search,
|
Search,
|
||||||
|
GroupDirectory,
|
||||||
Directory,
|
Directory,
|
||||||
FollowRecommendations,
|
FollowRecommendations,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
|
@ -168,6 +169,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||||
|
|
||||||
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
|
<WrappedRoute path='/start' component={FollowRecommendations} content={children} />
|
||||||
<WrappedRoute path='/search' component={Search} content={children} />
|
<WrappedRoute path='/search' component={Search} content={children} />
|
||||||
|
<WrappedRoute path='/group_directory' component={GroupDirectory} content={children} />
|
||||||
<WrappedRoute path='/directory' component={Directory} content={children} />
|
<WrappedRoute path='/directory' component={Directory} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
|
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
|
||||||
|
|
|
@ -158,6 +158,10 @@ export function Audio () {
|
||||||
return import(/* webpackChunkName: "features/audio" */'../../audio');
|
return import(/* webpackChunkName: "features/audio" */'../../audio');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GroupDirectory () {
|
||||||
|
return import(/* webpackChunkName: "features/group_directory" */'../../group_directory');
|
||||||
|
}
|
||||||
|
|
||||||
export function Directory () {
|
export function Directory () {
|
||||||
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"account.link_verified_on": "Ownership of this link was checked on {date}",
|
"account.link_verified_on": "Ownership of this link was checked on {date}",
|
||||||
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
||||||
"account.media": "Media",
|
"account.media": "Media",
|
||||||
|
"account.members": "Members",
|
||||||
"account.members_counter": "{count, plural, one {{counter} Follower} other {{counter} Members}}",
|
"account.members_counter": "{count, plural, one {{counter} Follower} other {{counter} Members}}",
|
||||||
"account.mention": "Mention @{name}",
|
"account.mention": "Mention @{name}",
|
||||||
"account.moved_to": "{name} has moved to:",
|
"account.moved_to": "{name} has moved to:",
|
||||||
|
@ -73,6 +74,7 @@
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
"column.direct": "Direct messages",
|
"column.direct": "Direct messages",
|
||||||
"column.directory": "Browse profiles",
|
"column.directory": "Browse profiles",
|
||||||
|
"column.group_directory": "Browse groups",
|
||||||
"column.domain_blocks": "Blocked domains",
|
"column.domain_blocks": "Blocked domains",
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
|
@ -200,12 +202,15 @@
|
||||||
"getting_started.developers": "Developers",
|
"getting_started.developers": "Developers",
|
||||||
"getting_started.directory": "Profile directory",
|
"getting_started.directory": "Profile directory",
|
||||||
"getting_started.documentation": "Documentation",
|
"getting_started.documentation": "Documentation",
|
||||||
|
"getting_started.group_directory": "Group directory",
|
||||||
"getting_started.heading": "Getting started",
|
"getting_started.heading": "Getting started",
|
||||||
"getting_started.invite": "Invite people",
|
"getting_started.invite": "Invite people",
|
||||||
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
|
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
|
||||||
"getting_started.security": "Account settings",
|
"getting_started.security": "Account settings",
|
||||||
"getting_started.terms": "Terms of service",
|
"getting_started.terms": "Terms of service",
|
||||||
"group.column_settings.media_only": "Media only",
|
"group.column_settings.media_only": "Media only",
|
||||||
|
"group_directory.new_arrivals": "New arrivals",
|
||||||
|
"group_directory.recently_active": "Recently active",
|
||||||
"hashtag.column_header.tag_mode.all": "and {additional}",
|
"hashtag.column_header.tag_mode.all": "and {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "or {additional}",
|
"hashtag.column_header.tag_mode.any": "or {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "without {additional}",
|
"hashtag.column_header.tag_mode.none": "without {additional}",
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"account.link_verified_on": "このリンクの所有権は{date}に確認されました",
|
"account.link_verified_on": "このリンクの所有権は{date}に確認されました",
|
||||||
"account.locked_info": "このアカウントは承認制アカウントです。相手が承認するまでフォローは完了しません。",
|
"account.locked_info": "このアカウントは承認制アカウントです。相手が承認するまでフォローは完了しません。",
|
||||||
"account.media": "メディア",
|
"account.media": "メディア",
|
||||||
|
"account.members": "参加者",
|
||||||
"account.members_counter": "{counter} 人の参加者",
|
"account.members_counter": "{counter} 人の参加者",
|
||||||
"account.mention": "@{name}さんに投稿",
|
"account.mention": "@{name}さんに投稿",
|
||||||
"account.moved_to": "{name}さんは引っ越しました:",
|
"account.moved_to": "{name}さんは引っ越しました:",
|
||||||
|
@ -77,6 +78,7 @@
|
||||||
"column.favourites": "お気に入り",
|
"column.favourites": "お気に入り",
|
||||||
"column.follow_requests": "フォローリクエスト",
|
"column.follow_requests": "フォローリクエスト",
|
||||||
"column.group": "グループタイムライン",
|
"column.group": "グループタイムライン",
|
||||||
|
"column.group_directory": "グループディレクトリ",
|
||||||
"column.home": "ホーム",
|
"column.home": "ホーム",
|
||||||
"column.lists": "リスト",
|
"column.lists": "リスト",
|
||||||
"column.mutes": "ミュートしたユーザー",
|
"column.mutes": "ミュートしたユーザー",
|
||||||
|
@ -200,12 +202,15 @@
|
||||||
"getting_started.developers": "開発",
|
"getting_started.developers": "開発",
|
||||||
"getting_started.directory": "ディレクトリ",
|
"getting_started.directory": "ディレクトリ",
|
||||||
"getting_started.documentation": "ドキュメント",
|
"getting_started.documentation": "ドキュメント",
|
||||||
|
"getting_started.group_directory": "グループディレクトリ",
|
||||||
"getting_started.heading": "スタート",
|
"getting_started.heading": "スタート",
|
||||||
"getting_started.invite": "招待",
|
"getting_started.invite": "招待",
|
||||||
"getting_started.open_source_notice": "Mastodonはオープンソースソフトウェアです。誰でもGitHub ( {github} ) から開発に参加したり、問題を報告したりできます。",
|
"getting_started.open_source_notice": "Mastodonはオープンソースソフトウェアです。誰でもGitHub ( {github} ) から開発に参加したり、問題を報告したりできます。",
|
||||||
"getting_started.security": "アカウント設定",
|
"getting_started.security": "アカウント設定",
|
||||||
"getting_started.terms": "プライバシーポリシー",
|
"getting_started.terms": "プライバシーポリシー",
|
||||||
"group.column_settings.media_only": "メディアのみ表示",
|
"group.column_settings.media_only": "メディアのみ表示",
|
||||||
|
"group_directory.new_arrivals": "新着順",
|
||||||
|
"group_directory.recently_active": "最近の活動順",
|
||||||
"hashtag.column_header.tag_mode.all": "と {additional}",
|
"hashtag.column_header.tag_mode.all": "と {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "か {additional}",
|
"hashtag.column_header.tag_mode.any": "か {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "({additional} を除く)",
|
"hashtag.column_header.tag_mode.none": "({additional} を除く)",
|
||||||
|
|
|
@ -49,6 +49,14 @@ import {
|
||||||
MUTES_EXPAND_SUCCESS,
|
MUTES_EXPAND_SUCCESS,
|
||||||
MUTES_EXPAND_FAIL,
|
MUTES_EXPAND_FAIL,
|
||||||
} from '../actions/mutes';
|
} from '../actions/mutes';
|
||||||
|
import {
|
||||||
|
GROUP_DIRECTORY_FETCH_REQUEST,
|
||||||
|
GROUP_DIRECTORY_FETCH_SUCCESS,
|
||||||
|
GROUP_DIRECTORY_FETCH_FAIL,
|
||||||
|
GROUP_DIRECTORY_EXPAND_REQUEST,
|
||||||
|
GROUP_DIRECTORY_EXPAND_SUCCESS,
|
||||||
|
GROUP_DIRECTORY_EXPAND_FAIL,
|
||||||
|
} from 'mastodon/actions/group_directory';
|
||||||
import {
|
import {
|
||||||
DIRECTORY_FETCH_REQUEST,
|
DIRECTORY_FETCH_REQUEST,
|
||||||
DIRECTORY_FETCH_SUCCESS,
|
DIRECTORY_FETCH_SUCCESS,
|
||||||
|
@ -167,6 +175,16 @@ export default function userLists(state = initialState, action) {
|
||||||
case MUTES_FETCH_FAIL:
|
case MUTES_FETCH_FAIL:
|
||||||
case MUTES_EXPAND_FAIL:
|
case MUTES_EXPAND_FAIL:
|
||||||
return state.setIn(['mutes', 'isLoading'], false);
|
return state.setIn(['mutes', 'isLoading'], false);
|
||||||
|
case GROUP_DIRECTORY_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, ['group_directory'], action.accounts, action.next);
|
||||||
|
case GROUP_DIRECTORY_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, ['group_directory'], action.accounts, action.next);
|
||||||
|
case GROUP_DIRECTORY_FETCH_REQUEST:
|
||||||
|
case GROUP_DIRECTORY_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['group_directory', 'isLoading'], true);
|
||||||
|
case GROUP_DIRECTORY_FETCH_FAIL:
|
||||||
|
case GROUP_DIRECTORY_EXPAND_FAIL:
|
||||||
|
return state.setIn(['group_directory', 'isLoading'], false);
|
||||||
case DIRECTORY_FETCH_SUCCESS:
|
case DIRECTORY_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['directory'], action.accounts, action.next);
|
return normalizeList(state, ['directory'], action.accounts, action.next);
|
||||||
case DIRECTORY_EXPAND_SUCCESS:
|
case DIRECTORY_EXPAND_SUCCESS:
|
||||||
|
|
|
@ -107,6 +107,7 @@ class Account < ApplicationRecord
|
||||||
scope :recent, -> { reorder(id: :desc) }
|
scope :recent, -> { reorder(id: :desc) }
|
||||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||||
scope :groups, -> { where(actor_type: 'Group') }
|
scope :groups, -> { where(actor_type: 'Group') }
|
||||||
|
scope :without_groups, -> { where.not(actor_type: 'Group') }
|
||||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||||
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
||||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
||||||
|
|
|
@ -428,7 +428,9 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :domain_blocks, only: [:show, :create, :destroy]
|
resource :domain_blocks, only: [:show, :create, :destroy]
|
||||||
resource :directory, only: [:show]
|
|
||||||
|
resource :directory, only: [:show]
|
||||||
|
resource :group_directory, only: [:show]
|
||||||
|
|
||||||
resources :follow_requests, only: [:index] do
|
resources :follow_requests, only: [:index] do
|
||||||
member do
|
member do
|
||||||
|
|
Loading…
Reference in a new issue