Merge pull request #1713 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
main
Claire 2022-03-08 22:40:21 +01:00 committed by GitHub
commit 02133866e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1057 additions and 906 deletions

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

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

View File

@ -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,

View File

@ -7,31 +7,28 @@ import { makeGetAccount } from 'flavours/glitch/selectors';
import Avatar from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import Permalink from 'flavours/glitch/components/permalink';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import IconButton from 'flavours/glitch/components/icon_button';
import Button from 'flavours/glitch/components/button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state';
import ShortNumber from 'flavours/glitch/components/short_number';
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
unmuteAccount,
} from 'flavours/glitch/actions/accounts';
import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/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;
let actionBtn;
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}
/>
);
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='directory__card__bar'>
<Permalink
className='directory__card__bar__name'
href={account.get('url')}
to={`/@${account.get('acct')}`}
>
<Avatar account={account} size={48} />
<DisplayName account={account} />
</Permalink>
<div className='directory__card__bar__relationship account__relationship'>
{buttons}
<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>
<div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='account-card__title'>
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
<DisplayName account={account} />
</div>
</Permalink>
{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'>
{account.get('followers_count') < 0 ? '-' : <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'>
{account.get('followers_count') < 0 ? '-' : <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>

View File

@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directo
import { List as ImmutableList } from 'immutable';
import AccountCard from './components/account_card';
import RadioButton from 'flavours/glitch/components/radio_button';
import classNames from 'classnames';
import LoadMore from 'flavours/glitch/components/load_more';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import LoadingIndicator from 'flavours/glitch/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} />

View File

@ -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' },

View File

@ -123,7 +123,7 @@ class Video extends React.PureComponent {
autoPlay: PropTypes.bool,
volume: PropTypes.number,
muted: PropTypes.bool,
componetIndex: PropTypes.number,
componentIndex: PropTypes.number,
};
static defaultProps = {
@ -516,7 +516,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,
});
}

View File

@ -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;
}
}
}

View File

@ -1,133 +1,17 @@
.directory {
&__list {
width: 100%;
margin: 10px 0;
transition: opacity 100ms ease-in;
.scrollable .account-card {
margin: 10px;
background: lighten($ui-base-color, 8%);
}
&.loading {
opacity: 0.7;
}
@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);
}
.filter-form {
@ -135,6 +19,7 @@
&__column {
padding: 10px 15px;
padding-bottom: 0;
}
.radio-button {

View File

@ -41,7 +41,7 @@
cursor: pointer;
display: inline-block;
font-family: inherit;
font-size: 17px;
font-size: 15px;
font-weight: 500;
letter-spacing: 0;
line-height: 22px;

View File

@ -94,17 +94,7 @@
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;
}

View File

@ -411,14 +411,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;
@ -841,19 +833,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;

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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;
let actionBtn;
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}
/>
);
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='directory__card__bar'>
<Permalink
className='directory__card__bar__name'
href={account.get('url')}
to={`/@${account.get('acct')}`}
>
<Avatar account={account} size={48} />
<DisplayName account={account} />
</Permalink>
<div className='directory__card__bar__relationship account__relationship'>
{buttons}
<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>
<div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='account-card__title'>
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
<DisplayName account={account} />
</div>
</Permalink>
{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>

View File

@ -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} />

View File

@ -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>
);

View File

@ -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' },

View File

@ -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,
});
}

View File

@ -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",

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
.scrollable .account-card {
margin: 10px;
background: lighten($ui-base-color, 8%);
}
&.loading {
opacity: 0.7;
}
@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 {

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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?
truthy_param?(:exclude_reblogs)
end
def tagged?
params[:tagged].present?
end
def truthy_param?(key)
ActiveModel::Type::Boolean.new.cast(params[key])
end
end

View File

@ -129,6 +129,6 @@ class Report < ApplicationRecord
def validate_rule_ids
return unless violation?
errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids.size
errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids&.size
end
end

View File

@ -399,28 +399,6 @@ class Status < ApplicationRecord
end
end
def permitted_for(target_account, account)
visibility = [:public, :unlisted]
if account.nil?
where(visibility: visibility).not_local_only
elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
none
elsif account.id == target_account.id # author can see own stuff
all
else
# followers can see followers-only stuff, but also things they are mentioned in.
# non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
visibility.push(:private) if account.following?(target_account)
scope = left_outer_joins(:reblog)
scope.where(visibility: visibility)
.or(scope.where(id: account.mentions.select(:status_id)))
.merge(scope.where(reblog_of_id: nil).or(scope.where.not(reblogs_statuses: { account_id: account.excluded_from_timeline_account_ids })))
end
end
def from_text(text)
return [] if text.blank?

View File

@ -126,7 +126,7 @@ class User < ApplicationRecord
has_many :session_activations, dependent: :destroy
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_followers_count,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
:disable_swiping, :default_content_type, :system_emoji_font,
@ -281,10 +281,6 @@ class User < ApplicationRecord
settings.notification_emails['trending_status']
end
def hides_network?
@hides_network ||= settings.hide_network
end
def aggregates_reblogs?
@aggregates_reblogs ||= settings.aggregate_reblogs
end

View File

@ -42,7 +42,7 @@ class UserPolicy < ApplicationPolicy
end
def promote?
admin? && promoteable?
admin? && promotable?
end
def demote?
@ -51,7 +51,7 @@ class UserPolicy < ApplicationPolicy
private
def promoteable?
def promotable?
record.approved? && (!record.staff? || !record.admin?)
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class FamiliarFollowersPresenter
class Result < ActiveModelSerializers::Model
attributes :id, :accounts
end
def initialize(accounts, current_account_id)
@accounts = accounts
@current_account_id = current_account_id
end
def accounts
map = Follow.includes(account: :account_stat).where(target_account_id: @accounts.map(&:id)).where(account_id: Follow.where(account_id: @current_account_id).joins(:target_account).merge(Account.where(hide_collections: [nil, false])).select(:target_account_id)).group_by(&:target_account_id)
@accounts.map { |account| Result.new(id: account.id, accounts: (account.hide_collections? ? [] : (map[account.id] || [])).map(&:account)) }
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class REST::FamiliarFollowersSerializer < ActiveModel::Serializer
attribute :id
has_many :accounts, serializer: REST::AccountSerializer
def id
object.id.to_s
end
end

View File

@ -10,8 +10,8 @@
= account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: disabled
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: disabled
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: disabled
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: disabled
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: disabled
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'new-password' }, hint: false, disabled: disabled
= f.input :confirm_password, as: :string, placeholder: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' }, hint: false, disabled: disabled
= f.input :website, as: :url, placeholder: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' }, hint: false, disabled: disabled

View File

@ -19,37 +19,36 @@
- else
.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: '', class: 'u-photo'
.account-card
= link_to TagManager.instance.url_for(account), class: 'account-card__permalink' do
.account-card__header
= image_tag account.header.url, alt: ''
.account-card__title
.account-card__title__avatar
= image_tag account.avatar.url, alt: ''
.display-name
%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
= friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase
.accounts-table__count
= hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
%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.to_date.iso8601, title: l(account.last_status_at.to_date) }= l account.last_status_at.to_date
- else
= t('accounts.never_active')
%small= t('accounts.last_active')
%span
= acct(account)
= fa_icon('lock') if account.locked?
- if account.note.present?
.account-card__bio.emojify
= Formatter.instance.simplified_format(account, custom_emojify: true)
- else
.flex-spacer
.account-card__actions
.account-card__counters
.account-card__counters__item
= friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase
.account-card__counters__item
= hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
%small= t('accounts.followers', count: account.followers_count).downcase
.account-card__counters__item
= friendly_number_to_human account.following_count
%small= t('accounts.following', count: account.following_count).downcase
.account-card__actions__button
= account_action_button(account)
= paginate @accounts

View File

@ -7,7 +7,7 @@
= render 'accounts/header', account: @account
- if @account.user_hides_network?
- if @account.hide_collections?
.nothing-here= t('accounts.network_hidden')
- elsif user_signed_in? && @account.blocking?(current_account)
.nothing-here= t('accounts.unavailable')

View File

@ -7,7 +7,7 @@
= render 'accounts/header', account: @account
- if @account.user_hides_network?
- if @account.hide_collections?
.nothing-here= t('accounts.network_hidden')
- elsif user_signed_in? && @account.blocking?(current_account)
.nothing-here= t('accounts.unavailable')

View File

@ -10,9 +10,6 @@
.fields-group
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_hide_network, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true

View File

@ -30,7 +30,10 @@
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')
.fields-group
= f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t(Setting.profile_directory ? 'simple_form.hints.defaults.discoverable' : 'simple_form.hints.defaults.discoverable_no_directory'), recommended: true
= f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true
.fields-group
= f.input :hide_collections, as: :boolean, wrapper: :with_label, label: t('simple_form.labels.defaults.setting_hide_network'), hint: t('simple_form.hints.defaults.setting_hide_network')
%hr.spacer/

View File

@ -1,5 +1,5 @@
Rails.application.config.middleware.use OmniAuth::Builder do
# Vanilla omniauth stategies
# Vanilla omniauth strategies
end
Devise.setup do |config|

View File

@ -72,7 +72,6 @@ en:
media: Media
moved_html: "%{name} has moved to %{new_profile_link}:"
network_hidden: This information is not available
never_active: Never
nothing_here: There is nothing here!
people_followed_by: People whom %{name} follows
people_who_follow: People who follow %{name}

View File

@ -986,7 +986,7 @@ en_GB:
enabled: Two-factor authentication is enabled
enabled_success: Two-factor authentication successfully enabled
generate_recovery_codes: Generate recovery codes
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
recovery_codes: Backup recovery codes

View File

@ -37,8 +37,7 @@ en:
current_password: For security purposes please enter the password of the current account
current_username: To confirm, please enter the username of the current account
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
discoverable: Allow your account to be discovered by strangers through recommendations, profile directory and other features
discoverable_no_directory: Allow your account to be discovered by strangers through recommendations and other features
discoverable: Allow your account to be discovered by strangers through recommendations, trends and other features
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

View File

@ -498,6 +498,7 @@ Rails.application.routes.draw do
resource :search, only: :show, controller: :search
resource :lookup, only: :show, controller: :lookup
resources :relationships, only: :index
resources :familiar_followers, only: :index
end
resources :accounts, only: [:create, :show] do

View File

@ -17,7 +17,6 @@ defaults: &defaults
min_invite_role: 'admin'
show_staff_badge: true
default_sensitive: false
hide_network: false
unfollow_modal: false
boost_modal: false
favourite_modal: false

View File

@ -16,7 +16,7 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
# is once again set to the reblogging status' ID, and the value
# is set to the reblogged status' ID). This is safe for Redis'
# float conversion because in this reblog tracking zset, we only
# need the rebloggging status' ID to be able to stop tracking
# need the reblogging status' ID to be able to stop tracking
# entries after they have gotten too far down the feed, which
# does not require an exact value.

View File

@ -22,13 +22,13 @@ class RejectFollowingBlockedUsers < ActiveRecord::Migration[5.2]
follows.each do |follow|
blocked_account = follow.account
followed_acccount = follow.target_account
followed_account = follow.target_account
next follow.destroy! if blocked_account.local?
reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_acccount))
reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_account))
ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_acccount, blocked_account.inbox_url)
ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_account, blocked_account.inbox_url)
follow.destroy!
end

View File

@ -0,0 +1,37 @@
class MigrateHideNetworkPreference < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
# Dummy classes, to make migration possible across version changes
class Account < ApplicationRecord
has_one :user, inverse_of: :account
scope :local, -> { where(domain: nil) }
end
class User < ApplicationRecord
belongs_to :account
end
def up
Account.reset_column_information
Setting.unscoped.where(thing_type: 'User', var: 'hide_network').find_each do |setting|
account = User.find(setting.thing_id).account
ApplicationRecord.transaction do
account.update(hide_collections: setting.value)
setting.delete
end
rescue ActiveRecord::RecordNotFound
next
end
end
def down
Account.local.where(hide_collections: true).includes(:user).find_each do |account|
ApplicationRecord.transaction do
Setting.create(thing_type: 'User', thing_id: account.user.id, var: 'hide_network', value: account.hide_collections?)
account.update(hide_collections: nil)
end
end
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_02_27_041951) do
ActiveRecord::Schema.define(version: 2022_03_04_195405) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

View File

@ -41,7 +41,7 @@ module Mastodon
Gem::Package::TarReader.new(Zlib::GzipReader.open(path)) do |tar|
tar.each do |entry|
next unless entry.file? && entry.full_name.end_with?('.png')
next unless entry.file? && entry.full_name.end_with?('.png', '.gif')
filename = File.basename(entry.full_name, '.*')

View File

@ -510,7 +510,7 @@ module Mastodon
accounts = accounts.sort_by(&:id).reverse
@prompt.warn "Multiple local accounts were found for username '#{accounts.first.username}'."
@prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functionnal.'
@prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.'
accounts.each_with_index do |account, idx|
@prompt.say '%2d. %s: created at: %s; updated at: %s; last logged in at: %s; statuses: %5d; last status at: %s' % [idx, account.username, account.created_at, account.updated_at, account.user&.last_sign_in_at&.to_s || 'N/A', account.account_stat&.statuses_count || 0, account.account_stat&.last_status_at || 'N/A']

View File

@ -156,7 +156,7 @@ module Mastodon
ActiveRecord::Base.connection.add_index(:statuses, :conversation_id, name: :index_statuses_conversation_id, algorithm: :concurrently, if_not_exists: true)
say('Extract the deletion target from coversations... This might take a while...')
say('Extract the deletion target from conversations... This might take a while...')
ActiveRecord::Base.connection.create_table('conversations_to_be_deleted', force: true)

View File

@ -2,6 +2,50 @@
namespace :tests do
namespace :migrations do
desc 'Check that database state is consistent with a successful migration from populated data'
task check_database: :environment do
unless Account.find_by(username: 'admin', domain: nil)&.hide_collections? == false
puts 'Unexpected value for Account#hide_collections? for user @admin'
exit(1)
end
unless Account.find_by(username: 'user', domain: nil)&.hide_collections? == true
puts 'Unexpected value for Account#hide_collections? for user @user'
exit(1)
end
unless Account.find_by(username: 'evil', domain: 'activitypub.com')&.suspended?
puts 'Unexpected value for Account#suspended? for user @evil@activitypub.com'
exit(1)
end
unless Status.find(6).account_id == Status.find(7).account_id
puts 'Users @remote@remote.com and @Remote@remote.com not properly merged'
exit(1)
end
if Account.where(domain: Rails.configuration.x.local_domain).exists?
puts 'Faux remote accounts not properly claned up'
exit(1)
end
unless AccountConversation.first&.last_status_id == 11
puts 'AccountConversation records not created as expected'
exit(1)
end
end
desc 'Populate the database with test data for 2.4.0'
task populate_v2_4: :environment do
ActiveRecord::Base.connection.execute(<<~SQL)
INSERT INTO "settings"
(id, thing_type, thing_id, var, value, created_at, updated_at)
VALUES
(1, 'User', 1, 'hide_network', E'--- false\n', now(), now()),
(2, 'User', 2, 'hide_network', E'--- true\n', now(), now());
SQL
end
desc 'Populate the database with test data for 2.0.0'
task populate_v2: :environment do
admin_key = OpenSSL::PKey::RSA.new(2048)
@ -34,7 +78,7 @@ namespace :tests do
'https://remote.com/@remote', 'https://remote.com/salmon/1'),
(4, 'Remote', 'remote.com', NULL, #{remote_public_key}, now(), now(),
'https://remote.com/@Remote', 'https://remote.com/salmon/1'),
(5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now(), now(),
(5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now() - interval '1 year', now() - interval '1 year',
'https://remote.com/stale/@REMOTE', 'https://remote.com/stale/salmon/1');
INSERT INTO "accounts"
@ -49,6 +93,13 @@ namespace :tests do
(7, 'user', #{local_domain}, #{user_private_key}, #{user_public_key}, now(), now()),
(8, 'pt_user', NULL, #{user_private_key}, #{user_public_key}, now(), now());
INSERT INTO "accounts"
(id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url, suspended)
VALUES
(9, 'evil', 'activitypub.com', NULL, #{remote_public_key_ap}, now(), now(),
1, 'https://activitypub.com/users/evil/inbox', 'https://activitypub.com/users/evil/outbox',
'https://activitypub.com/users/evil/followers', true);
-- users
INSERT INTO "users"
@ -62,6 +113,9 @@ namespace :tests do
VALUES
(3, 7, 'ptuser@localhost', now(), now(), false, 'pt');
-- conversations
INSERT INTO "conversations" (id, created_at, updated_at) VALUES (1, now(), now());
-- statuses
INSERT INTO "statuses"
@ -97,14 +151,22 @@ namespace :tests do
VALUES
(9, 1, 2, now(), now());
INSERT INTO "statuses"
(id, account_id, text, in_reply_to_id, conversation_id, visibility, created_at, updated_at)
VALUES
(10, 2, '@admin hey!', NULL, 1, 3, now(), now()),
(11, 1, '@user hey!', 10, 1, 3, now(), now());
-- mentions (from previous statuses)
INSERT INTO "mentions"
(status_id, account_id, created_at, updated_at)
(id, status_id, account_id, created_at, updated_at)
VALUES
(2, 3, now(), now()),
(3, 4, now(), now()),
(4, 5, now(), now());
(1, 2, 3, now(), now()),
(2, 3, 4, now(), now()),
(3, 4, 5, now(), now()),
(4, 10, 1, now(), now()),
(5, 11, 2, now(), now());
-- stream entries
@ -121,7 +183,6 @@ namespace :tests do
(8, 5, 'status', now(), now()),
(9, 1, 'status', now(), now());
-- custom emoji
INSERT INTO "custom_emojis"
@ -161,12 +222,12 @@ namespace :tests do
-- follows
INSERT INTO "follows"
(account_id, target_account_id, created_at, updated_at)
(id, account_id, target_account_id, created_at, updated_at)
VALUES
(1, 5, now(), now()),
(6, 2, now(), now()),
(5, 2, now(), now()),
(6, 1, now(), now());
(1, 1, 5, now(), now()),
(2, 6, 2, now(), now()),
(3, 5, 2, now(), now()),
(4, 6, 1, now(), now());
-- follow requests
@ -175,6 +236,15 @@ namespace :tests do
VALUES
(2, 5, now(), now()),
(5, 1, now(), now());
-- notifications
INSERT INTO "notifications"
(id, from_account_id, account_id, activity_type, activity_id, created_at, updated_at)
VALUES
(1, 6, 2, 'Follow', 2, now(), now()),
(2, 2, 1, 'Mention', 4, now(), now()),
(3, 1, 2, 'Mention', 5, now(), now());
SQL
end
end

View File

@ -5,7 +5,7 @@ RSpec.describe AccountsController, type: :controller do
let(:account) { Fabricate(:account) }
shared_examples 'cachable response' do
shared_examples 'cacheable response' do
it 'does not set cookies' do
expect(response.cookies).to be_empty
expect(response.headers['Set-Cookies']).to be nil
@ -374,7 +374,7 @@ RSpec.describe AccountsController, type: :controller do
expect(response.media_type).to eq 'application/activity+json'
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'renders account' do
json = body_as_json
@ -432,7 +432,7 @@ RSpec.describe AccountsController, type: :controller do
expect(response.media_type).to eq 'application/activity+json'
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'renders account' do
json = body_as_json
@ -499,7 +499,7 @@ RSpec.describe AccountsController, type: :controller do
expect(response).to have_http_status(200)
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
end
context do

View File

@ -7,7 +7,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
let(:remote_account) { nil }
shared_examples 'cachable response' do
shared_examples 'cacheable response' do
it 'does not set cookies' do
expect(response.cookies).to be_empty
expect(response.headers['Set-Cookies']).to be nil
@ -48,7 +48,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
expect(response.media_type).to eq 'application/activity+json'
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'returns orderedItems with pinned statuses' do
expect(body[:orderedItems]).to be_an Array
@ -101,7 +101,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
expect(response.media_type).to eq 'application/activity+json'
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'returns orderedItems with pinned statuses' do
json = body_as_json

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe ActivityPub::OutboxesController, type: :controller do
let!(:account) { Fabricate(:account) }
shared_examples 'cachable response' do
shared_examples 'cacheable response' do
it 'does not set cookies' do
expect(response.cookies).to be_empty
expect(response.headers['Set-Cookies']).to be nil
@ -53,7 +53,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
expect(body[:totalItems]).to eq 4
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'does not have a Vary header' do
expect(response.headers['Vary']).to be_nil
@ -98,7 +98,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
expect(body[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'returns Vary header with Signature' do
expect(response.headers['Vary']).to include 'Signature'

View File

@ -8,7 +8,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }
let(:remote_querier) { nil }
shared_examples 'cachable response' do
shared_examples 'cacheable response' do
it 'does not set cookies' do
expect(response.cookies).to be_empty
expect(response.headers['Set-Cookies']).to be nil
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
expect(response.media_type).to eq 'application/activity+json'
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
context 'without only_other_accounts' do
it "returns items with thread author's replies" do

View File

@ -31,7 +31,7 @@ describe Api::V1::Accounts::NotesController do
end
end
context 'when account note exceends allowed length' do
context 'when account note exceeds allowed length' do
let(:comment) { 'a' * 2_001 }
it 'returns 422' do

View File

@ -140,7 +140,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
expect(response).to have_http_status(200)
end
it 'unsensitives account' do
it 'unsensitizes account' do
expect(account.reload.sensitized?).to be false
end
end

View File

@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
Fabricate(:favourite, status: status)
end
it 'returns http unautharized' do
it 'returns http unauthorized' do
get :index, params: { status_id: status.id }
expect(response).to have_http_status(404)
end

View File

@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
Fabricate(:status, reblog_of_id: status.id)
end
it 'returns http unautharized' do
it 'returns http unauthorized' do
get :index, params: { status_id: status.id }
expect(response).to have_http_status(404)
end

View File

@ -130,7 +130,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
describe 'GET #show' do
it 'returns http unautharized' do
it 'returns http unauthorized' do
get :show, params: { id: status.id }
expect(response).to have_http_status(404)
end
@ -141,7 +141,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
Fabricate(:status, account: user.account, thread: status)
end
it 'returns http unautharized' do
it 'returns http unauthorized' do
get :context, params: { id: status.id }
expect(response).to have_http_status(404)
end

View File

@ -191,30 +191,30 @@ describe ApplicationController, type: :controller do
controller do
before_action :require_admin!
def sucesss
def success
head 200
end
end
before do
routes.draw { get 'sucesss' => 'anonymous#sucesss' }
routes.draw { get 'success' => 'anonymous#success' }
end
it 'returns a 403 if current user is not admin' do
sign_in(Fabricate(:user, admin: false))
get 'sucesss'
get 'success'
expect(response).to have_http_status(403)
end
it 'returns a 403 if current user is only a moderator' do
sign_in(Fabricate(:user, moderator: true))
get 'sucesss'
get 'success'
expect(response).to have_http_status(403)
end
it 'does nothing if current user is admin' do
sign_in(Fabricate(:user, admin: true))
get 'sucesss'
get 'success'
expect(response).to have_http_status(200)
end
end
@ -223,30 +223,30 @@ describe ApplicationController, type: :controller do
controller do
before_action :require_staff!
def sucesss
def success
head 200
end
end
before do
routes.draw { get 'sucesss' => 'anonymous#sucesss' }
routes.draw { get 'success' => 'anonymous#success' }
end
it 'returns a 403 if current user is not admin or moderator' do
sign_in(Fabricate(:user, admin: false, moderator: false))
get 'sucesss'
get 'success'
expect(response).to have_http_status(403)
end
it 'does nothing if current user is moderator' do
sign_in(Fabricate(:user, moderator: true))
get 'sucesss'
get 'success'
expect(response).to have_http_status(200)
end
it 'does nothing if current user is admin' do
sign_in(Fabricate(:user, admin: true))
get 'sucesss'
get 'success'
expect(response).to have_http_status(200)
end
end

View File

@ -103,7 +103,7 @@ describe FollowerAccountsController do
context 'when account hides their network' do
before do
alice.user.settings.hide_network = true
alice.update(hide_collections: true)
end
it 'returns followers count' do

View File

@ -103,7 +103,7 @@ describe FollowingAccountsController do
context 'when account hides their network' do
before do
alice.user.settings.hide_network = true
alice.update(hide_collections: true)
end
it 'returns followers count' do

View File

@ -5,7 +5,7 @@ require 'rails_helper'
describe StatusesController do
render_views
shared_examples 'cachable response' do
shared_examples 'cacheable response' do
it 'does not set cookies' do
expect(response.cookies).to be_empty
expect(response.headers['Set-Cookies']).to be nil
@ -108,7 +108,7 @@ describe StatusesController do
expect(response.headers['Vary']).to eq 'Accept'
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'returns Content-Type header' do
expect(response.headers['Content-Type']).to include 'application/activity+json'
@ -496,7 +496,7 @@ describe StatusesController do
expect(response.headers['Vary']).to eq 'Accept'
end
it_behaves_like 'cachable response'
it_behaves_like 'cacheable response'
it 'returns Content-Type header' do
expect(response.headers['Content-Type']).to include 'application/activity+json'

View File

@ -60,7 +60,7 @@ describe ApplicationHelper do
end
describe 'favicon_path' do
it 'returns /favicon.ico on production enviromnent' do
it 'returns /favicon.ico on production environment' do
expect(Rails.env).to receive(:production?).and_return(true)
expect(helper.favicon_path).to eq '/favicon.ico'
end

View File

@ -6,7 +6,7 @@ RSpec.describe TagManager do
around do |example|
original_local_domain = Rails.configuration.x.local_domain
Rails.configuration.x.local_domain = 'domain.test'
Rails.configuration.x.local_domain = 'domain.example.com'
example.run
@ -18,11 +18,11 @@ RSpec.describe TagManager do
end
it 'returns true if the slash-stripped string equals to local domain' do
expect(TagManager.instance.local_domain?('DoMaIn.Test/')).to eq true
expect(TagManager.instance.local_domain?('DoMaIn.Example.com/')).to eq true
end
it 'returns false for irrelevant string' do
expect(TagManager.instance.local_domain?('DoMaIn.Test!')).to eq false
expect(TagManager.instance.local_domain?('DoMaIn.Example.com!')).to eq false
end
end
@ -31,7 +31,7 @@ RSpec.describe TagManager do
around do |example|
original_web_domain = Rails.configuration.x.web_domain
Rails.configuration.x.web_domain = 'domain.test'
Rails.configuration.x.web_domain = 'domain.example.com'
example.run
@ -43,11 +43,11 @@ RSpec.describe TagManager do
end
it 'returns true if the slash-stripped string equals to web domain' do
expect(TagManager.instance.web_domain?('DoMaIn.Test/')).to eq true
expect(TagManager.instance.web_domain?('DoMaIn.Example.com/')).to eq true
end
it 'returns false for string with irrelevant characters' do
expect(TagManager.instance.web_domain?('DoMaIn.Test!')).to eq false
expect(TagManager.instance.web_domain?('DoMaIn.Example.com!')).to eq false
end
end
@ -57,7 +57,7 @@ RSpec.describe TagManager do
end
it 'returns normalized domain' do
expect(TagManager.instance.normalize_domain('DoMaIn.Test/')).to eq 'domain.test'
expect(TagManager.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com'
end
end
@ -69,18 +69,18 @@ RSpec.describe TagManager do
end
it 'returns true if the normalized string with port is local URL' do
Rails.configuration.x.web_domain = 'domain.test:42'
expect(TagManager.instance.local_url?('https://DoMaIn.Test:42/')).to eq true
Rails.configuration.x.web_domain = 'domain.example.com:42'
expect(TagManager.instance.local_url?('https://DoMaIn.Example.com:42/')).to eq true
end
it 'returns true if the normalized string without port is local URL' do
Rails.configuration.x.web_domain = 'domain.test'
expect(TagManager.instance.local_url?('https://DoMaIn.Test/')).to eq true
Rails.configuration.x.web_domain = 'domain.example.com'
expect(TagManager.instance.local_url?('https://DoMaIn.Example.com/')).to eq true
end
it 'returns false for string with irrelevant characters' do
Rails.configuration.x.web_domain = 'domain.test'
expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
Rails.configuration.x.web_domain = 'domain.example.com'
expect(TagManager.instance.local_url?('https://domain.example.net/')).to eq false
end
end
end

View File

@ -0,0 +1,229 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AccountStatusesFilter do
let(:account) { Fabricate(:account) }
let(:current_account) { nil }
let(:params) { {} }
subject { described_class.new(account, current_account, params) }
def status!(visibility)
Fabricate(:status, account: account, visibility: visibility)
end
def status_with_tag!(visibility, tag)
Fabricate(:status, account: account, visibility: visibility, tags: [tag])
end
def status_with_parent!(visibility)
Fabricate(:status, account: account, visibility: visibility, thread: Fabricate(:status))
end
def status_with_reblog!(visibility)
Fabricate(:status, account: account, visibility: visibility, reblog: Fabricate(:status))
end
def status_with_mention!(visibility, mentioned_account = nil)
Fabricate(:status, account: account, visibility: visibility).tap do |status|
Fabricate(:mention, status: status, account: mentioned_account || Fabricate(:account))
end
end
def status_with_media_attachment!(visibility)
Fabricate(:status, account: account, visibility: visibility).tap do |status|
Fabricate(:media_attachment, account: account, status: status)
end
end
describe '#results' do
let(:tag) { Fabricate(:tag) }
before do
status!(:public)
status!(:unlisted)
status!(:private)
status_with_parent!(:public)
status_with_reblog!(:public)
status_with_tag!(:public, tag)
status_with_mention!(:direct)
status_with_media_attachment!(:public)
end
shared_examples 'filter params' do
context 'with only_media param' do
let(:params) { { only_media: true } }
it 'returns only statuses with media' do
expect(subject.results.all?(&:with_media?)).to be true
end
end
context 'with tagged param' do
let(:params) { { tagged: tag.name } }
it 'returns only statuses with tag' do
expect(subject.results.all? { |s| s.tags.include?(tag) }).to be true
end
end
context 'with exclude_replies param' do
let(:params) { { exclude_replies: true } }
it 'returns only statuses that are not replies' do
expect(subject.results.none?(&:reply?)).to be true
end
end
context 'with exclude_reblogs param' do
let(:params) { { exclude_reblogs: true } }
it 'returns only statuses that are not reblogs' do
expect(subject.results.none?(&:reblog?)).to be true
end
end
end
context 'when accessed anonymously' do
let(:current_account) { nil }
let(:direct_status) { nil }
it 'returns only public statuses' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
end
it 'returns public replies' do
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns public reblogs' do
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end
it_behaves_like 'filter params'
end
context 'when accessed with a blocked account' do
let(:current_account) { Fabricate(:account) }
before do
account.block!(current_account)
end
it 'returns nothing' do
expect(subject.results.to_a).to be_empty
end
end
context 'when accessed by self' do
let(:current_account) { account }
it 'returns everything' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(direct private unlisted public)
end
it 'returns replies' do
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns reblogs' do
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end
it_behaves_like 'filter params'
end
context 'when accessed by a follower' do
let(:current_account) { Fabricate(:account) }
before do
current_account.follow!(account)
end
it 'returns private statuses' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(private unlisted public)
end
it 'returns replies' do
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns reblogs' do
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end
context 'when there is a direct status mentioning the non-follower' do
let!(:direct_status) { status_with_mention!(:direct, current_account) }
it 'returns the direct status' do
expect(subject.results.pluck(:id)).to include(direct_status.id)
end
end
it_behaves_like 'filter params'
end
context 'when accessed by a non-follower' do
let(:current_account) { Fabricate(:account) }
it 'returns only public statuses' do
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
end
it 'returns public replies' do
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
end
it 'returns public reblogs' do
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
end
context 'when there is a private status mentioning the non-follower' do
let!(:private_status) { status_with_mention!(:private, current_account) }
it 'returns the private status' do
expect(subject.results.pluck(:id)).to include(private_status.id)
end
end
context 'when blocking a reblogged account' do
let(:reblog) { status_with_reblog!('public') }
before do
current_account.block!(reblog.reblog.account)
end
it 'does not return reblog of blocked account' do
expect(subject.results.pluck(:id)).to_not include(reblog.id)
end
end
context 'when muting a reblogged account' do
let(:reblog) { status_with_reblog!('public') }
before do
current_account.mute!(reblog.reblog.account)
end
it 'does not return reblog of muted account' do
expect(subject.results.pluck(:id)).to_not include(reblog.id)
end
end
context 'when blocked by a reblogged account' do
let(:reblog) { status_with_reblog!('public') }
before do
reblog.reblog.account.block!(current_account)
end
it 'does not return reblog of blocked-by account' do
expect(subject.results.pluck(:id)).to_not include(reblog.id)
end
end
it_behaves_like 'filter params'
end
end
end

View File

@ -119,7 +119,7 @@ describe Report do
end
end
describe 'validatiions' do
describe 'validations' do
it 'has a valid fabricator' do
report = Fabricate(:report)
report.valid?

View File

@ -435,59 +435,6 @@ RSpec.describe Status, type: :model do
end
end
describe '.permitted_for' do
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
let(:target_account) { alice }
let(:account) { bob }
let!(:public_status) { Fabricate(:status, account: target_account, visibility: 'public') }
let!(:unlisted_status) { Fabricate(:status, account: target_account, visibility: 'unlisted') }
let!(:private_status) { Fabricate(:status, account: target_account, visibility: 'private') }
let!(:direct_status) do
Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
Fabricate(:mention, status: status, account: account)
end
end
let!(:other_direct_status) do
Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
Fabricate(:mention, status: status)
end
end
context 'given nil' do
let(:account) { nil }
let(:direct_status) { nil }
it { is_expected.to eq(%w(unlisted public)) }
end
context 'given blocked account' do
before do
target_account.block!(account)
end
it { is_expected.to be_empty }
end
context 'given same account' do
let(:account) { target_account }
it { is_expected.to eq(%w(direct direct private unlisted public)) }
end
context 'given followed account' do
before do
account.follow!(target_account)
end
it { is_expected.to eq(%w(direct private unlisted public)) }
end
context 'given unfollowed account' do
it { is_expected.to eq(%w(direct unlisted public)) }
end
end
describe 'before_validation' do
it 'sets account being replied to correctly over intermediary nodes' do
first_status = Fabricate(:status, account: bob)

View File

@ -114,13 +114,13 @@ RSpec.describe UserPolicy do
permissions :promote? do
context 'admin?' do
context 'promoteable?' do
context 'promotable?' do
it 'permits' do
expect(subject).to permit(admin, john.user)
end
end
context '!promoteable?' do
context '!promotable?' do
it 'denies' do
expect(subject).to_not permit(admin, admin.user)
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe FamiliarFollowersPresenter do
describe '#accounts' do
let(:account) { Fabricate(:account) }
let(:familiar_follower) { Fabricate(:account) }
let(:requested_accounts) { Fabricate.times(2, :account) }
subject { described_class.new(requested_accounts, account.id) }
before do
familiar_follower.follow!(requested_accounts.first)
account.follow!(familiar_follower)
end
it 'returns a result for each requested account' do
expect(subject.accounts.map(&:id)).to eq requested_accounts.map(&:id)
end
it 'returns followers you follow' do
result = subject.accounts.first
expect(result).to_not be_nil
expect(result.id).to eq requested_accounts.first.id
expect(result.accounts).to match_array([familiar_follower])
end
context 'when requested account hides followers' do
before do
requested_accounts.first.update(hide_collections: true)
end
it 'does not return followers you follow' do
result = subject.accounts.first
expect(result).to_not be_nil
expect(result.id).to eq requested_accounts.first.id
expect(result.accounts).to be_empty
end
end
context 'when familiar follower hides follows' do
before do
familiar_follower.update(hide_collections: true)
end
it 'does not return followers you follow' do
result = subject.accounts.first
expect(result).to_not be_nil
expect(result.id).to eq requested_accounts.first.id
expect(result.accounts).to be_empty
end
end
end
end

View File

@ -63,20 +63,20 @@ RSpec.describe UnsuspendAccountService, type: :service do
describe 'unsuspending a remote account' do
include_examples 'common behavior' do
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
let!(:reslove_account_service) { double }
let!(:resolve_account_service) { double }
before do
allow(ResolveAccountService).to receive(:new).and_return(reslove_account_service)
allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service)
end
context 'when the account is not remotely suspended' do
before do
allow(reslove_account_service).to receive(:call).with(account).and_return(account)
allow(resolve_account_service).to receive(:call).with(account).and_return(account)
end
it 're-fetches the account' do
subject.call
expect(reslove_account_service).to have_received(:call).with(account)
expect(resolve_account_service).to have_received(:call).with(account)
end
it "merges back into local followers' feeds" do
@ -92,7 +92,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
context 'when the account is remotely suspended' do
before do
allow(reslove_account_service).to receive(:call).with(account) do |account|
allow(resolve_account_service).to receive(:call).with(account) do |account|
account.suspend!(origin: :remote)
account
end
@ -100,7 +100,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
it 're-fetches the account' do
subject.call
expect(reslove_account_service).to have_received(:call).with(account)
expect(resolve_account_service).to have_received(:call).with(account)
end
it "does not merge back into local followers' feeds" do
@ -116,12 +116,12 @@ RSpec.describe UnsuspendAccountService, type: :service do
context 'when the account is remotely deleted' do
before do
allow(reslove_account_service).to receive(:call).with(account).and_return(nil)
allow(resolve_account_service).to receive(:call).with(account).and_return(nil)
end
it 're-fetches the account' do
subject.call
expect(reslove_account_service).to have_received(:call).with(account)
expect(resolve_account_service).to have_received(:call).with(account)
end
it "does not merge back into local followers' feeds" do

View File

@ -22,7 +22,7 @@ module ProfileStories
def with_alice_as_local_user
@alice_bio = '@alice and @bob are fictional characters commonly used as'\
'placeholder names in #cryptology, as well as #science and'\
'engineering 📖 literature. Not affilated with @pepe.'
'engineering 📖 literature. Not affiliated with @pepe.'
@alice = Fabricate(
:user,