diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index 8a14c67653705f..2a533a0c5f40ac 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -26,7 +26,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
-import { enableEmojiReaction , bookmarkCategoryNeeded, simpleTimelineMenu, me } from '../initial_state';
+import { enableEmojiReaction , bookmarkCategoryNeeded, simpleTimelineMenu, me, hideEmojiReactionUnavailableServer } from '../initial_state';
import { IconButton } from './icon_button';
@@ -453,6 +453,7 @@ class StatusActionBar extends ImmutablePureComponent {
);
+ const emojiReactionAvailableServer = !hideEmojiReactionUnavailableServer || status.get('emoji_reaction_available_server');
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'));
@@ -462,7 +463,7 @@ class StatusActionBar extends ImmutablePureComponent {
const emojiPickerButton = (
);
- const emojiPickerDropdown = enableEmojiReaction && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
+ const emojiPickerDropdown = enableEmojiReaction && emojiReactionAvailableServer && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
);
diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx
index 6f5ef7f3fe5be7..e6436aa5f37f69 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.jsx
+++ b/app/javascript/mastodon/features/status/components/action_bar.jsx
@@ -25,7 +25,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { IconButton } from '../../../components/icon_button';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
-import { enableEmojiReaction , bookmarkCategoryNeeded, me } from '../../../initial_state';
+import { enableEmojiReaction , bookmarkCategoryNeeded, me, hideEmojiReactionUnavailableServer } from '../../../initial_state';
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
const messages = defineMessages({
@@ -358,6 +358,7 @@ class ActionBar extends PureComponent {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
+ const emojiReactionAvailableServer = !hideEmojiReactionUnavailableServer || status.get('emoji_reaction_available_server');
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'));
@@ -367,7 +368,7 @@ class ActionBar extends PureComponent {
const emojiPickerButton = (
);
- const emojiPickerDropdown = enableEmojiReaction && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
+ const emojiPickerDropdown = enableEmojiReaction && emojiReactionAvailableServer && denyFromAll && (writtenByMe || (following && followed && mutual && outside)) && (
);
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx
index 656e59ca83b4dd..709ea8527ca153 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.jsx
+++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx
@@ -21,7 +21,7 @@ import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { SearchabilityIcon } from 'mastodon/components/searchability_icon';
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
-import { enableEmojiReaction } from 'mastodon/initial_state';
+import { enableEmojiReaction, hideEmojiReactionUnavailableServer } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar';
@@ -233,7 +233,8 @@ class DetailedStatus extends ImmutablePureComponent {
if (status.get('emoji_reactions')) {
const emojiReactions = status.get('emoji_reactions');
const emojiReactionPolicy = status.getIn(['account', 'other_settings', 'emoji_reaction_policy']) || 'allow';
- if (emojiReactions.size > 0 && enableEmojiReaction && emojiReactionPolicy !== 'block') {
+ const emojiReactionAvailableServer = !hideEmojiReactionUnavailableServer || status.get('emoji_reaction_available_server');
+ if (emojiReactions.size > 0 && enableEmojiReaction && emojiReactionAvailableServer && emojiReactionPolicy !== 'block') {
emojiReactionsBar = ;
}
}
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index f90b85525c9459..942e764725e391 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -66,6 +66,7 @@
* @property {boolean} enable_dtl_menu
* @property {boolean=} expand_spoilers
* @property {boolean} hide_blocking_quote
+ * @property {boolean} hide_emoji_reaction_unavailable_server
* @property {boolean} hide_recent_emojis
* @property {boolean} limited_federation_mode
* @property {string} locale
@@ -143,6 +144,7 @@ export const enableDtlMenu = getMeta('enable_dtl_menu');
export const expandSpoilers = getMeta('expand_spoilers');
export const forceSingleColumn = !getMeta('advanced_layout');
export const hideBlockingQuote = getMeta('hide_blocking_quote');
+export const hideEmojiReactionUnavailableServer = getMeta('hide_emoji_reaction_unavailable_server');
export const hideRecentEmojis = getMeta('hide_recent_emojis');
export const limitedFederationMode = getMeta('limited_federation_mode');
export const mascot = getMeta('mascot');
diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb
index dda0f97eeefc87..0b1a41bc25c200 100644
--- a/app/models/concerns/has_user_settings.rb
+++ b/app/models/concerns/has_user_settings.rb
@@ -259,6 +259,10 @@ def setting_lock_follow_from_bot
settings['lock_follow_from_bot']
end
+ def setting_hide_emoji_reaction_unavailable_server
+ settings['web.hide_emoji_reaction_unavailable_server']
+ end
+
def allows_report_emails?
settings['notification_emails.report']
end
diff --git a/app/models/instance_info.rb b/app/models/instance_info.rb
index 297134884f86c7..515ded4dca17cc 100644
--- a/app/models/instance_info.rb
+++ b/app/models/instance_info.rb
@@ -14,4 +14,36 @@
#
class InstanceInfo < ApplicationRecord
+ EMOJI_REACTION_AVAILABLE_SOFTWARES = %w(
+ misskey
+ calckey
+ cherrypick
+ meisskey
+ firefish
+ renedon
+ fedibird
+ kmyblue
+ pleroma
+ akkoma
+ ).freeze
+
+ def self.emoji_reaction_available?(domain)
+ return Setting.enable_emoji_reaction if domain.nil?
+
+ Rails.cache.fetch("emoji_reaction_available_domain:#{domain}") { fetch_emoji_reaction_available(domain) }
+ end
+
+ def self.fetch_emoji_reaction_available(domain)
+ return Setting.enable_emoji_reaction if domain.nil?
+
+ info = InstanceInfo.find_by(domain: domain)
+ return false if info.nil?
+
+ return true if EMOJI_REACTION_AVAILABLE_SOFTWARES.include?(info['software'])
+
+ features = info.data.dig('metadata', 'features')
+ return false if features.nil? || !features.is_a?(Array)
+
+ features.include?('emoji_reaction')
+ end
end
diff --git a/app/models/status.rb b/app/models/status.rb
index 5e6df5972f4b3d..25fcedb15616a9 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -525,6 +525,10 @@ def emoji_reaction_allows_map(status_ids, account_id)
Status.where(id: status_ids).pluck(:account_id).uniq.index_with { |a| Account.find_by(id: a).show_emoji_reaction?(my_account) }
end
+ def emoji_reaction_availables_map(domains)
+ domains.index_with { |d| InstanceInfo.emoji_reaction_available?(d) }
+ end
+
def reload_stale_associations!(cached_items)
account_ids = []
diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb
index 6c25b61367e2fb..3bfded889c81bb 100644
--- a/app/models/user_settings.rb
+++ b/app/models/user_settings.rb
@@ -71,6 +71,7 @@ class KeyError < Error; end
setting :show_quote_in_home, default: true
setting :show_quote_in_public, default: false
setting :hide_blocking_quote, default: true
+ setting :hide_emoji_reaction_unavailable_server, default: false
end
namespace :notification_emails do
diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb
index 35c9e3e3f32dba..d9453903ebfce8 100644
--- a/app/presenters/status_relationships_presenter.rb
+++ b/app/presenters/status_relationships_presenter.rb
@@ -4,7 +4,7 @@ class StatusRelationshipsPresenter
PINNABLE_VISIBILITIES = %w(public public_unlisted unlisted login private).freeze
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, :blocks_map, :domain_blocks_map,
- :bookmarks_map, :filters_map, :attributes_map, :emoji_reaction_allows_map
+ :bookmarks_map, :filters_map, :attributes_map, :emoji_reaction_allows_map, :emoji_reaction_availables_map
def initialize(statuses, current_account_id = nil, **options)
@current_account_id = current_account_id
@@ -19,6 +19,7 @@ def initialize(statuses, current_account_id = nil, **options)
@pins_map = {}
@filters_map = {}
@emoji_reaction_allows_map = nil
+ @emoji_reaction_availables_map = {}
else
statuses = statuses.compact
statuses += statuses.filter_map(&:quote)
@@ -35,6 +36,7 @@ def initialize(statuses, current_account_id = nil, **options)
@domain_blocks_map = Status.domain_blocks_map(statuses.filter_map { |status| status.account.domain }.uniq, current_account_id).merge(options[:domain_blocks_map] || {})
@pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {})
@emoji_reaction_allows_map = Status.emoji_reaction_allows_map(status_ids, current_account_id).merge(options[:emoji_reaction_allows_map] || {})
+ @emoji_reaction_availables_map = Status.emoji_reaction_availables_map(statuses.filter_map { |status| status.account.domain }.uniq).merge(options[:emoji_reaction_availables_map] || {})
@attributes_map = options[:attributes_map] || {}
end
end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 9dfc70b2f727fb..8b6e61a151253d 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -66,6 +66,7 @@ def meta
store[:show_quote_in_home] = object.current_account.user.setting_show_quote_in_home
store[:show_quote_in_public] = object.current_account.user.setting_show_quote_in_public
store[:hide_blocking_quote] = object.current_account.user.setting_hide_blocking_quote
+ store[:hide_emoji_reaction_unavailable_server] = object.current_account.user.setting_hide_emoji_reaction_unavailable_server
else
store[:auto_play_gif] = Setting.auto_play_gif
store[:display_media] = Setting.display_media
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index 1fcc939cdbd0e9..d6c4b5422830e1 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -6,7 +6,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
:sensitive, :spoiler_text, :visibility, :visibility_ex, :limited_scope, :language,
:uri, :url, :replies_count, :reblogs_count, :searchability, :markdown,
- :status_reference_ids, :status_references_count, :status_referred_by_count,
+ :status_reference_ids, :status_references_count, :status_referred_by_count, :emoji_reaction_available_server,
:favourites_count, :emoji_reactions, :emoji_reactions_count, :reactions, :edited_at
attribute :favourited, if: :current_user?
@@ -166,6 +166,14 @@ def show_emoji_reaction?
end
end
+ def emoji_reaction_available_server
+ if relationships
+ relationships.emoji_reaction_availables_map[object.account.domain] || false
+ else
+ InstanceInfo.emoji_reaction_available?(object.account.domain)
+ end
+ end
+
def reactions
emoji_reactions.tap do |rs|
rs.each do |emoji_reaction|
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index dccb66ff1dc0a2..2690c46748fac3 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -42,6 +42,7 @@
- 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')
+ = ff.input :'web.hide_emoji_reaction_unavailable_server', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_emoji_reaction_unavailable_server')
.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')
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index c6b00a43bec297..6f7df0bb840b2a 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -265,6 +265,7 @@ en:
outside_only: Followings or followers only
setting_expand_spoilers: Always expand posts marked with content warnings
setting_hide_blocking_quote: Hide posts which have a quote written by the user you are blocking
+ setting_hide_emoji_reaction_unavailable_server: Hide stamp button from unavailable server
setting_hide_followers_count: Hide followers count
setting_hide_following_count: Hide following count
setting_hide_network: Hide your social graph
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 410373860ed4ba..80b493b3a369f9 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -280,6 +280,7 @@ ja:
setting_enable_emoji_reaction: スタンプ機能を使用する
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
setting_hide_blocking_quote: ブロックしたユーザーの投稿を引用した投稿を隠す
+ setting_hide_emoji_reaction_unavailable_server: スタンプに対応していないと思われるサーバーの投稿からスタンプボタンを隠す
setting_hide_followers_count: フォロワー数を隠す
setting_hide_following_count: フォロー数を隠す
setting_hide_network: 繋がりを隠す
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 153d6db25c2242..4e10f3d316e425 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -460,6 +460,45 @@
end
end
+ describe '.emoji_reaction_availables_map' do
+ subject { described_class.emoji_reaction_availables_map(domains) }
+
+ let(:domains) { %w(features_available.com features_unavailable.com features_invalid.com features_nil.com no_info.com mastodon.com misskey.com) }
+
+ before do
+ Fabricate(:instance_info, domain: 'features_available.com', software: 'mastodon', data: { metadata: { features: ['emoji_reaction'] } })
+ Fabricate(:instance_info, domain: 'features_unavailable.com', software: 'mastodon', data: { metadata: { features: ['ohagi'] } })
+ Fabricate(:instance_info, domain: 'features_invalid.com', software: 'mastodon', data: { metadata: { features: 'good_for_ohagi' } })
+ Fabricate(:instance_info, domain: 'features_nil.com', software: 'mastodon', data: { metadata: { features: nil } })
+ Fabricate(:instance_info, domain: 'mastodon.com', software: 'mastodon')
+ Fabricate(:instance_info, domain: 'misskey.com', software: 'misskey')
+ end
+
+ it 'availables if features contains emoji_reaction' do
+ expect(subject['features_available.com']).to be true
+ end
+
+ it 'unavailables if features does not contain emoji_reaction' do
+ expect(subject['features_unavailable.com']).to be false
+ end
+
+ it 'unavailables if features is not valid' do
+ expect(subject['features_invalid.com']).to be false
+ end
+
+ it 'unavailables if features is nil' do
+ expect(subject['features_nil.com']).to be false
+ end
+
+ it 'unavailables if mastodon server' do
+ expect(subject['mastodon.com']).to be false
+ end
+
+ it 'availables if misskey server' do
+ expect(subject['misskey.com']).to be true
+ end
+ end
+
describe '.tagged_with' do
let(:tag_cats) { Fabricate(:tag, name: 'cats') }
let(:tag_dogs) { Fabricate(:tag, name: 'dogs') }