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') }