diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c91543e3a..226dd840c 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -19,7 +19,7 @@ class Api::V1::DirectoriesController < Api::BaseController end 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.by_recent_status) if params[:order].blank? || params[:order] == 'active' scope.merge!(Account.order(id: :desc)) if params[:order] == 'new' diff --git a/app/controllers/api/v1/group_directories_controller.rb b/app/controllers/api/v1/group_directories_controller.rb new file mode 100644 index 000000000..5730d70ab --- /dev/null +++ b/app/controllers/api/v1/group_directories_controller.rb @@ -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 diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index f28c5b2af..1ac82c168 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -21,7 +21,7 @@ class DirectoriesController < ApplicationController end 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 end end diff --git a/app/javascript/mastodon/actions/group_directory.js b/app/javascript/mastodon/actions/group_directory.js new file mode 100644 index 000000000..a76da8b5d --- /dev/null +++ b/app/javascript/mastodon/actions/group_directory.js @@ -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, +}); diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index ea93b6c6c..1a4c2fc31 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -34,6 +34,7 @@ const messages = defineMessages({ personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' }, security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, 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' }, }); @@ -102,6 +103,12 @@ class GettingStarted extends ImmutablePureComponent { height += 34 + 48; + navItems.push( + , + ); + + height += 48; + if (profile_directory) { navItems.push( , @@ -115,12 +122,20 @@ class GettingStarted extends ImmutablePureComponent { ); height += 34; - } else if (profile_directory) { + } else { navItems.push( - , + , ); height += 48; + + if (profile_directory) { + navItems.push( + , + ); + + height += 48; + } } if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) { diff --git a/app/javascript/mastodon/features/group_directory/components/account_card.js b/app/javascript/mastodon/features/group_directory/components/account_card.js new file mode 100644 index 000000000..e8a62a950 --- /dev/null +++ b/app/javascript/mastodon/features/group_directory/components/account_card.js @@ -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: ( + @{account.get('acct')} }} + /> + ), + 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 = ( + + ); + } else if (blocking) { + buttons = ( + + ); + } else if (muting) { + buttons = ( + + ); + } else if (!account.get('moved') || following) { + buttons = ( + + ); + } + } + + return ( +
+
+ +
+ +
+ + + + + +
+ {buttons} +
+
+ +
+
+
+ +
+
+ + + + +
+
+ {' '} + + + +
+
+ {account.get('last_status_at') === null ? ( + + ) : ( + + )}{' '} + + + +
+
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/group_directory/index.js b/app/javascript/mastodon/features/group_directory/index.js new file mode 100644 index 000000000..a32c0d6d6 --- /dev/null +++ b/app/javascript/mastodon/features/group_directory/index.js @@ -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 = ( +
+
+
+ + +
+
+ +
+ {accountIds.map(accountId => )} +
+ + +
+ ); + + return ( + + + + {multiColumn && !pinned ? {scrollableArea} : scrollableArea} + + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 7a7f0b841..189e8d598 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -26,6 +26,7 @@ import { FavouritedStatuses, BookmarkedStatuses, ListTimeline, + GroupDirectory, Directory, } from '../../ui/util/async-components'; import Icon from 'mastodon/components/icon'; @@ -49,6 +50,7 @@ const componentMap = { 'FAVOURITES': FavouritedStatuses, 'BOOKMARKS': BookmarkedStatuses, 'LIST': ListTimeline, + 'GROUP_DIRECTORY': GroupDirectory, 'DIRECTORY': Directory, }; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index d54c0cb39..308a1e3e5 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -20,6 +20,7 @@ const NavigationPanel = () => ( + {profile_directory && } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 13227e47c..60c497901 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -52,6 +52,7 @@ import { PinnedStatuses, Lists, Search, + GroupDirectory, Directory, FollowRecommendations, } from './util/async-components'; @@ -168,6 +169,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index f5121af78..9dd0f29e0 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -158,6 +158,10 @@ export function Audio () { return import(/* webpackChunkName: "features/audio" */'../../audio'); } +export function GroupDirectory () { + return import(/* webpackChunkName: "features/group_directory" */'../../group_directory'); +} + export function Directory () { return import(/* webpackChunkName: "features/directory" */'../../directory'); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 61779d5ac..0b576d13c 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -28,6 +28,7 @@ "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.media": "Media", + "account.members": "Members", "account.members_counter": "{count, plural, one {{counter} Follower} other {{counter} Members}}", "account.mention": "Mention @{name}", "account.moved_to": "{name} has moved to:", @@ -73,6 +74,7 @@ "column.community": "Local timeline", "column.direct": "Direct messages", "column.directory": "Browse profiles", + "column.group_directory": "Browse groups", "column.domain_blocks": "Blocked domains", "column.favourites": "Favourites", "column.follow_requests": "Follow requests", @@ -200,12 +202,15 @@ "getting_started.developers": "Developers", "getting_started.directory": "Profile directory", "getting_started.documentation": "Documentation", + "getting_started.group_directory": "Group directory", "getting_started.heading": "Getting started", "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.security": "Account settings", "getting_started.terms": "Terms of service", "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.any": "or {additional}", "hashtag.column_header.tag_mode.none": "without {additional}", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index c705d2bb0..de0d99be8 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -28,6 +28,7 @@ "account.link_verified_on": "このリンクの所有権は{date}に確認されました", "account.locked_info": "このアカウントは承認制アカウントです。相手が承認するまでフォローは完了しません。", "account.media": "メディア", + "account.members": "参加者", "account.members_counter": "{counter} 人の参加者", "account.mention": "@{name}さんに投稿", "account.moved_to": "{name}さんは引っ越しました:", @@ -77,6 +78,7 @@ "column.favourites": "お気に入り", "column.follow_requests": "フォローリクエスト", "column.group": "グループタイムライン", + "column.group_directory": "グループディレクトリ", "column.home": "ホーム", "column.lists": "リスト", "column.mutes": "ミュートしたユーザー", @@ -200,12 +202,15 @@ "getting_started.developers": "開発", "getting_started.directory": "ディレクトリ", "getting_started.documentation": "ドキュメント", + "getting_started.group_directory": "グループディレクトリ", "getting_started.heading": "スタート", "getting_started.invite": "招待", "getting_started.open_source_notice": "Mastodonはオープンソースソフトウェアです。誰でもGitHub ( {github} ) から開発に参加したり、問題を報告したりできます。", "getting_started.security": "アカウント設定", "getting_started.terms": "プライバシーポリシー", "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.any": "か {additional}", "hashtag.column_header.tag_mode.none": "({additional} を除く)", diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 917890ca1..4b49117e3 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -49,6 +49,14 @@ import { MUTES_EXPAND_SUCCESS, MUTES_EXPAND_FAIL, } 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 { DIRECTORY_FETCH_REQUEST, DIRECTORY_FETCH_SUCCESS, @@ -167,6 +175,16 @@ export default function userLists(state = initialState, action) { case MUTES_FETCH_FAIL: case MUTES_EXPAND_FAIL: 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: return normalizeList(state, ['directory'], action.accounts, action.next); case DIRECTORY_EXPAND_SUCCESS: diff --git a/app/models/account.rb b/app/models/account.rb index 7826faad3..3c782ad55 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -107,6 +107,7 @@ class Account < ApplicationRecord scope :recent, -> { reorder(id: :desc) } scope :bots, -> { where(actor_type: %w(Application Service)) } scope :groups, -> { where(actor_type: 'Group') } + scope :without_groups, -> { where.not(actor_type: 'Group') } scope :alphabetic, -> { order(domain: :asc, username: :asc) } scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } diff --git a/config/routes.rb b/config/routes.rb index 9208c933b..555329b26 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -428,7 +428,9 @@ Rails.application.routes.draw do end 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 member do