Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `app/controllers/settings/preferences_controller.rb`:
  Conflicts due to us having more user settings and upstream dropping
  `hide_network` (to replace it with an account attribute, properly migrated).
  Dropped `hide_network` like upstream.
- `app/lib/user_settings_decorator.rb`:
  Conflicts due to us having more user settings and upstream dropping
  `hide_network` (to replace it with an account attribute, properly migrated).
  Dropped `hide_network` like upstream.
- `app/models/status.rb`:
  Conflict because of slight change in how glitch-soc handles the scope to
  filter out local-only posts for anonymous viewers.
  Took upstream's changes and re-applied glitch-soc's change.
- `app/models/user.rb`:
  Conflicts due to us having more user settings and upstream dropping
  `hide_network` (to replace it with an account attribute, properly migrated).
  Dropped `hide_network` like upstream.
- `app/views/directories/index.html.haml`:
  Conflict because upstream redesigned that page while glitch-soc had a minor
  change to support hiding the number of followers.
  Ported glitch-soc's change on top of upstream's redesign.

Additional changes:
- `app/models/account_statuses_filter.rb`:
  See change to `app/models/status.rb`.
master
Claire 1 year ago
commit 1fbd1fa5c4

@ -127,9 +127,18 @@ jobs:
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate VERSION=20180514140000
name: Run migrations up to v2.4.0
- run:
command: ./bin/rails tests:migrations:populate_v2_4
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all remaining migrations
- run:
command: ./bin/rails tests:migrations:check_database
name: Check migration result
test-two-step-migrations:
executor:
@ -150,14 +159,25 @@ jobs:
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate VERSION=20180514140000
name: Run pre-deployment migrations up to v2.4.0
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails tests:migrations:populate_v2_4
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all pre-deployment migrations
evironment:
environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
- run:
command: ./bin/rails tests:migrations:check_database
name: Check migration result
workflows:
version: 2

@ -107,7 +107,7 @@ SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notificatons@example.com
SMTP_FROM_ADDRESS=notifications@example.com
# File storage (optional)

@ -87,7 +87,7 @@ All notable changes to this project will be documented in this file.
- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
- Fix newlines being added to account notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
- Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941))
- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
@ -420,7 +420,7 @@ All notable changes to this project will be documented in this file.
- Fix inefficiency when fetching bookmarks ([akihikodaki](https://github.com/mastodon/mastodon/pull/14674))
- Fix inefficiency when fetching favourites ([akihikodaki](https://github.com/mastodon/mastodon/pull/14673))
- Fix inefficiency when fetching media-only account timeline ([akihikodaki](https://github.com/mastodon/mastodon/pull/14675))
- Fix inefficieny when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
- Fix inefficiency when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
- Fix redundant query when processing batch actions on custom emojis ([niwatori24](https://github.com/mastodon/mastodon/pull/14534))
- Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/mastodon/mastodon/pull/15287))
- Fix performance on instances list in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/15282))
@ -507,7 +507,7 @@ All notable changes to this project will be documented in this file.
- Add blurhash to link previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13984), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14143), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14278), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14126), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14260))
- In web UI, toots cannot be marked as sensitive unless there is media attached
- However, it's possible to do via API or ActivityPub
- Thumnails of link previews of such posts now use blurhash in web UI
- Thumbnails of link previews of such posts now use blurhash in web UI
- The Card entity in REST API has a new `blurhash` attribute
- Add support for `summary` field for media description in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13763))
- Add hints about incomplete remote content to web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14031), [noellabo](https://github.com/mastodon/mastodon/pull/14195))
@ -530,7 +530,7 @@ All notable changes to this project will be documented in this file.
- The `meta` attribute on the Media Attachment entity in REST API can now have a `colors` attribute which in turn contains three hex colors: `background`, `foreground`, and `accent`
- The background color is chosen from the most dominant color around the edges of the thumbnail
- The foreground and accent colors are chosen from the colors that are the most different from the background color using the CIEDE2000 algorithm
- The most satured color of the two is designated as the accent color
- The most saturated color of the two is designated as the accent color
- The one with the highest W3C contrast is designated as the foreground color
- If there are not enough colors in the thumbnail, new ones are generated using a monochrome pattern
- Add a visibility indicator to toots in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14123), [highemerly](https://github.com/mastodon/mastodon/pull/14292))
@ -556,7 +556,7 @@ All notable changes to this project will be documented in this file.
- Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14132), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14373))
- Change contrast of flash messages ([cchoi12](https://github.com/mastodon/mastodon/pull/13892))
- Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13834))
- Change appearence of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
- Change appearance of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
- Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13954))
- Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/mastodon/mastodon/pull/13773), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13772), [mfmfuyu](https://github.com/mastodon/mastodon/pull/14020), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14015))
- Change structure of unavailable content section on about page ([ariasuni](https://github.com/mastodon/mastodon/pull/13930))
@ -578,8 +578,8 @@ All notable changes to this project will be documented in this file.
### Fixed
- Fix `following` param not working when exact match is found in account search ([noellabo](https://github.com/mastodon/mastodon/pull/14394))
- Fix sometimes occuring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
- Fix RSS feeds not being cachable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
- Fix sometimes occurring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
- Fix RSS feeds not being cacheable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
- Fix lack of locking around processing of Announce activities in ActivityPub ([noellabo](https://github.com/mastodon/mastodon/pull/14365))
- Fix boosted toots from blocked account not being retroactively removed from TL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14339))
- Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14061))
@ -706,7 +706,7 @@ All notable changes to this project will be documented in this file.
- Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/mastodon/mastodon/pull/13485), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13490))
- Fix confusing error when failing to add an alias to an unknown account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13480))
- Fix "Email changed" notification sometimes having wrong e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13475))
- Fix varioues issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
- Fix various issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
- Fix API footer link in web UI ([bubblineyuri](https://github.com/mastodon/mastodon/pull/13441))
- Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13445))
- Fix styling of polls in JS-less fallback on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13436))
@ -1496,7 +1496,7 @@ All notable changes to this project will be documented in this file.
- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/mastodon/mastodon/pull/10100), [BenLubar](https://github.com/mastodon/mastodon/pull/10212))
- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/mastodon/mastodon/pull/9059))
- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10339))
- Change web UI to not not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
- Change web UI to not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/mastodon/mastodon/pull/10378))
- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/mastodon/mastodon/pull/9924))
- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/mastodon/mastodon/pull/10289))
@ -1673,7 +1673,7 @@ All notable changes to this project will be documented in this file.
- Limit maximum visibility of local silenced users to unlisted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9583))
- Change API error message for unconfirmed accounts ([noellabo](https://github.com/mastodon/mastodon/pull/9625))
- Change the icon to "reply-all" when it's a reply to other accounts ([mayaeh](https://github.com/mastodon/mastodon/pull/9378))
- Do not ignore federated reports targetting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
- Do not ignore federated reports targeting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
- Upgrade default Ruby version to 2.6.0 ([Gargron](https://github.com/mastodon/mastodon/pull/9688))
- Change e-mail digest frequency ([Gargron](https://github.com/mastodon/mastodon/pull/9689))
- Change Docker images for Tor support in docker-compose.yml ([Sir-Boops](https://github.com/mastodon/mastodon/pull/9438))

@ -62,7 +62,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
return unless page_requested?
@statuses = cache_collection_paginated_by_id(
@account.statuses.permitted_for(@account, signed_request_account),
AccountStatusesFilter.new(@account, signed_request_account).results,
Status,
LIMIT,
params_slice(:max_id, :min_id, :since_id)

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' }
before_action :require_user!
before_action :set_accounts
def index
render json: familiar_followers.accounts, each_serializer: REST::FamiliarFollowersSerializer
end
private
def set_accounts
@accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact
end
def familiar_followers
FamiliarFollowersPresenter.new(@accounts, current_user.account_id)
end
def account_ids
Array(params[:id]).map(&:to_i)
end
end

@ -22,53 +22,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def cached_account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
cache_collection_paginated_by_id(
statuses,
AccountStatusesFilter.new(@account, current_account, params).results,
Status,
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end
def permitted_account_statuses
@account.statuses.permitted_for(@account, current_account)
end
def only_media_scope
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
end
def pinned_scope
@account.pinned_statuses.permitted_for(@account, current_account)
end
def no_replies_scope
Status.without_replies
end
def no_reblogs_scope
Status.without_reblogs
end
def hashtag_scope
tag = Tag.find_normalized(params[:tagged])
if tag
Status.tagged_with(tag.id)
else
Status.none
end
end
def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
end
def insert_pagination_headers

@ -16,13 +16,13 @@ class FollowerAccountsController < ApplicationController
use_pack 'public'
expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
next if @account.hide_collections?
follows
end
format.json do
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
@ -83,7 +83,7 @@ class FollowerAccountsController < ApplicationController
end
def restrict_fields_to
if page_requested? || !@account.user_hides_network?
if page_requested? || !@account.hide_collections?
# Return all fields
else
%i(id type total_items)

@ -16,13 +16,13 @@ class FollowingAccountsController < ApplicationController
use_pack 'public'
expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
next if @account.hide_collections?
follows
end
format.json do
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
@ -83,7 +83,7 @@ class FollowingAccountsController < ApplicationController
end
def restrict_fields_to
if page_requested? || !@account.user_hides_network?
if page_requested? || !@account.hide_collections?
# Return all fields
else
%i(id type total_items)

@ -48,7 +48,6 @@ class Settings::PreferencesController < Settings::BaseController
:setting_system_font_ui,
:setting_system_emoji_font,
:setting_noindex,
:setting_hide_network,
:setting_hide_followers_count,
:setting_aggregate_reblogs,
:setting_show_application,

@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, :hide_collections, fields_attributes: [:name, :value])
end
def set_account

@ -151,7 +151,7 @@ class ScrollableList extends PureComponent {
attachFullscreenListener(this.onFullScreenChange);
// Handle initial scroll posiiton
// Handle initial scroll position
this.handleScroll();
}

@ -43,7 +43,7 @@ export default class MediaContainer extends PureComponent {
handleOpenVideo = (options) => {
const { components } = this.props;
const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props'));
const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
const mediaList = fromJS(media);
document.body.classList.add('with-modals--active');
@ -87,7 +87,7 @@ export default class MediaContainer extends PureComponent {
...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
...(componentName === 'Video' ? {
componetIndex: i,
componentIndex: i,
onOpenVideo: this.handleOpenVideo,
} : {
onOpenMedia: this.handleOpenMedia,

@ -7,31 +7,28 @@ 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 Button from 'mastodon/components/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';
import classNames from 'classnames';
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',
},
follow: { id: 'account.follow', defaultMessage: 'Follow' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
});
const makeMapStateToProps = () => {
@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
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
@ -138,130 +132,92 @@ class AccountCard extends ImmutablePureComponent {
handleMute = () => {
this.props.onMute(this.props.account);
};
}
handleEditProfile = () => {
window.open('/settings/profile', '_blank');
}
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}
/>
);
let actionBtn;
if (me !== account.get('id')) {
if (!account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
} else if (account.getIn(['relationship', 'muting'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
}
} else {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
}
return (
<div className='directory__card'>
<div className='directory__card__img'>
<img
src={
autoPlayGif ? account.get('header') : account.get('header_static')
}
alt=''
/>
</div>
<div className='account-card'>
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
<div className='account-card__header'>
<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={`/@${account.get('acct')}`}
>
<Avatar account={account} size={48} />
<div className='account-card__title'>
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
<DisplayName account={account} />
</Permalink>
<div className='directory__card__bar__relationship account__relationship'>
{buttons}
</div>
</div>
</Permalink>
<div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{account.get('note').length > 0 && (
<div
className='account__header__content translate'
className='account-card__bio translate'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
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 className='account-card__actions'>
<div className='account-card__counters'>
<div className='account-card__counters__item'>
<ShortNumber value={account.get('statuses_count')} />
<small>
<FormattedMessage id='account.posts' defaultMessage='Toots' />
</small>
</div>
<div className='account-card__counters__item'>
<ShortNumber value={account.get('followers_count')} />{' '}
<small>
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
</small>
</div>
<div className='account-card__counters__item'>
<ShortNumber value={account.get('following_count')} />{' '}
<small>
<FormattedMessage
id='account.following'
defaultMessage='Following'
/>
</small>
</div>
</div>
<div className='accounts-table__count'>
<ShortNumber value={account.get('followers_count')} />{' '}
<small>
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
</small>
</div>
<div className='accounts-table__count'>
{account.get('last_status_at') === null ? (
<FormattedMessage
id='account.never_active'
defaultMessage='Never'
/>
) : (
<RelativeTimestamp timestamp={account.get('last_status_at')} />
)}{' '}
<small>
<FormattedMessage
id='account.last_status'
defaultMessage='Last active'
/>
</small>
<div className='account-card__actions__button'>
{actionBtn}
</div>
</div>
</div>

@ -10,9 +10,9 @@ 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 'mastodon/containers/scroll_container';
import LoadingIndicator from 'mastodon/components/loading_indicator';
const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@ -129,7 +129,7 @@ class Directory extends React.PureComponent {
const pinned = !!columnId;
const scrollableArea = (
<div className='scrollable' style={{ background: 'transparent' }}>
<div className='scrollable'>
<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} />
@ -142,8 +142,10 @@ class Directory extends React.PureComponent {
</div>
</div>
<div className={classNames('directory__list', { loading: isLoading })}>
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
<div className='directory__list'>
{isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
<AccountCard id={accountId} key={accountId} />
))}
</div>
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Account from 'mastodon/containers/account_container';
import AccountCard from 'mastodon/features/directory/components/account_card';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
@ -29,9 +29,9 @@ class Suggestions extends React.PureComponent {
const { isLoading, suggestions } = this.props;
return (
<div className='explore__links'>
{isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => (
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
<div className='explore__suggestions'>
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
))}
</div>
);

@ -8,7 +8,7 @@ const messages = defineMessages({
dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' },
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' },
violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },

@ -121,7 +121,7 @@ class Video extends React.PureComponent {
autoPlay: PropTypes.bool,
volume: PropTypes.number,
muted: PropTypes.bool,
componetIndex: PropTypes.number,
componentIndex: PropTypes.number,
};
static defaultProps = {
@ -502,7 +502,7 @@ class Video extends React.PureComponent {
startTime: this.video.currentTime,
autoPlay: !this.state.paused,
defaultVolume: this.state.volume,
componetIndex: this.props.componetIndex,
componentIndex: this.props.componentIndex,
});
}

@ -414,7 +414,7 @@
"report.reasons.other": "It's something else",
"report.reasons.other_description": "The issue does not fit into other categories",
"report.reasons.spam": "It's spam",
"report.reasons.spam_description": "Malicious links, fake engagement, or repetetive replies",
"report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies",
"report.reasons.violation": "It violates server rules",
"report.reasons.violation_description": "You are aware that it breaks specific rules",
"report.rules.subtitle": "Select all that apply",

@ -40,19 +40,11 @@ html {
background: lighten($ui-base-color, 12%);
}
.filter-form,
.directory__card__bar {
.filter-form {
background: $white;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.scrollable .directory__list {
width: calc(100% + 2px);
margin-left: -1px;
margin-right: -1px;
}
.directory__card,
.table-of-contents {
border: 1px solid lighten($ui-base-color, 8%);
}
@ -75,8 +67,7 @@ html {
.column-header__back-button,
.column-header__button,
.column-header__button.active,
.account__header__bar,
.directory__card__extra {
.account__header__bar {
background: $white;
}

@ -1236,6 +1236,11 @@ a.sparkline {
background: $ui-base-color;
border-radius: 4px;
&__permalink {
color: inherit;
text-decoration: none;
}
&__header {
padding: 4px;
border-radius: 4px;
@ -1252,20 +1257,22 @@ a.sparkline {
}
&__title {
margin-top: -25px;
margin-top: -(15px + 8px);
display: flex;
align-items: flex-end;
&__avatar {
padding: 15px;
padding: 14px;
img {
img,
.account__avatar {
display: block;
margin: 0;
width: 56px;
height: 56px;
background: darken($ui-base-color, 8%);
background-color: darken($ui-base-color, 8%);
border-radius: 8px;
border: 1px solid $ui-base-color;
}
}
@ -1273,30 +1280,34 @@ a.sparkline {
color: $darker-text-color;
padding-bottom: 15px;
font-size: 15px;
line-height: 20px;
bdi {
display: block;
color: $primary-text-color;
font-weight: 500;
font-weight: 700;
}
}
}
&__bio {
padding: 0 15px;
margin: 8px 0;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
max-height: 18px * 2;
max-height: 21px * 2;
position: relative;
font-size: 15px;
line-height: 21px;
&::after {
display: block;
content: "";
width: 50px;
height: 18px;
height: 21px;
position: absolute;
bottom: 0;
bottom: 8px;
right: 15px;
background: linear-gradient(to left, $ui-base-color, transparent);
pointer-events: none;
@ -1309,10 +1320,6 @@ a.sparkline {
&:hover {
text-decoration: underline;
.fa {
color: lighten($dark-text-color, 7%);
}
}
&.mention {
@ -1329,12 +1336,21 @@ a.sparkline {
&__actions {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
&__button {
flex: 0 0 auto;
flex-shrink: 1;
padding: 0 15px;
overflow: hidden;
.button {
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
}
}
}
@ -1343,19 +1359,23 @@ a.sparkline {
display: grid;
grid-auto-columns: minmax(0, 1fr);
grid-auto-flow: column;
max-width: 340px;
min-width: 65px * 3;
&__item {
padding: 15px;
padding: 15px 0;
text-align: center;
color: $primary-text-color;
font-weight: 600;
font-size: 15px;
line-height: 21px;
small {
display: block;
color: $darker-text-color;
font-weight: 400;
font-size: 13px;
line-height: 18px;
}
}
}

@ -50,7 +50,7 @@
cursor: pointer;
display: inline-block;
font-family: inherit;
font-size: 17px;
font-size: 15px;
font-weight: 500;
letter-spacing: 0;
line-height: 22px;
@ -2333,17 +2333,7 @@ a.account__display-name {
padding: 0;
}
.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;
}
}
.directory__card {
.account-card {
margin-bottom: 0;
}
@ -4315,7 +4305,7 @@ a.status-card.compact:hover {
}
}
.upload-progess__message {
.upload-progress__message {
flex: 1 1 auto;
}
@ -6219,136 +6209,20 @@ a.status-card.compact:hover {
}
}
.directory {
&__list {
width: 100%;
margin: 10px 0;
transition: opacity 100ms ease-in;
&.loading {
opacity: 0.7;
}
.scrollable .account-card {
margin: 10px;
background: lighten($ui-base-color, 8%);
}
@media screen and (max-width: $no-gap-breakpoint) {
margin: 0;
}
.scrollable .account-card__title__avatar {
img,
.account__avatar {
border-color: lighten($ui-base-color, 8%);
}
}
&__card {
box-sizing: border-box;
margin-bottom: 10px;
&__img {
height: 125px;
position: relative;
background: darken($ui-base-color, 12%);
overflow: hidden;
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;
overflow: hidden;
}
&__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%;
min-height: 18px + 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
p {
display: none;
&:first-child {
display: inline;
}
}
br {
display: none;
}
}
}
}
.scrollable .account-card__bio::after {
background: linear-gradient(to left, lighten($ui-base-color, 8%), transparent);
}
.account-gallery__container {
@ -6452,6 +6326,7 @@ a.status-card.compact:hover {
&__column {
padding: 10px 15px;
padding-bottom: 0;
}
.radio-button {

@ -409,14 +409,6 @@
}
}
.directory__card {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
}
.page-header {
@media screen and (max-width: $no-gap-breakpoint) {
border-bottom: 0;
@ -835,19 +827,21 @@
grid-gap: 10px;
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
.account-card {
display: flex;
flex-direction: column;
}
@media screen and (max-width: $no-gap-breakpoint) {
display: block;
}
.icon-button {
font-size: 18px;
.account-card {
margin-bottom: 10px;
display: block;
}
}
}
.directory__card {
margin-bottom: 0;
}
.card-grid {
display: flex;
flex-wrap: wrap;

@ -69,7 +69,7 @@
display: none;
}
.autossugest-input {
.autosuggest-input {
flex: 1 1 auto;
}

@ -12,11 +12,6 @@ body.rtl {
margin-left: 10px;
}
.directory__card__bar .display-name {
margin-left: 0;
margin-right: 15px;
}
.display-name,
.announcements__item {
text-align: right;

@ -32,7 +32,6 @@ class UserSettingsDecorator
user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui')
user.settings['system_emoji_font'] = system_emoji_font_preference if change?('setting_system_emoji_font')
user.settings['noindex'] = noindex_preference if change?('setting_noindex')
user.settings['hide_followers_count']= hide_followers_count_preference if change?('setting_hide_followers_count')
user.settings['flavour'] = flavour_preference if change?('setting_flavour')
user.settings['skin'] = skin_preference if change?('setting_skin')
user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network')
@ -110,10 +109,6 @@ class UserSettingsDecorator
boolean_cast_setting 'setting_noindex'
end
def hide_followers_count_preference
boolean_cast_setting 'setting_hide_followers_count'
end
def flavour_preference
settings['setting_flavour']
end

@ -351,11 +351,11 @@ class Account < ApplicationRecord
end
def hides_followers?
hide_collections? || user_hides_network?
hide_collections?
end
def hides_following?
hide_collections? || user_hides_network?
hide_collections?
end
def object_type

@ -0,0 +1,134 @@
# frozen_string_literal: true
class AccountStatusesFilter
KEYS = %i(
pinned
tagged
only_media
exclude_replies
exclude_reblogs
).freeze
attr_reader :params, :account, :current_account
def initialize(account, current_account, params = {})
@account = account
@current_account = current_account
@params = params
end
def results
scope = initial_scope
scope.merge!(pinned_scope) if pinned?
scope.merge!(only_media_scope) if only_media?
scope.merge!(no_replies_scope) if exclude_replies?
scope.merge!(no_reblogs_scope) if exclude_reblogs?
scope.merge!(hashtag_scope) if tagged?
scope
end
private
def initial_scope
if suspended?
Status.none
elsif anonymous?
account.statuses.not_local_only.where(visibility: %i(public unlisted))
elsif author?
account.statuses.all # NOTE: #merge! does not work without the #all
elsif blocked?
Status.none
else
filtered_scope
end
end
def filtered_scope
scope = account.statuses.left_outer_joins(:mentions)
scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id]))
scope.merge!(filtered_reblogs_scope) if reblogs_may_occur?
scope
end
def filtered_reblogs_scope
Status.left_outer_joins(:reblog).where(reblog_of_id: nil).or(Status.where.not(reblogs_statuses: { account_id: current_account.excluded_from_timeline_account_ids }))
end
def only_media_scope
Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id])
end
def no_replies_scope
Status.without_replies
end
def no_reblogs_scope
Status.without_reblogs
end
def pinned_scope
account.pinned_statuses.group(Status.arel_table[:id], StatusPin.arel_table[:created_at])
end
def hashtag_scope
tag = Tag.find_normalized(params[:tagged])
if tag
Status.tagged_with(tag.id)
else
Status.none
end
end
def suspended?
account.suspended?
end
def anonymous?
current_account.nil?
end
def author?
current_account.id == account.id
end
def blocked?
account.blocking?(current_account) || (current_account.domain.present? && account.domain_blocking?(current_account.domain))
end
def follower?
current_account.following?(account)
end
def reblogs_may_occur?
!exclude_reblogs? && !only_media? && !tagged?
end
def pinned?
truthy_param?(:pinned)
end
def only_media?
truthy_param?(:only_media)
end
def exclude_replies?
truthy_param?(:exclude_replies)
end
def exclude_reblogs?