diff --git a/app/controllers/admin/media_attachments_controller.rb b/app/controllers/admin/media_attachments_controller.rb new file mode 100644 index 0000000000..9d6927a39d --- /dev/null +++ b/app/controllers/admin/media_attachments_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Admin + class MediaAttachmentsController < BaseController + def index + authorize :account, :index? + + @media_attachments = filtered_attachments.page(params[:page]) + @form = Form::MediaAttachmentsBatch.new + end + + private + + def filtered_attachments + MediaAttachment.local_attached.order(created_at: :desc) + end + end +end diff --git a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_slim_controller.rb b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_slim_controller.rb index 2220174527..ace2939291 100644 --- a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_slim_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_slim_controller.rb @@ -10,7 +10,10 @@ class Api::V1::Statuses::EmojiReactionedByAccountsSlimController < Api::BaseCont @accounts = load_emoji_reactions # TODO for serialize hash object - render json: @accounts, each_serializer: REST::EmojiReactedBySlimSerializer + #render json: @accounts, each_serializer: REST::EmojiReactedBySlimSerializer + + # Hide WIP api for hide account secret + not_found end private diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 7da531f6fc..7670ee8ac6 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -106,6 +106,7 @@ class Status extends ImmutablePureComponent { inUse: PropTypes.bool, available: PropTypes.bool, }), + withoutEmojiReactions: PropTypes.bool, }; // Avoid checking props that are functions (and whose equality will always @@ -512,7 +513,7 @@ class Status extends ImmutablePureComponent { const visibilityIcon = visibilityIconInfo[status.get('visibility')]; let emojiReactionsBar = null; - if (status.get('emoji_reactions')) { + if (!this.props.withoutEmojiReactions && status.get('emoji_reactions')) { const emojiReactions = status.get('emoji_reactions'); emojiReactionsBar = ; } diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index ba686d45a7..b328155f82 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -179,6 +179,7 @@ class Notification extends ImmutablePureComponent { cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} unread={this.props.unread} + withoutEmojiReactions={true} /> ); } @@ -209,6 +210,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> @@ -216,7 +218,6 @@ class Notification extends ImmutablePureComponent { } renderEmojiReaction (notification, link) { - console.dir(notification) const { intl, unread } = this.props; const emoji_reaction = notification.get('emoji_reaction'); @@ -243,6 +244,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> @@ -275,6 +277,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> @@ -312,6 +315,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> @@ -349,6 +353,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> @@ -392,6 +397,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index c007eb4b57..1adf1bf747 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -380,3 +380,14 @@ color: $gold-star; } } + +.batch-table__row--attention .media-attachments-table { + .image { + max-width: 80%; + max-height: 200px; + } + + .detailed-status__meta { + margin-top: 8px; + } +} diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 3bd8c0b895..0ebb28dc23 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -24,7 +24,7 @@ class CustomEmoji < ApplicationRecord include Attachmentable - LIMIT = 256.kilobytes + LIMIT = 512.kilobytes SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}' diff --git a/app/models/form/media_attachments_batch.rb b/app/models/form/media_attachments_batch.rb new file mode 100644 index 0000000000..9a2aa4cb15 --- /dev/null +++ b/app/models/form/media_attachments_batch.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Form::MediaAttachmentsBatch + include ActiveModel::Model + include Authorization + include AccountableConcern + include Payloadable + + attr_accessor :query + + def save + end +end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 08abd4e43b..f390d87aaf 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -33,6 +33,7 @@ class MediaAttachment < ApplicationRecord self.inheritance_column = nil include Attachmentable + include RoutingHelper enum type: { :image => 0, :gifv => 1, :video => 2, :unknown => 3, :audio => 4 } enum processing: { :queued => 0, :in_progress => 1, :complete => 2, :failed => 3 }, _prefix: true @@ -208,6 +209,7 @@ class MediaAttachment < ApplicationRecord scope :local, -> { where(remote_url: '') } scope :remote, -> { where.not(remote_url: '') } scope :cached, -> { remote.where.not(file_file_name: nil) } + scope :local_attached, -> { attached.where(remote_url: '') } default_scope { order(id: :asc) } @@ -271,6 +273,10 @@ class MediaAttachment < ApplicationRecord delay_processing? && attachment_name == :file end + def url + full_asset_url(file.url(:original)) + end + after_commit :enqueue_processing, on: :create after_commit :reset_parent_cache, on: :update diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index a07840f0ca..f36591939a 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -11,7 +11,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer attributes :domain, :title, :version, :source_url, :description, :usage, :thumbnail, :languages, :configuration, - :registrations + :registrations, :fedibird_capabilities has_one :contact, serializer: ContactSerializer has_many :rules, serializer: REST::RuleSerializer @@ -88,6 +88,17 @@ class REST::InstanceSerializer < ActiveModel::Serializer } end + # for third party apps + def fedibird_capabilities + capabilities = [ + :emoji_reaction, + ] + + capabilities << :profile_search unless Chewy.enabled? + + capabilities + end + private def registrations_enabled? diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 15c62476dc..f3235b3b11 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -6,7 +6,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer belongs_to :from_account, key: :account, serializer: REST::AccountSerializer belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer - belongs_to :emoji_reaction, if: :emoji_reaction_type?, serializer: REST::EmojiReactionSerializer + belongs_to :emoji_reaction, if: :emoji_reaction_type?, serializer: REST::NotifyEmojiReactionSerializer def id object.id.to_s diff --git a/app/serializers/rest/notify_emoji_reaction_serializer.rb b/app/serializers/rest/notify_emoji_reaction_serializer.rb new file mode 100644 index 0000000000..a78740d56c --- /dev/null +++ b/app/serializers/rest/notify_emoji_reaction_serializer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class REST::NotifyEmojiReactionSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name + + attribute :count, if: :count? + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + + def count? + object.respond_to?(:count) + end + + def custom_emoji? + object.respond_to?(:custom_emoji) && object.custom_emoji.present? + end + + def account_ids? + object.respond_to?(:account_ids) + end + + def url + full_asset_url(object.custom_emoji.image.url) + end + + def static_url + full_asset_url(object.custom_emoji.image.url(:static)) + end + + def domain + object.custom_emoji.domain + end +end diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index 99d1b2bd62..a96d2adf53 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -6,7 +6,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer attributes :uri, :title, :short_description, :description, :email, :version, :urls, :stats, :thumbnail, :languages, :registrations, :approval_required, :invites_enabled, - :configuration + :configuration, :fedibird_capabilities has_one :contact_account, serializer: REST::AccountSerializer @@ -98,6 +98,17 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer UserRole.everyone.can?(:invite_users) end + # for third party apps + def fedibird_capabilities + capabilities = [ + :emoji_reaction, + ] + + capabilities << :profile_search unless Chewy.enabled? + + capabilities + end + private def instance_presenter diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index b1e0833a56..035d5d5079 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -197,7 +197,7 @@ class DeleteAccountService < BaseService def purge_emoji_reactions! @account.emoji_reactions.in_batches do |reactions| reactions.each do |reaction| - reaction.status.refresh_emoji_reactions_grouped_by_name + reaction.status.refresh_emoji_reactions_grouped_by_name! end Chewy.strategy.current.update(StatusesIndex, reactions.pluck(:status_id)) if Chewy.enabled? reactions.delete_all diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb index d13b8fd2a9..5ff45af935 100644 --- a/app/services/un_emoji_react_service.rb +++ b/app/services/un_emoji_react_service.rb @@ -5,16 +5,16 @@ class UnEmojiReactService < BaseService include Payloadable def call(account_id, status_id, emoji_reaction = nil) - @account_id = account_id - @account = Account.find(account_id) - @status = Status.find(status_id) + @status = Status.find(status_id) if emoji_reaction - create_notification(emoji_reaction) if !@account.local? && @account.activitypub? - notify_to_followers(emoji_reaction) if @account.local? + emoji_reaction.destroy! + create_notification(emoji_reaction) if !@status.account.local? && @status.account.activitypub? + notify_to_followers(emoji_reaction) if @status.account.local? write_stream(emoji_reaction) else - bulk(@account, @status) + account = Account.find(account_id) + bulk(account, @status) end emoji_reaction end @@ -22,17 +22,17 @@ class UnEmojiReactService < BaseService private def bulk(account, status) - EmojiReaction.where(account: account).where(status: status).tap do |emoji_reaction| - call(account, status, emoji_reaction) + EmojiReaction.where(account: account).where(status: status).each do |emoji_reaction| + call(account.id, status.id, emoji_reaction) end end def create_notification(emoji_reaction) - ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), @account_id, @account.inbox_url) + ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, @status.account.inbox_url) end def notify_to_followers(emoji_reaction) - ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), @account_id) + ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), @status.account_id) end def write_stream(emoji_reaction) @@ -45,7 +45,7 @@ class UnEmojiReactService < BaseService emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => @status.id.to_s } emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji end - FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @status.id, @account_id) + FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @status.id, emoji_reaction.account_id) end def build_json(emoji_reaction) diff --git a/app/views/admin/media_attachments/_media_attachment.html.haml b/app/views/admin/media_attachments/_media_attachment.html.haml new file mode 100644 index 0000000000..d5794cae9e --- /dev/null +++ b/app/views/admin/media_attachments/_media_attachment.html.haml @@ -0,0 +1,30 @@ +.batch-table__row{ class: ['batch-table__row--attention'] } + .batch-table__row__content.batch-table__row__content--unpadded + %table.media-attachments-table + %tbody + %tr + %td + %img{ src: media_attachment.url, class: 'image' } + .detailed-status__meta + - if media_attachment.status.application + = media_attachment.status.application.name + · + = link_to ActivityPub::TagManager.instance.url_for(media_attachment.status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do + %time.formatted{ datetime: media_attachment.status.created_at.iso8601, title: l(media_attachment.status.created_at) }= l(media_attachment.status.created_at) + - if media_attachment.status.edited? + · + = link_to t('statuses.edited_at_html', date: content_tag(:time, l(media_attachment.status.edited_at), datetime: media_attachment.status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')), admin_account_status_path(status.account_id, status), class: 'detailed-status__datetime' + - if media_attachment.status.discarded? + · + %span.negative-hint= t('admin.statuses.deleted') + · + - if media_attachment.status.reblog? + = fa_icon('retweet fw') + = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account)) + - else + = fa_visibility_icon(media_attachment.status) + = t("statuses.visibilities.#{media_attachment.status.visibility}") + - if media_attachment.status.proper.sensitive? + · + = fa_icon('eye-slash fw') + = t('stream_entries.sensitive_content') diff --git a/app/views/admin/media_attachments/index.html.haml b/app/views/admin/media_attachments/index.html.haml new file mode 100644 index 0000000000..e248f883a8 --- /dev/null +++ b/app/views/admin/media_attachments/index.html.haml @@ -0,0 +1,30 @@ +- content_for :page_title do + = t('admin.media_attachments.title') + +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + += form_for(@form, url: admin_media_attachments_path) do |f| + = hidden_field_tag :page, params[:page] || 1 + = hidden_field_tag :select_all_matching, '0' + + - AccountFilter::KEYS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? + + .batch-table + .batch-table__toolbar + - if @media_attachments.total_count > @media_attachments.size + .batch-table__select-all + .not-selected.active + %span= t('generic.all_items_on_page_selected_html', count: @media_attachments.size) + %button{ type: 'button' }= t('generic.select_all_matching_items', count: @media_attachments.total_count) + .selected + %span= t('generic.all_matching_items_selected_html', count: @media_attachments.total_count) + %button{ type: 'button' }= t('generic.deselect') + .batch-table__body + - if @media_attachments.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'media_attachment', collection: @media_attachments, locals: { f: f } + += paginate @media_attachments diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 43aac5769f..ee9e32d253 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -97,6 +97,7 @@ Doorkeeper.configure do :push, :'admin:read', :'admin:read:accounts', + :'admin:read:media_attachments', :'admin:read:reports', :'admin:read:domain_allows', :'admin:read:domain_blocks', @@ -105,6 +106,7 @@ Doorkeeper.configure do :'admin:read:canonical_email_blocks', :'admin:write', :'admin:write:accounts', + :'admin:write:media_attachments', :'admin:write:reports', :'admin:write:domain_allows', :'admin:write:domain_blocks', diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f8ba7ce78..99f8f42e76 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -547,6 +547,8 @@ en: title: Create new IP rule no_ip_block_selected: No IP rules were changed as none were selected title: IP rules + media_attachments: + title: Media attachments relationships: title: "%{acct}'s relationships" relays: @@ -1328,6 +1330,10 @@ en: body: 'Your post was favourited by %{name}:' subject: "%{name} favourited your post" title: New favourite + emoji_reaction: + body: 'Your post was reacted with emoji by %{name}:' + subject: "%{name} reacted your post with emoji" + title: New emoji reaction follow: body: "%{name} is now following you!" subject: "%{name} is now following you" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 374fdaf84a..d503395a9c 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -536,6 +536,8 @@ ja: title: 新規IPルール no_ip_block_selected: 何も選択されていないためIPルールを変更しませんでした title: IPルール + media_attachments: + title: 投稿された画像 relationships: title: "%{acct} さんのフォロー・フォロワー" relays: @@ -1302,6 +1304,10 @@ ja: body: "%{name}さんにお気に入り登録された、あなたの投稿があります:" subject: "%{name}さんにお気に入りに登録されました" title: 新たなお気に入り登録 + emoji_reaction: + body: "%{name}さんに絵文字リアクションされた、あなたの投稿があります:" + subject: "%{name}さんに絵文字リアクションされました" + title: 新たな絵文字リアクション follow: body: "%{name}さんにフォローされています!" subject: "%{name}さんにフォローされています" diff --git a/config/navigation.rb b/config/navigation.rb index 30817d0252..758ea96d15 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -42,6 +42,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), nil, if: -> { current_user.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks) } do |s| s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_path, highlights_on: %r{/admin/reports}, if: -> { current_user.can?(:manage_reports) } s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes|/admin/users}, if: -> { current_user.can?(:manage_users) } + s.item :media_attachments, safe_join([fa_icon('picture-o fw'), t('admin.media_attachments.title')]), admin_media_attachments_path, highlights_on: %r{/admin/media_attachments}, if: -> { current_user.can?(:manage_users) } s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) } s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}, if: -> { current_user.can?(:manage_taxonomies) } s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_path(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.can?(:manage_federation) } diff --git a/config/routes.rb b/config/routes.rb index 51cf7f72e1..82ec5a5f4b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -257,6 +257,7 @@ Rails.application.routes.draw do resources :action_logs, only: [:index] resources :warning_presets, except: [:new] + resources :media_attachments, only: [:index] resources :announcements, except: [:show] do member do