diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index efeb8e992..790ea0adc 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 dde646c16..fb5be8a4e 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -216,7 +216,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)); @@ -231,7 +230,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 8af4b34ff..d2258c5a8 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -677,7 +677,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 c3c9c917c..57f5ebe02 100644 --- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -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 ( - @@ -47,11 +61,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 dc2e03f8a..4b3238b49 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -7,6 +7,7 @@ import { favourite, unreblog, unfavourite, + emojiReact, } from '../../../actions/interactions'; import { hideStatus, @@ -63,7 +64,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 898ca7ebf..201a3214b 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -83,6 +83,7 @@ class ActionBar extends PureComponent { onReblog: PropTypes.func.isRequired, onQuote: PropTypes.func, onFavourite: PropTypes.func.isRequired, + onEmojiReact: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired, @@ -206,13 +207,7 @@ class ActionBar extends 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 9e4ac8980..2cff3de86 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -260,7 +260,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 c6af91738..cd76f0251 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -17,6 +17,7 @@ import { unfavourite, pin, unpin, + emojiReact, } from '../../../actions/interactions'; import { openModal } from '../../../actions/modal'; import { initMuteModal } from '../../../actions/mutes'; @@ -101,7 +102,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 4fbdec5bb..f98acef30 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 d6acee868..5610ae6f4 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -388,6 +388,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 5d53040fb..2991c7748 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -4,16 +4,17 @@ class StatusRelationshipsPresenter PINNABLE_VISIBILITIES = %w(public unlisted private).freeze attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, - :bookmarks_map, :filters_map, :attributes_map + :bookmarks_map, :filters_map, :attributes_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 @@ -27,6 +28,7 @@ class StatusRelationshipsPresenter @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] || {}) @attributes_map = options[:attributes_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 617edd44c..6a5e3167d 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 000000000..24f7c83b2 --- /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 000000000..c7c267109 --- /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