Merge remote-tracking branch 'parent/main' into upstream-20231110

This commit is contained in:
KMY 2023-11-10 09:02:03 +09:00
commit bfc7b0101d
44 changed files with 992 additions and 744 deletions

View file

@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action :require_user!
def index
accounts = Account.without_suspended.where(id: account_ids).select('id')
scope = Account.where(id: account_ids).select('id')
scope.merge!(Account.without_suspended) unless truthy_param?(:with_suspended)
# .where doesn't guarantee that our results are in the same order
# we requested them, so return the "right" order to the requestor.
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
@accounts = scope.index_by(&:id).values_at(*account_ids).compact
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
end

View file

@ -91,6 +91,14 @@ module ApplicationHelper
end
end
def html_title
safe_join(
[content_for(:page_title).to_s.chomp, title]
.select(&:present?),
' - '
)
end
def title
Rails.env.production? ? site_title : "#{site_title} (Dev)"
end

View file

@ -460,7 +460,7 @@ export function fetchRelationships(accountIds) {
dispatch(fetchRelationshipsRequest(newAccountIds));
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));

View file

@ -29,6 +29,7 @@ export interface ApiAccountOtherSettingsJSON {
| 'followers_only'
| 'mutuals_only'
| 'block';
subscription_policy: 'allow' | 'followers_only' | 'block';
}
// See app/serializers/rest/account_serializer.rb
@ -63,4 +64,5 @@ export interface ApiAccountJSON {
suspended?: boolean;
limited?: boolean;
memorial?: boolean;
hide_collections: boolean;
}

View file

@ -121,7 +121,7 @@ class Account extends ImmutablePureComponent {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('moved') || following) {
} else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
}
}

View file

@ -295,7 +295,7 @@ class Header extends ImmutablePureComponent {
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
}
if (signedIn && account.get('id') !== me) {
if (signedIn && account.get('id') !== me && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
@ -305,7 +305,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
}
if ('share' in navigator) {
if ('share' in navigator && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
menu.push(null);
}
@ -358,7 +358,9 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
if (!account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
}
}
if (signedIn && isRemote) {
@ -406,7 +408,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__image'>
<div className='account__header__info'>
{!suspended && info}
{info}
</div>
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
@ -418,18 +420,16 @@ class Header extends ImmutablePureComponent {
<Avatar account={suspended || hidden ? undefined : account} size={90} />
</a>
{!suspended && (
<div className='account__header__tabs__buttons'>
{!hidden && (
<>
{actionBtn}
{bellBtn}
</>
)}
<div className='account__header__tabs__buttons'>
{!hidden && (
<>
{actionBtn}
{bellBtn}
</>
)}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
</div>
)}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
</div>
</div>
<div className='account__header__tabs__name'>

View file

@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
@ -111,7 +112,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) {
return (
@ -137,6 +138,8 @@ class Followers extends ImmutablePureComponent {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {

View file

@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
@ -111,7 +112,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) {
return (
@ -137,6 +138,8 @@ class Following extends ImmutablePureComponent {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {

View file

@ -469,6 +469,10 @@ class Video extends PureComponent {
};
_syncVideoToVolumeState = (volume = null, muted = null) => {
if (!this.video) {
return;
}
this.video.volume = volume ?? this.state.volume;
this.video.muted = muted ?? this.state.muted;
};

View file

@ -238,6 +238,7 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "No posts here!",
"empty_column.account_unavailable": "Profile unavailable",

View file

@ -59,6 +59,7 @@ const AccountOtherSettingsFactory = ImmutableRecord<AccountOtherSettingsShape>({
link_preview: true,
allow_quote: true,
emoji_reaction_policy: 'allow',
subscription_policy: 'allow',
});
// Account
@ -111,6 +112,7 @@ export const accountDefaultValues: AccountShape = {
memorial: false,
limited: false,
moved: null,
hide_collections: false,
other_settings: AccountOtherSettingsFactory(),
subscribable: true,
};

View file

@ -18,12 +18,37 @@ class StatusCacheHydrator
# We take advantage of the fact that some relationships can only occur with an original status, not
# the reblog that wraps it, so we can assume that some values are always false
if payload[:reblog]
hydrate_reblog_payload(payload, account_id)
else
hydrate_non_reblog_payload(payload, account_id, account)
end
end
private
def hydrate_non_reblog_payload(empty_payload, account_id, account)
empty_payload.tap do |payload|
payload[:favourited] = Favourite.where(account_id: account_id, status_id: @status.id).exists?
payload[:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.id).exists?
payload[:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.conversation_id).exists?
payload[:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.id).exists?
payload[:pinned] = StatusPin.where(account_id: account_id, status_id: @status.id).exists? if @status.account_id == account_id
payload[:filtered] = mapped_applied_custom_filter(account_id, @status)
payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
if payload[:poll]
payload[:poll][:voted] = @status.account_id == account_id
payload[:poll][:own_votes] = []
end
end
end
def hydrate_reblog_payload(empty_payload, account_id)
empty_payload.tap do |payload|
payload[:muted] = false
payload[:bookmarked] = false
payload[:pinned] = false if @status.account_id == account_id
payload[:filtered] = CustomFilter
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), @status.reblog, following?(account_id))
.map { |filter| serialized_filter(filter) }
payload[:filtered] = mapped_applied_custom_filter(account_id, @status.reblog)
# If the reblogged status is being delivered to the author who disabled the display of the application
# used to create the status, we need to hydrate it here too
@ -50,27 +75,14 @@ class StatusCacheHydrator
payload[:favourited] = payload[:reblog][:favourited]
payload[:reblogged] = payload[:reblog][:reblogged]
else
payload[:favourited] = Favourite.where(account_id: account_id, status_id: @status.id).exists?
payload[:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.id).exists?
payload[:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.conversation_id).exists?
payload[:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.id).exists?
payload[:pinned] = StatusPin.where(account_id: account_id, status_id: @status.id).exists? if @status.account_id == account_id
payload[:filtered] = CustomFilter
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), @status, following?(account_id))
.map { |filter| serialized_filter(filter) }
payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
if payload[:poll]
payload[:poll][:voted] = @status.account_id == account_id
payload[:poll][:own_votes] = []
end
end
payload
end
private
def mapped_applied_custom_filter(account_id, status)
CustomFilter
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), status, following?(account_id))
.map { |filter| serialized_filter(filter) }
end
def following?(account_id)
Follow.exists?(account_id: account_id, target_account_id: @status.account_id)

View file

@ -8,7 +8,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
:note, :url, :uri, :avatar, :avatar_static, :header, :header_static, :subscribable,
:followers_count, :following_count, :statuses_count, :last_status_at, :other_settings, :noindex
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections, :other_settings, :noindex
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?

View file

@ -24,7 +24,7 @@
%meta{ name: 'theme-color', content: '#191b22' }/
%meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
%title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title
%title= html_title
= stylesheet_pack_tag 'common', media: 'all', crossorigin: 'anonymous'
= stylesheet_pack_tag current_theme, media: 'all', crossorigin: 'anonymous'