From 24552b5160a5090e7d6056fb69a209aa48fe4fce Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 30 Jul 2019 11:10:46 +0200 Subject: Add whitelist mode (#11291) --- app/controllers/directories_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'app/controllers/directories_controller.rb') diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index 594907674..d2ef76f06 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -3,7 +3,8 @@ class DirectoriesController < ApplicationController layout 'public' - before_action :check_enabled + before_action :authenticate_user!, if: :whitelist_mode? + before_action :require_enabled! before_action :set_instance_presenter before_action :set_tag, only: :show before_action :set_tags @@ -19,7 +20,7 @@ class DirectoriesController < ApplicationController private - def check_enabled + def require_enabled! return not_found unless Setting.profile_directory end -- cgit From ac33f1aedd9a6c72c6c176afb1f5d62a1ce5d44d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 7 Aug 2019 10:01:55 +0200 Subject: Fix account tags not being saved correctly (#11507) * Fix account tags not being saved correctly Regression from f371b32 Fix Tag#discoverable not returning tags where listable is nil instead of true Add notice when saving hashtags in admin UI Change public hashtag and directory pages to return 404 for forbidden tags * Remove unused locale string --- app/controllers/admin/tags_controller.rb | 2 +- app/controllers/directories_controller.rb | 2 +- app/controllers/tags_controller.rb | 2 +- app/models/account.rb | 12 +----------- app/models/tag.rb | 3 ++- config/locales/en.yml | 1 + 6 files changed, 7 insertions(+), 15 deletions(-) (limited to 'app/controllers/directories_controller.rb') diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 0e9dda302..ed271aedc 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -17,7 +17,7 @@ module Admin authorize @tag, :update? if @tag.update(tag_params.merge(reviewed_at: Time.now.utc)) - redirect_to admin_tag_path(@tag.id) + redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg') else render :show end diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index d2ef76f06..a5c47b515 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -25,7 +25,7 @@ class DirectoriesController < ApplicationController end def set_tag - @tag = Tag.discoverable.find_by!(name: params[:id].downcase) + @tag = Tag.discoverable.find_normalized!(params[:id]) end def set_tags diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 3cd2d9e20..5a6fcc8fd 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -47,7 +47,7 @@ class TagsController < ApplicationController private def set_tag - @tag = Tag.find_normalized!(params[:id]) + @tag = Tag.usable.find_normalized!(params[:id]) end def set_body_classes diff --git a/app/models/account.rb b/app/models/account.rb index ccd116d6e..b205c8c9e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -227,17 +227,7 @@ class Account < ApplicationRecord end def tags_as_strings=(tag_names) - tag_names.map! { |name| name.mb_chars.downcase.to_s } - tag_names.uniq! - - # Existing hashtags - hashtags_map = Tag.where(name: tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag } - - # Initialize not yet existing hashtags - tag_names.each do |name| - next if hashtags_map.key?(name) - hashtags_map[name] = Tag.new(name: name) - end + hashtags_map = Tag.find_or_create_by_names(tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag } # Remove hashtags that are to be deleted tags.each do |tag| diff --git a/app/models/tag.rb b/app/models/tag.rb index 6a02581fa..e2fe91da1 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -31,7 +31,8 @@ class Tag < ApplicationRecord scope :reviewed, -> { where.not(reviewed_at: nil) } scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) } - scope :discoverable, -> { where.not(listable: false).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) } + scope :usable, -> { where(usable: [true, nil]) } + scope :discoverable, -> { where(listable: [true, nil]).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } delegate :accounts_count, diff --git a/config/locales/en.yml b/config/locales/en.yml index 333d4f172..20baf634e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -496,6 +496,7 @@ en: title: Hashtags trending_right_now: Trending right now unique_uses_today: "%{count} posting today" + updated_msg: Hashtag settings updated successfully title: Administration warning_presets: add_new: Add new -- cgit From cb447b28c403c7db32e3e3d7c2510004287edfda Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 30 Aug 2019 00:14:36 +0200 Subject: Add profile directory to web UI (#11688) * Add profile directory to web UI * Add a line of bio to the directory --- app/controllers/api/v1/directories_controller.rb | 30 +++ app/controllers/directories_controller.rb | 8 +- app/javascript/mastodon/actions/directory.js | 61 +++++ app/javascript/mastodon/components/radio_button.js | 35 +++ .../features/directory/components/account_card.js | 149 +++++++++++ .../mastodon/features/directory/index.js | 171 +++++++++++++ .../mastodon/features/getting_started/index.js | 4 +- .../containers/column_settings_container.js | 2 +- .../features/ui/components/columns_area.js | 14 +- .../features/ui/components/navigation_panel.js | 2 +- app/javascript/mastodon/features/ui/index.js | 2 + .../mastodon/features/ui/util/async-components.js | 4 + app/javascript/mastodon/reducers/user_lists.js | 18 ++ app/javascript/styles/mastodon/components.scss | 284 +++++++++++++++------ app/lib/activitypub/adapter.rb | 1 + app/models/account.rb | 7 +- app/serializers/activitypub/actor_serializer.rb | 6 +- app/serializers/rest/account_serializer.rb | 2 +- .../activitypub/process_account_service.rb | 1 + app/views/application/_card.html.haml | 2 +- app/views/directories/index.html.haml | 60 +---- app/views/settings/profiles/show.html.haml | 2 +- config/locales/en.yml | 6 - config/locales/simple_form.en.yml | 2 +- config/routes.rb | 1 + 25 files changed, 715 insertions(+), 159 deletions(-) create mode 100644 app/controllers/api/v1/directories_controller.rb create mode 100644 app/javascript/mastodon/actions/directory.js create mode 100644 app/javascript/mastodon/components/radio_button.js create mode 100644 app/javascript/mastodon/features/directory/components/account_card.js create mode 100644 app/javascript/mastodon/features/directory/index.js (limited to 'app/controllers/directories_controller.rb') diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb new file mode 100644 index 000000000..c91543e3a --- /dev/null +++ b/app/controllers/api/v1/directories_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::DirectoriesController < Api::BaseController + before_action :require_enabled! + before_action :set_accounts + + def show + render json: @accounts, each_serializer: REST::AccountSerializer + end + + private + + def require_enabled! + return not_found unless Setting.profile_directory + end + + def set_accounts + @accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT)) + end + + def accounts_scope + Account.discoverable.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' + 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 && !truthy_param?(:local) + end + end +end diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index a5c47b515..7244f02f0 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -7,7 +7,6 @@ class DirectoriesController < ApplicationController before_action :require_enabled! before_action :set_instance_presenter before_action :set_tag, only: :show - before_action :set_tags before_action :set_accounts def index @@ -28,13 +27,10 @@ class DirectoriesController < ApplicationController @tag = Tag.discoverable.find_normalized!(params[:id]) end - def set_tags - @tags = Tag.discoverable.limit(30).reject { |tag| tag.cached_sample_accounts.empty? } - end - def set_accounts - @accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query| + @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(15).tap do |query| query.merge!(Account.tagged_with(@tag.id)) if @tag + query.merge!(Account.not_excluded_by_account(current_account)) if current_account end end diff --git a/app/javascript/mastodon/actions/directory.js b/app/javascript/mastodon/actions/directory.js new file mode 100644 index 000000000..4b2b6dd56 --- /dev/null +++ b/app/javascript/mastodon/actions/directory.js @@ -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, +}); diff --git a/app/javascript/mastodon/components/radio_button.js b/app/javascript/mastodon/components/radio_button.js new file mode 100644 index 000000000..0496fa286 --- /dev/null +++ b/app/javascript/mastodon/components/radio_button.js @@ -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 ( + + ); + } + +} diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js new file mode 100644 index 000000000..a9c9976be --- /dev/null +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -0,0 +1,149 @@ +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 { shortNumberFormat } from 'mastodon/utils/numbers'; +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}' }, +}); + +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, + }; + + 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 } = 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('note').length > 0 && account.get('note') !== '

' &&
} +
+ +
+
{shortNumberFormat(account.get('statuses_count'))}
+
{shortNumberFormat(account.get('followers_count'))}
+
{account.get('last_status_at') === null ? : }
+
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js new file mode 100644 index 000000000..2f91e759b --- /dev/null +++ b/app/javascript/mastodon/features/directory/index.js @@ -0,0 +1,171 @@ +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 { fetchDirectory, expandDirectory } from 'mastodon/actions/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.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), + domain: state.getIn(['meta', 'domain']), +}); + +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, + domain: PropTypes.string.isRequired, + params: PropTypes.shape({ + order: PropTypes.string, + local: PropTypes.bool, + }), + }; + + state = { + order: null, + local: null, + }; + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state))); + } + } + + getParams = (props, state) => ({ + order: state.order === null ? (props.params.order || 'active') : state.order, + local: state.local === null ? (props.params.local || false) : state.local, + }); + + handleMove = dir => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + 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)); + } + } + + 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 }); + } + } + + handleChangeLocal = e => { + const { dispatch, columnId } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1')); + } else { + 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, columnId, multiColumn, domain, shouldUpdateScroll } = this.props; + const { order, local } = 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/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 6a122a750..f6d90580b 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -107,7 +107,7 @@ class GettingStarted extends ImmutablePureComponent { if (profile_directory) { navItems.push( - + ); height += 48; @@ -120,7 +120,7 @@ class GettingStarted extends ImmutablePureComponent { height += 34; } else if (profile_directory) { navItems.push( - + ); height += 48; diff --git a/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js index c5098052c..5914bbeaf 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js @@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { columnId }) => ({ }, onLoad (value) { - return api().get('/api/v2/search', { params: { q: value } }).then(response => { + return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => { return (response.data.hashtags || []).map((tag) => { return { value: tag.name, label: `#${tag.name}` }; }); diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 042e44e43..8a4e89b3d 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -12,7 +12,18 @@ import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; +import { + Compose, + Notifications, + HomeTimeline, + CommunityTimeline, + PublicTimeline, + HashtagTimeline, + DirectTimeline, + FavouritedStatuses, + ListTimeline, + Directory, +} from '../../ui/util/async-components'; import Icon from 'mastodon/components/icon'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; @@ -30,6 +41,7 @@ const componentMap = { 'DIRECT': DirectTimeline, 'FAVOURITES': FavouritedStatuses, 'LIST': ListTimeline, + 'DIRECTORY': Directory, }; const messages = defineMessages({ diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index 64a40a9da..6f07778f2 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -18,6 +18,7 @@ const NavigationPanel = () => ( + {profile_directory && } @@ -25,7 +26,6 @@ const NavigationPanel = () => ( - {!!profile_directory && } {showTrends &&
} {showTrends && } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 9d284c221..49c5c8d0e 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -47,6 +47,7 @@ import { PinnedStatuses, Lists, Search, + Directory, } from './util/async-components'; import { me, forceSingleColumn } from '../../initial_state'; import { previewState as previewMediaState } from './components/media_modal'; @@ -188,6 +189,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 a9b95c7b8..0084c1510 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -141,3 +141,7 @@ export function Tesseract () { export function Audio () { return import(/* webpackChunkName: "features/audio" */'../../audio'); } + +export function Directory () { + return import(/* webpackChunkName: "features/directory" */'../../directory'); +} diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 8db18c5dc..08e94022f 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -20,6 +20,14 @@ import { MUTES_FETCH_SUCCESS, MUTES_EXPAND_SUCCESS, } from '../actions/mutes'; +import { + DIRECTORY_FETCH_REQUEST, + DIRECTORY_FETCH_SUCCESS, + DIRECTORY_FETCH_FAIL, + DIRECTORY_EXPAND_REQUEST, + DIRECTORY_EXPAND_SUCCESS, + DIRECTORY_EXPAND_FAIL, +} from 'mastodon/actions/directory'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; const initialState = ImmutableMap({ @@ -74,6 +82,16 @@ export default function userLists(state = initialState, action) { return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); case MUTES_EXPAND_SUCCESS: 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'], ImmutableList(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); default: return state; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 36466d5c1..1129680f1 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2092,13 +2092,23 @@ a.account__display-name { padding: 0; } - //.column { - // margin-top: 0; + .directory__list { + display: grid; + grid-gap: 10px; + grid-template-columns: minmax(0, 50%) minmax(0, 50%); - // @media screen and (min-width: $no-gap-breakpoint) { - // margin-top: 10px; - // } - //} + @media screen and (max-width: $no-gap-breakpoint) { + display: block; + } + } + + .directory__card { + margin-bottom: 0; + } + + .filter-form { + display: flex; + } .autosuggest-textarea__textarea { font-size: 16px; @@ -4982,59 +4992,6 @@ a.status-card.compact:hover { } /* End Media Gallery */ -/* Status Video Player */ -.status__video-player { - background: $base-overlay-background; - box-sizing: border-box; - cursor: default; /* May not be needed */ - margin-top: 8px; - overflow: hidden; - position: relative; -} - -.status__video-player-video { - height: 100%; - object-fit: cover; - position: relative; - top: 50%; - transform: translateY(-50%); - width: 100%; - z-index: 1; -} - -.status__video-player-expand, -.status__video-player-mute { - color: $primary-text-color; - opacity: 0.8; - position: absolute; - right: 4px; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; -} - -.status__video-player-spoiler { - display: none; - color: $primary-text-color; - left: 4px; - position: absolute; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; - top: 4px; - z-index: 100; - - &.status__video-player-spoiler--visible { - display: block; - } -} - -.status__video-player-expand { - bottom: 4px; - z-index: 100; -} - -.status__video-player-mute { - top: 4px; - z-index: 5; -} - .detailed, .fullscreen { .video-player__volume__current, @@ -5387,28 +5344,130 @@ a.status-card.compact:hover { } } -.media-spoiler-video { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; - margin-top: 8px; - position: relative; - border: 0; - display: block; -} +.directory { + &__list { + width: 100%; + margin: 10px 0; + transition: opacity 100ms ease-in; -.media-spoiler-video-play-icon { - border-radius: 100px; - color: rgba($primary-text-color, 0.8); - font-size: 36px; - left: 50%; - padding: 5px; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); + &.loading { + opacity: 0.7; + } + + @media screen and (max-width: $no-gap-breakpoint) { + margin: 0; + } + } + + &__card { + box-sizing: border-box; + margin-bottom: 10px; + + &__img { + height: 125px; + position: relative; + background: darken($ui-base-color, 12%); + + img { + display: block; + width: 100%; + height: 100%; + margin: 0; + object-fit: cover; + } + } + + &__bar { + display: flex; + align-items: center; + background: lighten($ui-base-color, 4%); + padding: 10px; + + &__name { + flex: 1 1 auto; + display: flex; + align-items: center; + text-decoration: none; + } + + &__relationship { + width: 23px; + min-height: 1px; + flex: 0 0 auto; + } + + .avatar { + flex: 0 0 auto; + width: 48px; + height: 48px; + padding-top: 2px; + + img { + width: 100%; + height: 100%; + display: block; + margin: 0; + border-radius: 4px; + background: darken($ui-base-color, 8%); + object-fit: cover; + } + } + + .display-name { + margin-left: 15px; + text-align: left; + + strong { + font-size: 15px; + color: $primary-text-color; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + } + + span { + display: block; + font-size: 14px; + color: $darker-text-color; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + &__extra { + background: $ui-base-color; + display: flex; + align-items: center; + justify-content: center; + + .accounts-table__count { + width: 33.33%; + flex: 0 0 auto; + padding: 15px 0; + } + + .account__header__content { + box-sizing: border-box; + padding: 15px 10px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + p { + display: none; + + &:first-child { + display: inline; + } + } + } + } + } } -/* End Video Player */ .account-gallery__container { display: flex; @@ -5484,6 +5543,73 @@ a.status-card.compact:hover { } } } + + &.directory__section-headline { + background: darken($ui-base-color, 2%); + border-bottom-color: transparent; + + a, + button { + &.active { + &::before { + display: none; + } + + &::after { + border-color: transparent transparent darken($ui-base-color, 7%); + } + } + } + } +} + +.filter-form { + background: $ui-base-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; + cursor: default; + 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 $ui-primary-color; + 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: lighten($ui-highlight-color, 8%); + background: lighten($ui-highlight-color, 8%); + } + } } ::-webkit-scrollbar-thumb { diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index a1d84de2f..1c58be8c0 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -20,6 +20,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } }, identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' }, blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, + discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' }, }.freeze def self.default_key_transform diff --git a/app/models/account.rb b/app/models/account.rb index 392cc625f..8c9388b95 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -51,7 +51,6 @@ class Account < ApplicationRecord USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i - MIN_FOLLOWERS_DISCOVERY = 10 include AccountAssociations include AccountAvatar @@ -100,11 +99,13 @@ class Account < ApplicationRecord scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) } - scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) } + scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) } scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } - scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } + scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) } scope :popular, -> { order('account_stats.followers_count desc') } scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } + scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } + scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) } delegate :email, :unconfirmed_email, diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 0bd7aed2e..222e17c99 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -6,12 +6,14 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer context :security context_extensions :manually_approves_followers, :featured, :also_known_as, - :moved_to, :property_value, :hashtag, :emoji, :identity_proof + :moved_to, :property_value, :hashtag, :emoji, :identity_proof, + :discoverable attributes :id, :type, :following, :followers, :inbox, :outbox, :featured, :preferred_username, :name, :summary, - :url, :manually_approves_followers + :url, :manually_approves_followers, + :discoverable has_one :public_key, serializer: ActivityPub::PublicKeySerializer diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 272e3eb9c..75b6cf13b 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -5,7 +5,7 @@ class REST::AccountSerializer < ActiveModel::Serializer attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at, :note, :url, :avatar, :avatar_static, :header, :header_static, - :followers_count, :following_count, :statuses_count + :followers_count, :following_count, :statuses_count, :last_status_at has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? has_many :emojis, serializer: REST::CustomEmojiSerializer diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 603e27ed9..cef658e19 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -83,6 +83,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.fields = property_values || {} @account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) } @account.actor_type = actor_type + @account.discoverable = @json['discoverable'] || false end def set_fetchable_attributes! diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index 00254c40c..8719ce484 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -9,7 +9,7 @@ = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' .display-name - %span{id: "default_account_display_name", style: "display:none;"}= account.username + %span{ id: "default_account_display_name", style: "display: none" }= account.username %bdi %strong.emojify.p-name= display_name(account, custom_emojify: true) %span diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml index a8aa68cc4..54b27114c 100644 --- a/app/views/directories/index.html.haml +++ b/app/views/directories/index.html.haml @@ -14,58 +14,10 @@ %h1= t('directories.explore_mastodon', title: site_title) %p= t('directories.explanation') -.grid - .column-0 - - if @accounts.empty? - = nothing_here - - else - .directory - %table.accounts-table - %tbody - - @accounts.each do |account| - %tr - %td= account_link_to account - %td.accounts-table__count.optional - = number_to_human account.statuses_count, strip_insignificant_zeros: true - %small= t('accounts.posts', count: account.statuses_count).downcase - %td.accounts-table__count.optional - = number_to_human account.followers_count, strip_insignificant_zeros: true - %small= t('accounts.followers', count: account.followers_count).downcase - %td.accounts-table__count - - if account.last_status_at.present? - %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at - - else - \- - %small= t('accounts.last_active') +- if @accounts.empty? + = nothing_here +- else + .card-grid + = render partial: 'application/card', collection: @accounts, as: :account - = paginate @accounts - - .column-1 - - if user_signed_in? - .box-widget.notice-widget - - if current_account.discoverable? - - if current_account.followers_count < Account::MIN_FOLLOWERS_DISCOVERY - %p= t('directories.enabled_but_waiting', min_followers: Account::MIN_FOLLOWERS_DISCOVERY) - - else - %p= t('directories.enabled') - - else - %p= t('directories.how_to_enable') - - = link_to settings_profile_path do - = t('settings.edit_profile') - = fa_icon 'chevron-right fw' - - - if @tags.empty? && !user_signed_in? - .nothing-here - - else - - @tags.each do |tag| - .directory__tag{ class: tag.id == @tag&.id ? 'active' : nil } - = link_to explore_hashtag_path(tag) do - %h4 - = fa_icon 'hashtag' - = tag.name - %small= t('directories.people', count: tag.accounts_count) - - .avatar-stack - - tag.cached_sample_accounts.each do |account| - = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar' + = paginate @accounts diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index f8a8fddd3..f042011d6 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -28,7 +28,7 @@ - if Setting.profile_directory .fields-group - = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable_html', min_followers: Account::MIN_FOLLOWERS_DISCOVERY, path: explore_path), recommended: true + = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true %hr.spacer/ diff --git a/config/locales/en.yml b/config/locales/en.yml index ee78e4720..2f601f274 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -630,14 +630,8 @@ en: warning_title: Disseminated content availability directories: directory: Profile directory - enabled: You are currently listed in the directory. - enabled_but_waiting: You have opted-in to be listed in the directory, but you do not have the minimum number of followers (%{min_followers}) to be listed yet. explanation: Discover users based on their interests explore_mastodon: Explore %{title} - how_to_enable: You are not currently opted-in to the directory. You can opt-in below. Use hashtags in your bio text to be listed under specific hashtags! - people: - one: "%{count} person" - other: "%{count} people" domain_blocks: blocked_domains: List of limited and blocked domains description: This is the list of servers that %{instance} limits or reject federation with. diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 2d5ada0a4..2e5982de9 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -16,7 +16,7 @@ en: bot: This account mainly performs automated actions and might not be monitored context: One or multiple contexts where the filter should apply digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence - discoverable_html: The directory lets people find accounts based on interests and activity. Requires at least %{min_followers} followers + discoverable: The profile directory is another way by which your account can reach a wider audience email: You will be sent a confirmation e-mail fields: You can have up to 4 items displayed as a table on your profile header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px diff --git a/config/routes.rb b/config/routes.rb index 9ae24b0cd..92f272ff5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -325,6 +325,7 @@ Rails.application.routes.draw do end resource :domain_blocks, only: [:show, :create, :destroy] + resource :directory, only: [:show] resources :follow_requests, only: [:index] do member do -- cgit From 987190417228bb56041ea824772341f07f4263d6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 30 Aug 2019 07:41:16 +0200 Subject: Change layout of public profile directory to be the same as in web UI (#11705) --- app/controllers/directories_controller.rb | 2 +- app/helpers/statuses_helper.rb | 20 ++++++++++++ .../features/directory/components/account_card.js | 4 +-- app/javascript/styles/mastodon/components.scss | 7 ++++ app/javascript/styles/mastodon/containers.scss | 18 +++++++++++ app/views/directories/index.html.haml | 37 ++++++++++++++++++++-- 6 files changed, 83 insertions(+), 5 deletions(-) (limited to 'app/controllers/directories_controller.rb') diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index 7244f02f0..7da975a23 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -28,7 +28,7 @@ class DirectoriesController < ApplicationController end def set_accounts - @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(15).tap do |query| + @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query| query.merge!(Account.tagged_with(@tag.id)) if @tag query.merge!(Account.not_excluded_by_account(current_account)) if current_account end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index e067380f6..8380b3c42 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -34,6 +34,26 @@ module StatusesHelper end end + def minimal_account_action_button(account) + if user_signed_in? + return if account.id == current_user.account_id + + if current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do + fa_icon('user-times fw') + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + end + def svg_logo content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') end diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index a9c9976be..cb23a02ba 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -119,7 +119,7 @@ class AccountCard extends ImmutablePureComponent { return (
- +
@@ -134,7 +134,7 @@ class AccountCard extends ImmutablePureComponent {
- {account.get('note').length > 0 && account.get('note') !== '

' &&
} +
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1129680f1..d7e90fcaf 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5367,6 +5367,7 @@ a.status-card.compact:hover { height: 125px; position: relative; background: darken($ui-base-color, 12%); + overflow: hidden; img { display: block; @@ -5388,6 +5389,7 @@ a.status-card.compact:hover { display: flex; align-items: center; text-decoration: none; + overflow: hidden; } &__relationship { @@ -5453,6 +5455,7 @@ a.status-card.compact:hover { padding: 15px 10px; border-bottom: 1px solid lighten($ui-base-color, 8%); width: 100%; + min-height: 18px + 30px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -5464,6 +5467,10 @@ a.status-card.compact:hover { display: inline; } } + + br { + display: none; + } } } } diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 2b6794ee2..e769c495b 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -763,6 +763,24 @@ } } + .directory__list { + display: grid; + grid-gap: 10px; + grid-template-columns: minmax(0, 50%) minmax(0, 50%); + + @media screen and (max-width: $no-gap-breakpoint) { + display: block; + } + + .icon-button { + font-size: 18px; + } + } + + .directory__card { + margin-bottom: 0; + } + .card-grid { display: flex; flex-wrap: wrap; diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml index 54b27114c..30daa6bb1 100644 --- a/app/views/directories/index.html.haml +++ b/app/views/directories/index.html.haml @@ -17,7 +17,40 @@ - if @accounts.empty? = nothing_here - else - .card-grid - = render partial: 'application/card', collection: @accounts, as: :account + .directory__list + - @accounts.each do |account| + .directory__card + .directory__card__img + = image_tag account.header.url, alt: '' + .directory__card__bar + = link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do + .avatar + = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' + + .display-name + %span{ id: "default_account_display_name", style: "display: none" }= account.username + %bdi + %strong.emojify.p-name= display_name(account, custom_emojify: true) + %span= acct(account) + .directory__card__bar__relationship.account__relationship + = minimal_account_action_button(account) + + .directory__card__extra + .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true) + + .directory__card__extra + .accounts-table__count + = number_to_human account.statuses_count, strip_insignificant_zeros: true + %small= t('accounts.posts', count: account.statuses_count).downcase + .accounts-table__count + = number_to_human account.followers_count, strip_insignificant_zeros: true + %small= t('accounts.followers', count: account.followers_count).downcase + .accounts-table__count + - if account.last_status_at.present? + %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at + - else + = t('invites.expires_in_prompt') + + %small= t('accounts.last_active') = paginate @accounts -- cgit From de5305a3a5b2c2c3e92f6e92ad8c72742c501e73 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 28 Sep 2019 01:33:27 +0200 Subject: Fix redirecting non-functional accounts on public pages (#11978) Fix #11969 --- app/controllers/accounts_controller.rb | 1 + app/controllers/custom_css_controller.rb | 1 + app/controllers/directories_controller.rb | 2 ++ app/controllers/follower_accounts_controller.rb | 1 + app/controllers/following_accounts_controller.rb | 1 + app/controllers/manifests_controller.rb | 1 + app/controllers/media_controller.rb | 1 + app/controllers/media_proxy_controller.rb | 1 + app/controllers/remote_follow_controller.rb | 2 ++ app/controllers/remote_interaction_controller.rb | 2 ++ app/controllers/statuses_controller.rb | 1 + app/controllers/tags_controller.rb | 2 ++ 12 files changed, 16 insertions(+) (limited to 'app/controllers/directories_controller.rb') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 0f6f5e3a6..1dab5d5f2 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -10,6 +10,7 @@ class AccountsController < ApplicationController before_action :set_body_classes skip_around_action :set_locale, if: -> { request.format == :json } + skip_before_action :require_functional! def show respond_to do |format| diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index e3f67bd14..0a667a6a6 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -2,6 +2,7 @@ class CustomCssController < ApplicationController skip_before_action :store_current_location + skip_before_action :require_functional! before_action :set_cache_headers diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index 7da975a23..750c835dd 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -9,6 +9,8 @@ class DirectoriesController < ApplicationController before_action :set_tag, only: :show before_action :set_accounts + skip_before_action :require_functional! + def index render :index end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 892c51cf4..705ff4122 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -8,6 +8,7 @@ class FollowerAccountsController < ApplicationController before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } + skip_before_action :require_functional! def index respond_to do |format| diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 653d9a486..968de980d 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -8,6 +8,7 @@ class FollowingAccountsController < ApplicationController before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } + skip_before_action :require_functional! def index respond_to do |format| diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb index 491cde745..960510f60 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -2,6 +2,7 @@ class ManifestsController < ApplicationController skip_before_action :store_current_location + skip_before_action :require_functional! def show expires_in 3.minutes, public: true diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 1f693de32..05cf09c28 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -4,6 +4,7 @@ class MediaController < ApplicationController include Authorization skip_before_action :store_current_location + skip_before_action :require_functional! before_action :authenticate_user!, if: :whitelist_mode? before_action :set_media_attachment diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 47544f21c..014b89de1 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -4,6 +4,7 @@ class MediaProxyController < ApplicationController include RoutingHelper skip_before_action :store_current_location + skip_before_action :require_functional! before_action :authenticate_user!, if: :whitelist_mode? diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index ba963a7a0..db1604644 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -7,6 +7,8 @@ class RemoteFollowController < ApplicationController before_action :set_body_classes + skip_before_action :require_functional! + def new @remote_follow = RemoteFollow.new(session_params) end diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb index 15224e853..4073e7ac3 100644 --- a/app/controllers/remote_interaction_controller.rb +++ b/app/controllers/remote_interaction_controller.rb @@ -10,6 +10,8 @@ class RemoteInteractionController < ApplicationController before_action :set_status before_action :set_body_classes + skip_before_action :require_functional! + def new @remote_follow = RemoteFollow.new(session_params) end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 83131f484..57bbeca64 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -19,6 +19,7 @@ class StatusesController < ApplicationController before_action :set_autoplay, only: :embed skip_around_action :set_locale, if: -> { request.format == :json } + skip_before_action :require_functional!, only: [:show, :embed] content_security_policy only: :embed do |p| p.frame_ancestors(false) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 4dfa05264..77d5661b8 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -13,6 +13,8 @@ class TagsController < ApplicationController before_action :set_body_classes before_action :set_instance_presenter + skip_before_action :require_functional! + def show respond_to do |format| format.html do -- cgit