diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index efeb8e9929..790ea0adcc 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -6,26 +6,30 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:emoji_reactions' } before_action :require_user! before_action :set_status, only: [:update] + before_action :set_status_without_authorize, only: [:destroy] # For compatible with Fedibird API def update create_private end - # TODO: destroy emoji reaction api def destroy - # fav = current_account.favourites.find_by(status_id: params[:status_id]) + emoji = params[:emoji] - # if fav - # @status = fav.status - # UnfavouriteWorker.perform_async(current_account.id, @status.id) - # else - # @status = Status.find(params[:status_id]) - # authorize @status, :show? - # end + if emoji + shortcode, domain = emoji.split('@') + emoji_reaction = EmojiReaction.where(account_id: current_account.id).where(status_id: @status.id).where(name: shortcode) + .find { |reaction| domain == '' ? reaction.custom_emoji.nil? : reaction.custom_emoji&.domain == domain } - # render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }) - # rescue Mastodon::NotPermittedError + authorize @status, :show? if emoji_reaction.nil? + end + + UnEmojiReactWorker.perform_async(current_account.id, @status.id, emoji) + + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new( + [@status], current_account.id, emoji_reactions_map: { @status.id => false } + ) + rescue Mastodon::NotPermittedError not_found end @@ -37,9 +41,13 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController end def set_status - @status = Status.find(params[:status_id]) + set_status_without_authorize authorize @status, :show? rescue Mastodon::NotPermittedError not_found end + + def set_status_without_authorize + @status = Status.find(params[:status_id]) + end end diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 15b466141c..8669484602 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -206,7 +206,6 @@ export function unfavouriteFail(status, error) { export function emojiReact(status, emoji) { return function (dispatch, getState) { dispatch(emojiReactRequest(status, emoji)); - console.dir(emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native); api(getState).put(`/api/v1/statuses/${status.get('id')}/emoji_reactions/${emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native}`).then(function (response) { dispatch(importFetchedStatus(response.data)); @@ -221,7 +220,7 @@ export function unEmojiReact(status, emoji) { return (dispatch, getState) => { dispatch(unEmojiReactRequest(status, emoji)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreactions/${emoji.native}`).then(response => { + api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreaction`, { emoji }).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(unEmojiReactSuccess(status, emoji)); }).catch(error => { diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index ceda971feb..c3feb7efdb 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -511,7 +511,7 @@ class Status extends ImmutablePureComponent { let emojiReactionsBar = null; if (status.get('emoji_reactions')) { const emojiReactions = status.get('emoji_reactions'); - emojiReactionsBar = ; + emojiReactionsBar = ; } return ( diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx index d91525430d..7fd99bf312 100644 --- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -1,7 +1,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import emojify from '../features/emoji/emoji'; import classNames from 'classnames'; @@ -9,11 +9,25 @@ class EmojiReactionButton extends React.PureComponent { static propTypes = { name: PropTypes.string, + domain: PropTypes.string, url: PropTypes.string, staticUrl: PropTypes.string, count: PropTypes.number.isRequired, me: PropTypes.bool, - onClick: PropTypes.func, + status: PropTypes.map, + onEmojiReact: PropTypes.func, + onUnEmojiReact: PropTypes.func, + }; + + onClick = () => { + const { name, domain, me } = this.props; + + const nameParameter = domain ? `${name}@${domain}` : name; + if (me) { + if (this.props.onUnEmojiReact) this.props.onUnEmojiReact(nameParameter); + } else { + if (this.props.onEmojiReact) this.props.onEmojiReact(nameParameter); + } }; render () { @@ -34,7 +48,7 @@ class EmojiReactionButton extends React.PureComponent { }; return ( - @@ -48,11 +62,23 @@ class StatusEmojiReactionsBar extends React.PureComponent { static propTypes = { emojiReactions: ImmutablePropTypes.list.isRequired, - statusId: PropTypes.string, + status: ImmutablePropTypes.map, + onEmojiReact: PropTypes.func, + onUnEmojiReact: PropTypes.func, + }; + + onEmojiReact = (name) => { + if (!this.props.onEmojiReact) return; + this.props.onEmojiReact(this.props.status, name); + }; + + onUnEmojiReact = (name) => { + if (!this.props.onUnEmojiReact) return; + this.props.onUnEmojiReact(this.props.status, name); }; render () { - const { emojiReactions, statusId } = this.props; + const { emojiReactions } = this.props; const emojiButtons = Array.from(emojiReactions).map((emoji, index) => ( )); return ( diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 9220d0ad95..2353cb8222 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -8,6 +8,7 @@ import { favourite, unreblog, unfavourite, + emojiReact, } from '../../../actions/interactions'; import { hideStatus, @@ -62,7 +63,7 @@ const mapDispatchToProps = dispatch => ({ }, onEmojiReact (status, emoji) { - + dispatch(emojiReact(status, emoji)); }, onToggleHidden (status) { diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 02860357ce..dc87e9751f 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -63,6 +63,7 @@ class ActionBar extends React.PureComponent { onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, + onEmojiReact: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired, @@ -182,13 +183,7 @@ class ActionBar extends React.PureComponent { }; handleEmojiPick = (data) => { - /* - const { text } = this.props; - const position = this.autosuggestTextarea.textarea.selectionStart; - const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); - - this.props.onPickEmoji(position, data, needsSpace); - */ + this.props.onEmojiReact(this.props.status, data); }; render () { diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index fa67905ba0..bf77e0da75 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -191,7 +191,7 @@ class DetailedStatus extends ImmutablePureComponent { let emojiReactionsBar = null; if (status.get('emoji_reactions')) { const emojiReactions = status.get('emoji_reactions'); - emojiReactionsBar = ; + emojiReactionsBar = ; } if (status.get('application')) { diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index 546f60db80..34b88b0ec8 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -13,6 +13,7 @@ import { unfavourite, pin, unpin, + emojiReact, } from '../../../actions/interactions'; import { muteStatus, @@ -94,7 +95,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onEmojiReact (status, emoji) { - + dispatch(emojiReact(status, emoji)); }, onPin (status) { diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb index 4fbdec5bbf..f98acef303 100644 --- a/app/lib/potential_friendship_tracker.rb +++ b/app/lib/potential_friendship_tracker.rb @@ -6,7 +6,7 @@ class PotentialFriendshipTracker WEIGHTS = { reply: 1, - emoji_reaction: 5, + emoji_reaction: 2, favourite: 10, reblog: 20, }.freeze diff --git a/app/models/status.rb b/app/models/status.rb index dd9d2ccc6c..5e1606e2a1 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -369,6 +369,10 @@ class Status < ApplicationRecord StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true } end + def emoji_reactions_map(status_ids, account_id) + EmojiReaction.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |e, h| h[e.status_id] = true } + end + def reload_stale_associations!(cached_items) account_ids = [] diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index be818a2de7..6088dbcffb 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -2,28 +2,30 @@ class StatusRelationshipsPresenter attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, - :bookmarks_map, :filters_map + :bookmarks_map, :filters_map, :emoji_reactions_map def initialize(statuses, current_account_id = nil, **options) if current_account_id.nil? - @reblogs_map = {} - @favourites_map = {} - @bookmarks_map = {} - @mutes_map = {} - @pins_map = {} - @filters_map = {} + @reblogs_map = {} + @favourites_map = {} + @bookmarks_map = {} + @mutes_map = {} + @pins_map = {} + @filters_map = {} + @emoji_reactions_map = {} else statuses = statuses.compact status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact conversation_ids = statuses.filter_map(&:conversation_id).uniq pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) } - @filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {}) - @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) - @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {}) - @bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {}) - @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {}) - @pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {}) + @filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {}) + @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) + @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {}) + @bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {}) + @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {}) + @pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {}) + @emoji_reactions_map = Status.emoji_reactions_map(status_ids, current_account_id).merge(options[:emoji_reactions_map] || {}) end end diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index 617edd44cc..6a5e3167d9 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -49,7 +49,7 @@ class EmojiReactService < BaseService return unless status.account.local? - ActivityPub::RawDistributionWorker.perform_async(emoji_reaction, status.account_id) + ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), status.account_id) end def broadcast_updates!(emoji_reaction) diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb new file mode 100644 index 0000000000..24f7c83b26 --- /dev/null +++ b/app/services/un_emoji_react_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class UnEmojiReactService < BaseService + include Payloadable + + def call(account, status, emoji_reaction = nil) + if emoji_reaction + emoji_reaction.destroy! + create_notification(emoji_reaction) if !status.account.local? && status.account.activitypub? + notify_to_followers(emoji_reaction) if status.account.local? + else + bulk(account, status) + end + emoji_reaction + end + + private + + def bulk(account, status) + EmojiReaction.where(account: account).where(status: status).tap do |emoji_reaction| + call(account, status, emoji_reaction) + end + end + + def create_notification(emoji_reaction) + status = emoji_reaction.status + ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), status.account_id, status.account.inbox_url) + end + + def notify_to_followers(emoji_reaction) + status = emoji_reaction.status + ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), status.account_id) + end + + def build_json(emoji_reaction) + # TODO: change to original serializer for other servers + Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoLikeSerializer)) + end +end diff --git a/app/workers/un_emoji_react_worker.rb b/app/workers/un_emoji_react_worker.rb new file mode 100644 index 0000000000..c7c2671099 --- /dev/null +++ b/app/workers/un_emoji_react_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class UnEmojiReactWorker + include Sidekiq::Worker + + def perform(account_id, status_id, emoji = nil) + emoji_reaction = nil + + if emoji + shortcode, domain = emoji.split('@') + emoji_reaction = EmojiReaction.where(account_id: account_id).where(status_id: status_id).where(name: shortcode) + .find { |reaction| domain == '' ? reaction.custom_emoji.nil? : reaction.custom_emoji&.domain == domain } + end + + UnEmojiReactService.new.call(Account.find(account_id), Status.find(status_id), emoji_reaction) + rescue ActiveRecord::RecordNotFound + true + end +end