Merge branch 'kb_development' into kb_migration

This commit is contained in:
KMY 2023-09-13 22:07:59 +09:00
commit 74d4062f9d
60 changed files with 647 additions and 514 deletions

View file

@ -15,6 +15,10 @@ class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseControll
private
def load_accounts
return [] unless Setting.enable_emoji_reaction
return [] if current_account.nil? && @status.account.emoji_reaction_policy != :allow
return [] if current_account.present? && !@status.account.show_emoji_reaction?(current_account)
scope = default_accounts
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_emoji_reactions).to_a

View file

@ -42,6 +42,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
def create_private(emoji)
count = EmojiReaction.where(account: current_account, status: @status).count
raise Mastodon::ValidationError, I18n.t('reactions.errors.limit_reached') if count >= EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT
raise Mastodon::ValidationError, I18n.t('reactions.errors.disabled') unless Setting.enable_emoji_reaction
EmojiReactService.new.call(current_account, @status, emoji)
render json: @status, serializer: REST::StatusSerializer

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Settings::Preferences::ReachingController < Settings::Preferences::BaseController
private
def after_update_redirect_path
settings_preferences_reaching_path
end
end

View file

@ -18,7 +18,7 @@ import Card from '../features/status/components/card';
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia } from '../initial_state';
import { displayMedia, enableEmojiReaction, showEmojiReactionOnTimeline } from '../initial_state';
import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay';
@ -577,8 +577,8 @@ class Status extends ImmutablePureComponent {
let emojiReactionsBar = null;
if (!this.props.withoutEmojiReactions && status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions');
if (emojiReactions.size > 0) {
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
if (emojiReactions.size > 0 && enableEmojiReaction) {
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} myReactionOnly={!showEmojiReactionOnTimeline} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
}
}
@ -622,7 +622,7 @@ class Status extends ImmutablePureComponent {
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{expanded && hashtagBar}
{(!status.get('spoiler_text') || expanded) && hashtagBar}
{emojiReactionsBar}

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
import { bookmarkCategoryNeeded, me } from '../initial_state';
import { enableEmojiReaction , bookmarkCategoryNeeded, me } from '../initial_state';
import { IconButton } from './icon_button';
@ -409,13 +410,16 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleHideClick} />
);
const following = !account.getIn(['other_settings', 'emoji_reaction_must_follower']) || (relationship && relationship.get('following'));
const followed = !account.getIn(['other_settings', 'emoji_reaction_must_following']) || (relationship && relationship.get('followed_by'));
const denyFromAll = !account.getIn(['other_settings', 'emoji_reaction_deny_from_all']);
const emojiReactionPolicy = account.getIn(['other_settings', 'emoji_reaction_policy']) || 'allow';
const following = emojiReactionPolicy !== 'following_only' || (relationship && relationship.get('following'));
const followed = emojiReactionPolicy !== 'followers_only' || (relationship && relationship.get('followed_by'));
const mutual = emojiReactionPolicy !== 'mutuals_only' || (relationship && relationship.get('following') && relationship.get('followed_by'));
const outside = emojiReactionPolicy !== 'outside_only' || (relationship && (relationship.get('following') || relationship.get('followed_by')));
const denyFromAll = emojiReactionPolicy !== 'block' && emojiReactionPolicy !== 'block';
const emojiPickerButton = (
<IconButton className='status__action-bar__button' title={intl.formatMessage(messages.emojiReaction)} icon='smile-o' onClick={this.handleEmojiPickInnerButton} />
);
const emojiPickerDropdown = (writtenByMe || ((denyFromAll) && (following) && (followed))) && (
const emojiPickerDropdown = enableEmojiReaction && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={emojiPickerButton} />
);

View file

@ -60,6 +60,7 @@ class StatusEmojiReactionsBar extends PureComponent {
status: ImmutablePropTypes.map,
onEmojiReact: PropTypes.func,
onUnEmojiReact: PropTypes.func,
myReactionOnly: PropTypes.bool,
};
onEmojiReact = (name) => {
@ -73,20 +74,23 @@ class StatusEmojiReactionsBar extends PureComponent {
};
render () {
const { emojiReactions } = this.props;
const { emojiReactions, myReactionOnly } = this.props;
const emojiButtons = Array.from(emojiReactions).filter(emoji => emoji.get('count') !== 0).map((emoji, index) => (
<EmojiReactionButton
key={index}
name={emoji.get('name')}
count={emoji.get('count')}
me={emoji.get('me')}
url={emoji.get('url')}
staticUrl={emoji.get('static_url')}
domain={emoji.get('domain')}
onEmojiReact={this.onEmojiReact}
onUnEmojiReact={this.onUnEmojiReact}
/>));
const emojiButtons = Array.from(emojiReactions)
.filter(emoji => emoji.get('count') !== 0)
.filter(emoji => !myReactionOnly || emoji.get('me'))
.map((emoji, index) => (
<EmojiReactionButton
key={index}
name={emoji.get('name')}
count={myReactionOnly ? 1 : emoji.get('count')}
me={emoji.get('me')}
url={emoji.get('url')}
staticUrl={emoji.get('static_url')}
domain={emoji.get('domain')}
onEmojiReact={this.onEmojiReact}
onUnEmojiReact={this.onUnEmojiReact}
/>));
return (
<div className='status__emoji-reactions-bar'>

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { IconButton } from '../../../components/icon_button';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { bookmarkCategoryNeeded, me } from '../../../initial_state';
import { enableEmojiReaction , bookmarkCategoryNeeded, me } from '../../../initial_state';
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
const messages = defineMessages({
@ -305,10 +306,6 @@ class ActionBar extends PureComponent {
}
}
const emojiPickerButton = (
<IconButton icon='smile-o' onClick={this.handleEmojiPickInnerButton} title={intl.formatMessage(messages.pickEmoji)} />
);
let replyIcon;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
@ -329,13 +326,26 @@ class ActionBar extends PureComponent {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
const emojiReactionPolicy = account.getIn(['other_settings', 'emoji_reaction_policy']) || 'allow';
const following = emojiReactionPolicy !== 'following_only' || (relationship && relationship.get('following'));
const followed = emojiReactionPolicy !== 'followers_only' || (relationship && relationship.get('followed_by'));
const mutual = emojiReactionPolicy !== 'mutuals_only' || (relationship && relationship.get('following') && relationship.get('followed_by'));
const outside = emojiReactionPolicy !== 'outside_only' || (relationship && (relationship.get('following') || relationship.get('followed_by')));
const denyFromAll = emojiReactionPolicy !== 'block' && emojiReactionPolicy !== 'block';
const emojiPickerButton = (
<IconButton icon='smile-o' onClick={this.handleEmojiPickInnerButton} title={intl.formatMessage(messages.pickEmoji)} />
);
const emojiPickerDropdown = enableEmojiReaction && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
<div className='detailed-status__button'><EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={emojiPickerButton} /></div>
);
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__button'><EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={emojiPickerButton} /></div>
{emojiPickerDropdown}
<div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />

View file

@ -13,6 +13,7 @@ import EditedTimestamp from 'mastodon/components/edited_timestamp';
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { enableEmojiReaction } from 'mastodon/initial_state';
import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
@ -240,7 +241,8 @@ class DetailedStatus extends ImmutablePureComponent {
let emojiReactionsBar = null;
if (status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions');
if (emojiReactions.size > 0) {
const emojiReactionPolicy = status.getIn(['account', 'other_settings', 'emoji_reaction_policy']) || 'allow';
if (emojiReactions.size > 0 && enableEmojiReaction && emojiReactionPolicy !== 'block') {
emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />;
}
}
@ -398,7 +400,7 @@ class DetailedStatus extends ImmutablePureComponent {
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
{expanded && hashtagBar}
{(!status.get('spoiler_text') || expanded) && hashtagBar}
{emojiReactionsBar}

View file

@ -59,6 +59,7 @@
* @property {boolean} display_media_expand
* @property {string} domain
* @property {string} dtl_tag
* @property {boolean} enable_emoji_reaction
* @property {boolean} enable_login_privacy
* @property {boolean} enable_dtl_menu
* @property {boolean=} expand_spoilers
@ -75,6 +76,7 @@
* @property {string} repository
* @property {boolean} search_enabled
* @property {boolean} trends_enabled
* @property {boolean} show_emoji_reaction_on_timeline
* @property {boolean} single_user_mode
* @property {string} source_url
* @property {string} streaming_api_base_url
@ -126,6 +128,7 @@ export const displayMedia = getMeta('display_media');
export const displayMediaExpand = getMeta('display_media_expand');
export const domain = getMeta('domain');
export const dtlTag = getMeta('dtl_tag');
export const enableEmojiReaction = getMeta('enable_emoji_reaction');
export const enableLoginPrivacy = getMeta('enable_login_privacy');
export const enableDtlMenu = getMeta('enable_dtl_menu');
export const expandSpoilers = getMeta('expand_spoilers');
@ -142,6 +145,7 @@ export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled');
export const trendsEnabled = getMeta('trends_enabled');
export const showEmojiReactionOnTimeline = getMeta('show_emoji_reaction_on_timeline');
export const showTrends = getMeta('show_trends');
export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url');

View file

@ -32,7 +32,11 @@ textarea {
color: $primary-text-color;
}
.compose-form .compose-form__warning {
.compose-form .compose-form__warning,
.reply-indicator__content,
.reply-indicator__display-name,
.reply-indicator__cancel,
.autosuggest-textarea__suggestions__item {
color: $ui-base-color;
}

View file

@ -7,7 +7,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
def perform
@original_status = status_from_uri(object_uri)
return if @original_status.nil? || !@original_status.account.local? || delete_arrived_first?(@json['id']) || reject_favourite?
return if @original_status.nil? || delete_arrived_first?(@json['id']) || reject_favourite?
if shortcode.nil?
process_favourite
@ -32,6 +32,8 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
end
def process_emoji_reaction
return if !@original_status.account.local? && !Setting.receive_other_servers_emoji_reaction
if emoji_tag.present?
return if emoji_tag['id'].blank? || emoji_tag['name'].blank? || emoji_tag['icon'].blank? || emoji_tag['icon']['url'].blank?
@ -106,10 +108,10 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
end
def write_stream(emoji_reaction)
emoji_group = @original_status.emoji_reactions_grouped_by_name
emoji_group = @original_status.emoji_reactions_grouped_by_name(nil, force: true)
.find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
emoji_group['status_id'] = @original_status.id.to_s
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id)
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id) if @original_status.local? || Setting.streaming_other_servers_emoji_reaction
end
def render_emoji_reaction(emoji_group)

View file

@ -149,7 +149,7 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => @original_status.id.to_s }
emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji
end
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id)
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id) if @original_status.local? || Setting.streaming_other_servers_emoji_reaction
end
def render_emoji_reaction(emoji_group)

View file

@ -61,19 +61,19 @@ class SearchQueryTransformer < Parslet::Transform
when 'library'
[StatusesIndex]
else
[PublicStatusesIndex, StatusesIndex]
@options[:current_account].user&.setting_use_public_index ? [PublicStatusesIndex, StatusesIndex] : [StatusesIndex]
end
end
def default_filter
definition_should = [
default_should1,
default_should2,
non_publicly_searchable,
public_index,
searchability_limited,
]
definition_should << searchability_public if %i(public).include?(@searchability)
definition_should << searchability_private if %i(public unlisted private).include?(@searchability)
definition_should << searchable_by_me if %i(public unlisted private direct).include?(@searchability)
definition_should << self_posts if %i(public unlisted private direct).exclude?(@searchability)
{
bool: {
@ -83,7 +83,7 @@ class SearchQueryTransformer < Parslet::Transform
}
end
def default_should1
def public_index
{
term: {
_index: PublicStatusesIndex.index_name,
@ -91,24 +91,7 @@ class SearchQueryTransformer < Parslet::Transform
}
end
def default_should2
{
bool: {
must: [
{
term: { _index: StatusesIndex.index_name },
},
{
term: {
searchable_by: @options[:current_account].id,
},
},
],
},
}
end
def non_publicly_searchable
def searchable_by_me
{
bool: {
must: [
@ -128,6 +111,21 @@ class SearchQueryTransformer < Parslet::Transform
}
end
def self_posts
{
bool: {
must: [
{
term: { _index: StatusesIndex.index_name },
},
{
term: { account_id: @options[:current_account].id },
},
],
},
}
end
def searchability_public
{
bool: {
@ -349,6 +347,6 @@ class SearchQueryTransformer < Parslet::Transform
end
rule(query: sequence(:clauses)) do
Query.new(clauses, current_account: current_account)
Query.new(clauses, current_account: current_account, searchability: searchability)
end
end

View file

@ -6,6 +6,8 @@ class StatusCacheHydrator
end
def hydrate(account_id)
account = Account.find(account_id)
# The cache of the serialized hash is generated by the fan-out-on-write service
payload = Rails.cache.fetch("fan-out/#{@status.id}") { InlineRenderer.render(@status, nil, :status) }
@ -33,6 +35,7 @@ class StatusCacheHydrator
payload[:reblog][:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.reblog_of_id).exists?
payload[:reblog][:pinned] = StatusPin.where(account_id: account_id, status_id: @status.reblog_of_id).exists? if @status.reblog.account_id == account_id
payload[:reblog][:filtered] = payload[:filtered]
payload[:reblog][:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
if payload[:reblog][:poll]
if @status.reblog.account_id == account_id
@ -56,6 +59,7 @@ class StatusCacheHydrator
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

View file

@ -363,28 +363,37 @@ class Account < ApplicationRecord
false
end
def emoji_reactions_must_following?
return false unless Setting.enable_block_emoji_reaction_settings
return user&.settings&.[]('emoji_reactions.must_be_following') || false if user.present?
return settings['emoji_reactions_must_be_following'] || false if settings.present?
def emoji_reaction_policy
return settings['emoji_reaction_policy']&.to_sym || :allow if settings.present? && user.nil?
return :allow if user.nil?
return :block if local? && !Setting.enable_emoji_reaction
false
user.setting_emoji_reaction_policy&.to_sym
end
def emoji_reactions_must_follower?
return false unless Setting.enable_block_emoji_reaction_settings
return user&.settings&.[]('emoji_reactions.must_be_follower') || false if user.present?
return settings['emoji_reaction_must_be_follower'] || false if settings.present?
def show_emoji_reaction?(account)
return false unless Setting.enable_emoji_reaction
false
case emoji_reaction_policy
when :block
false
when :following_only
account.present? && (id == account.id || following?(account))
when :followers_only
account.present? && (id == account.id || followed_by?(account))
when :mutuals_only
account.present? && (id == account.id || mutual?(account))
when :outside_only
account.present? && (id == account.id || following?(account) || followed_by?(account))
else
true
end
end
def emoji_reactions_deny_from_all?
return false unless Setting.enable_block_emoji_reaction_settings
return user&.settings&.[]('emoji_reactions.deny_from_all') || false if user.present?
return settings['emoji_reaction_deny_from_all'] || false if settings.present?
def allow_emoji_reaction?(account)
return false if account.nil?
false
show_emoji_reaction?(account)
end
def public_settings
@ -398,17 +407,27 @@ class Account < ApplicationRecord
'translatable_private' => translatable_private?,
'link_preview' => link_preview?,
}
if Setting.enable_block_emoji_reaction_settings
if Setting.enable_emoji_reaction
config = config.merge({
'emoji_reaction_must_following' => emoji_reactions_must_following?,
'emoji_reaction_must_follower' => emoji_reactions_must_follower?,
'emoji_reaction_deny_from_all' => emoji_reactions_deny_from_all?,
'emoji_reaction_policy' => emoji_reaction_policy,
})
end
config = config.merge(settings) if settings.present?
config
end
def public_settings_for_local
config = public_settings
unless Setting.enable_emoji_reaction
config = config.merge({
'emoji_reaction_policy' => :block,
})
end
config
end
def previous_strikes_count
strikes.where(overruled_at: nil).count
end

View file

@ -211,6 +211,10 @@ module AccountInteractions
other_account.following?(self)
end
def mutual?(other_account)
following?(other_account) && followed_by?(other_account)
end
def blocking?(other_account)
block_relationships.where(target_account: other_account).exists?
end

View file

@ -43,6 +43,14 @@ module HasUserSettings
settings['web.hide_recent_emojis']
end
def setting_enable_emoji_reaction
settings['web.enable_emoji_reaction']
end
def setting_show_emoji_reaction_on_timeline
settings['web.show_emoji_reaction_on_timeline']
end
def setting_default_sensitive
settings['default_sensitive']
end
@ -75,6 +83,10 @@ module HasUserSettings
false
end
def setting_emoji_reaction_policy
settings['emoji_reaction_policy']
end
def setting_unfollow_modal
settings['web.unfollow_modal']
end
@ -204,7 +216,15 @@ module HasUserSettings
end
def setting_default_searchability
settings['default_searchability'] || 'private'
settings['default_searchability'] || 'direct'
end
def setting_default_searchability_of_search
settings['default_searchability_of_search']
end
def setting_use_public_index
settings['use_public_index']
end
def setting_disallow_unlisted_public_searchability

View file

@ -18,7 +18,7 @@ class EmojiReaction < ApplicationRecord
include Paginable
EMOJI_REACTION_LIMIT = 32_767
EMOJI_REACTION_PER_ACCOUNT_LIMIT = 3
EMOJI_REACTION_PER_ACCOUNT_LIMIT = ENV.fetch('EMOJI_REACTION_PER_ACCOUNT_LIMIT', 3)
update_index('statuses', :status)

View file

@ -37,12 +37,14 @@ class Form::AdminSettings
status_page_url
captcha_enabled
ng_words
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous
post_hash_tags_max
sensitive_words
sensitive_words_for_full
authorized_fetch
receive_other_servers_emoji_reaction
streaming_other_servers_emoji_reaction
enable_emoji_reaction
).freeze
INTEGER_KEYS = %i(
@ -64,9 +66,11 @@ class Form::AdminSettings
noindex
require_invite_text
captcha_enabled
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous
authorized_fetch
receive_other_servers_emoji_reaction
streaming_other_servers_emoji_reaction
enable_emoji_reaction
).freeze
UPLOAD_KEYS = %i(

View file

@ -349,14 +349,25 @@ class Status < ApplicationRecord
update_status_stat!(status_referred_by_count: [public_send(:status_referred_by_count) + diff, 0].max)
end
def emoji_reactions_grouped_by_name(account = nil)
def emoji_reactions_grouped_by_name(account = nil, **options)
return [] if account.present? && !self.account.show_emoji_reaction?(account)
return [] if account.nil? && !options[:force] && self.account.emoji_reaction_policy != :allow
(Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions|
if account.present?
remove_emoji_reactions = []
emoji_reactions.each do |emoji_reaction|
emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s)
emoji_reaction['account_ids'] -= account.excluded_from_timeline_account_ids.map(&:to_s)
accounts = Account.where(id: emoji_reaction['account_ids'], silenced_at: nil, suspended_at: nil)
accounts = accounts.where('domain IS NULL OR domain NOT IN (?)', account.excluded_from_timeline_domains) if account.excluded_from_timeline_domains.size.positive?
emoji_reaction['account_ids'] = accounts.pluck(:id).map(&:to_s)
emoji_reaction['count'] = emoji_reaction['account_ids'].size
remove_emoji_reactions << emoji_reaction if emoji_reaction['count'] <= 0
end
emoji_reactions - remove_emoji_reactions
end
end
end

View file

@ -12,7 +12,7 @@ class Trends::Tags < Trends::Base
}
def register(status, at_time = Time.now.utc)
return unless !status.reblog? && status.public_visibility? && !status.account.silenced?
return unless !status.reblog? && %i(public public_unlisted login).include?(status.visibility.to_sym) && !status.account.silenced?
status.tags.each do |tag|
add(tag, status.account_id, at_time) if tag.usable?

View file

@ -26,6 +26,8 @@ class UserSettings
setting :stay_privacy, default: false
setting :default_reblog_privacy, default: nil
setting :default_searchability, default: :direct, in: %w(public private direct limited)
setting :default_searchability_of_search, default: :public, in: %w(public private direct limited)
setting :use_public_index, default: true
setting :disallow_unlisted_public_searchability, default: false
setting :public_post_to_unlisted, default: false
setting :reject_public_unlisted_subscription, default: false
@ -34,6 +36,7 @@ class UserSettings
setting :reaction_deck, default: nil
setting :stop_emoji_reaction_streaming, default: false
setting :emoji_reaction_streaming_notify_impl2, default: false
setting :emoji_reaction_policy, default: :allow, in: %w(allow outside_only followers_only following_only mutuals_only block)
setting :unsafe_limited_distribution, default: false
setting :dtl_force_with_tag, default: :none, in: %w(full searchability none)
setting :dtl_force_subscribable, default: false
@ -52,6 +55,8 @@ class UserSettings
setting :enable_login_privacy, default: false
setting :enable_dtl_menu, default: false
setting :hide_recent_emojis, default: false
setting :enable_emoji_reaction, default: true
setting :show_emoji_reaction_on_timeline, default: true
setting :reblog_modal, default: false
setting :unfollow_modal, default: true
setting :reduce_motion, default: false
@ -80,12 +85,6 @@ class UserSettings
setting :must_be_following_dm, default: false
end
namespace :emoji_reactions do
setting :must_be_follower, default: false
setting :must_be_following, default: false
setting :deny_from_all, default: false
end
def initialize(original_hash)
@original_hash = original_hash || {}
end

View file

@ -48,6 +48,8 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:display_media] = object.current_account.user.setting_display_media
store[:display_media_expand] = object.current_account.user.setting_display_media_expand
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
store[:enable_emoji_reaction] = object.current_account.user.setting_enable_emoji_reaction
store[:show_emoji_reaction_on_timeline] = object.current_account.user.setting_show_emoji_reaction_on_timeline
store[:enable_login_privacy] = object.current_account.user.setting_enable_login_privacy
store[:enable_dtl_menu] = object.current_account.user.setting_enable_dtl_menu
store[:hide_recent_emojis] = object.current_account.user.setting_hide_recent_emojis
@ -63,6 +65,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:display_media] = Setting.display_media
store[:reduce_motion] = Setting.reduce_motion
store[:use_blurhash] = Setting.use_blurhash
store[:enable_emoji_reaction] = Setting.enable_emoji_reaction
end
store[:disabled_account_id] = object.disabled_account.id.to_s if object.disabled_account

View file

@ -168,6 +168,6 @@ class REST::AccountSerializer < ActiveModel::Serializer
end
def other_settings
object.suspended? ? {} : object.public_settings
object.suspended? ? {} : object.public_settings_for_local
end
end

View file

@ -108,7 +108,6 @@ class REST::InstanceSerializer < ActiveModel::Serializer
# for third party apps
def fedibird_capabilities
capabilities = [
:emoji_reaction,
:kmyblue_visibility_public_unlisted,
:enable_wide_emoji,
:enable_wide_emoji_reaction,
@ -126,6 +125,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
]
capabilities << :profile_search unless Chewy.enabled?
capabilities << :emoji_reaction if Setting.enable_emoji_reaction
capabilities
end

View file

@ -125,6 +125,16 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.emoji_reactions_grouped_by_name(current_user&.account)
end
def emoji_reactions_count
if current_user&.account.nil?
return 0 unless Setting.enable_emoji_reaction
object.account.emoji_reaction_policy == :allow ? object.emoji_reactions_count : 0
else
object.account.show_emoji_reaction?(current_user.account) ? object.emoji_reactions_count : 0
end
end
def reactions
emoji_reactions.tap do |rs|
rs.each do |emoji_reaction|

View file

@ -117,7 +117,6 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
# for third party apps
def fedibird_capabilities
capabilities = [
:emoji_reaction,
:kmyblue_visibility_public_unlisted,
:enable_wide_emoji,
:enable_wide_emoji_reaction,
@ -135,6 +134,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
]
capabilities << :profile_search unless Chewy.enabled?
capabilities << :emoji_reaction if Setting.enable_emoji_reaction
capabilities
end

View file

@ -46,8 +46,10 @@ class EmojiReactService < BaseService
status = emoji_reaction.status
if status.account.local?
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'reaction') if status.account.user&.setting_emoji_reaction_streaming_notify_impl2
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'emoji_reaction')
if status.account.user&.setting_enable_emoji_reaction
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'reaction') if status.account.user&.setting_emoji_reaction_streaming_notify_impl2
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'emoji_reaction')
end
elsif status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url)
end
@ -63,7 +65,7 @@ class EmojiReactService < BaseService
end
def write_stream(emoji_reaction)
emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name
emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name(nil, force: true)
.find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
emoji_group['status_id'] = emoji_reaction.status_id.to_s
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id)

View file

@ -9,7 +9,6 @@ class NotifyService < BaseService
update
poll
emoji_reaction
status
warning
).freeze
@ -53,18 +52,6 @@ class NotifyService < BaseService
@recipient.user.settings['interactions.must_be_following'] && !following_sender?
end
def optional_non_follower_emoji_reaction?
emoji_reaction? && @recipient.user.settings['emoji_reactions.must_be_follower'] && !@notification.from_account.following?(@recipient)
end
def optional_non_following_emoji_reaction?
emoji_reaction? && @recipient.user.settings['emoji_reactions.must_be_following'] && !following_sender?
end
def emoji_reaction?
@notification.type == :emoji_reaction
end
def message?
@notification.type == :mention
end
@ -134,8 +121,6 @@ class NotifyService < BaseService
blocked ||= optional_non_follower?
blocked ||= optional_non_following?
blocked ||= optional_non_following_and_direct?
blocked ||= optional_non_follower_emoji_reaction?
blocked ||= optional_non_following_emoji_reaction?
blocked ||= conversation_muted?
blocked ||= blocked_mention? if @notification.type == :mention
blocked

View file

@ -11,7 +11,7 @@ class SearchService < BaseService
@offset = options[:type].blank? ? 0 : options[:offset].to_i
@resolve = options[:resolve] || false
@following = options[:following] || false
@searchability = options[:searchability] || 'public'
@searchability = options[:searchability] || account&.user&.setting_default_searchability_of_search.to_s || 'public'
default_results.tap do |results|
next if @query.blank? || @limit.zero?

View file

@ -22,22 +22,6 @@ class EmojiReactionValidator < ActiveModel::Validator
end
def deny_emoji_reactions?(emoji_reaction)
return false unless Setting.enable_block_emoji_reaction_settings
return false if emoji_reaction.status.account.user.nil?
return false if emoji_reaction.status.account_id == emoji_reaction.account_id
deny_from_all?(emoji_reaction) || non_follower?(emoji_reaction) || non_following?(emoji_reaction)
end
def deny_from_all?(emoji_reaction)
emoji_reaction.status.account.user.settings['emoji_reactions.deny_from_all']
end
def non_following?(emoji_reaction)
emoji_reaction.status.account.user.settings['emoji_reactions.must_be_following'] && !emoji_reaction.status.account.following?(emoji_reaction.account)
end
def non_follower?(emoji_reaction)
emoji_reaction.status.account.user.settings['emoji_reactions.must_be_follower'] && !emoji_reaction.account.following?(emoji_reaction.status.account)
!emoji_reaction.status.account.allow_emoji_reaction?(emoji_reaction.account)
end
end

View file

@ -13,9 +13,6 @@
.fields-group
= f.input :post_hash_tags_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_hash_tags_max')
.fields-group
= f.input :enable_block_emoji_reaction_settings, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.enable_block_emoji_reaction_settings')
.fields-group
= f.input :hide_local_users_for_anonymous, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hide_local_users_for_anonymous')

View file

@ -29,6 +29,17 @@
.fields-group
= f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
%h4= t('admin.settings.discovery.emoji_reactions')
.fields-group
= f.input :enable_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false
.fields-group
= f.input :receive_other_servers_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true
.fields-group
= f.input :streaming_other_servers_emoji_reaction, as: :boolean, wrapper: :with_label, kmyblue: true
%h4= t('admin.settings.discovery.publish_statistics')
.fields-group

View file

@ -4,9 +4,6 @@
- content_for :heading_actions do
= link_to t('antennas.new.title'), new_antenna_path, class: 'button'
.flash-message.alert
%strong= t('antennas.beta')
- if @antennas.empty?
.muted-hint.center-text= t 'antennas.index.empty'
- else

View file

@ -39,6 +39,9 @@
.fields-group
= ff.input :'web.hide_recent_emojis', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_recent_emojis'), hint: false
- if Setting.enable_emoji_reaction
= ff.input :'web.enable_emoji_reaction', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_emoji_reaction'), hint: I18n.t('simple_form.hints.defaults.setting_enable_emoji_reaction')
= ff.input :'web.show_emoji_reaction_on_timeline', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_emoji_reaction_on_timeline')
.fields-group
= ff.input :'web.bookmark_category_needed', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_bookmark_category_needed'), hint: I18n.t('simple_form.hints.defaults.setting_bookmark_category_needed')

View file

@ -42,11 +42,8 @@
= ff.input :'interactions.must_be_follower', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_follower')
= ff.input :'interactions.must_be_following', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_following')
= ff.input :'interactions.must_be_following_dm', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_following_dm')
- if Setting.enable_block_emoji_reaction_settings
= ff.input :'emoji_reactions.must_be_follower', kmyblue: true, wrapper: :with_label, label: I18n.t('simple_form.labels.emoji_reactions.must_be_follower')
= ff.input :'emoji_reactions.must_be_following', kmyblue: true, wrapper: :with_label, label: I18n.t('simple_form.labels.emoji_reactions.must_be_following')
= ff.input :'emoji_reactions.deny_from_all', kmyblue: true, wrapper: :with_label, label: I18n.t('simple_form.labels.emoji_reactions.deny_from_all')
= f.simple_fields_for :settings, current_user.settings do |ff|
.fields-group
= ff.input :stop_emoji_reaction_streaming, as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stop_emoji_reaction_streaming'), hint: I18n.t('simple_form.hints.defaults.setting_stop_emoji_reaction_streaming')
- if Setting.enable_emoji_reaction
= f.simple_fields_for :settings, current_user.settings do |ff|
.fields-group
= ff.input :stop_emoji_reaction_streaming, as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stop_emoji_reaction_streaming'), hint: I18n.t('simple_form.hints.defaults.setting_stop_emoji_reaction_streaming')

View file

@ -14,33 +14,16 @@
%h4= t 'preferences.posting_defaults'
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: ->(visibility) { safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_privacy')
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_reblog_privacy, collection: Status.selectable_reblog_visibilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_reblog_privacy')
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability')
.fields-group.fields-row__column.fields-row__column-6
.fields-group.fields-row__column.fields-row__column-12
= ff.input :default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: ->(locale) { locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_language')
.fields-group
= ff.input :stay_privacy, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stay_privacy')
.fields-group
= ff.input :public_post_to_unlisted, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_public_post_to_unlisted'), hint: I18n.t('simple_form.hints.defaults.setting_public_post_to_unlisted')
.fields-group
= ff.input :disallow_unlisted_public_searchability, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_disallow_unlisted_public_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability')
.fields-group
= ff.input :default_sensitive, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'), hint: I18n.t('simple_form.hints.defaults.setting_default_sensitive')
.fields-group
= ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false
- if Setting.enable_emoji_reaction
.fields-row
.fields-group.fields-row__column.fields-row__column-12
= ff.input :emoji_reaction_policy, kmyblue: true, collection: ['allow', 'outside_only', 'followers_only', 'following_only', 'mutuals_only', 'block'], label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_emoji_reaction_policy_items.#{item}")]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', include_blank: false, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_emoji_reaction_policy'), hint: false, warning_hint: I18n.t('simple_form.hints.defaults.setting_emoji_reaction_policy')
- if @dtl_enabled

View file

@ -0,0 +1,49 @@
- content_for :page_title do
= t('settings.preferences')
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences'
= simple_form_for current_user, url: settings_preferences_reaching_path, html: { method: :put, id: 'edit_preferences' } do |f|
= render 'shared/error_messages', object: current_user
= f.simple_fields_for :settings, current_user.settings do |ff|
%h4= t 'preferences.visibility'
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: ->(visibility) { safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_privacy')
.fields-group.fields-row__column.fields-row__column-6
= ff.input :default_reblog_privacy, collection: Status.selectable_reblog_visibilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_reblog_privacy')
.fields-group
= ff.input :stay_privacy, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stay_privacy')
.fields-group
= ff.input :public_post_to_unlisted, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_public_post_to_unlisted'), hint: I18n.t('simple_form.hints.defaults.setting_public_post_to_unlisted')
.fields-group
= ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false
%h4= t 'preferences.searchability'
.fields-row
.fields-group.fields-row__column.fields-row__column-12
= ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability')
.fields-group
= ff.input :disallow_unlisted_public_searchability, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_disallow_unlisted_public_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability')
%h4= t 'preferences.search'
.fields-row
.fields-group.fields-row__column.fields-row__column-12
= ff.input :default_searchability_of_search, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_search_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability_of_search')
.fields-group
= ff.input :use_public_index, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_use_public_index')
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -6,12 +6,24 @@ class DeliveryEmojiReactionWorker
include Lockable
include AccountScope
def perform(payload_json, status_id, _my_account_id = nil)
status = Status.find(status_id.to_i)
def perform(payload_json, status_id, reacted_account_id)
return unless Setting.enable_emoji_reaction
status = Status.find(status_id)
reacted_account = Account.find(reacted_account_id)
if status.present?
scope_status(status).includes(:user).find_each do |account|
redis.publish("timeline:#{account.id}", payload_json) if (account.user.nil? || !account.user&.setting_stop_emoji_reaction_streaming) && redis.exists?("subscribed:timeline:#{account.id}")
scope = scope_status(status)
policy = status.account.emoji_reaction_policy
return if policy == :block
scope.select(:id).merge(policy_scope(status.account, policy)).includes(:user).find_each do |account|
next if account.user.present? && (account.user.setting_stop_emoji_reaction_streaming || !account.user.setting_enable_emoji_reaction)
next unless redis.exists?("subscribed:timeline:#{account.id}")
next if !reacted_account.local? && account.excluded_from_timeline_domains.include?(reacted_account.domain)
redis.publish("timeline:#{account.id}", payload_json)
end
end
@ -19,4 +31,21 @@ class DeliveryEmojiReactionWorker
rescue ActiveRecord::RecordNotFound
true
end
def policy_scope(account, policy)
case policy
when :block
Account.where(id: 0)
when :mutuals_only
account.mutuals.local.or(Account.where(id: account))
when :following_only
account.following.local.or(Account.where(id: account))
when :followers_only
account.followers.local.or(Account.where(id: account))
when :outside_only
account.followers.local.or(Account.where(id: account.following.local)).or(Account.where(id: account))
else
Account.local
end
end
end