From e443f3c90a4f96bc9f6c9ddac5663d6df41fdd5c Mon Sep 17 00:00:00 2001 From: KMY Date: Wed, 22 Feb 2023 16:48:37 +0900 Subject: [PATCH 01/39] Add emoji-reaction icons as an item of action bar --- .../mastodon/components/status_action_bar.jsx | 12 ++++++++++++ .../features/status/components/action_bar.jsx | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 91d29de6d..c6323ac9e 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; +import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container'; import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; @@ -263,6 +264,16 @@ class StatusActionBar extends ImmutablePureComponent { } } + 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); + */ + }; + render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.context.identity; @@ -408,6 +419,7 @@ class StatusActionBar extends ImmutablePureComponent { + {filterButton} diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 29ecdb9ff..898ca7ebf 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; +import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container'; import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; @@ -204,6 +205,16 @@ class ActionBar extends PureComponent { navigator.clipboard.writeText(url); }; + 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); + */ + }; + render () { const { status, relationship, intl } = this.props; const { signedIn, permissions } = this.context.identity; @@ -325,6 +336,7 @@ class ActionBar extends PureComponent {
+
From b88e3942cc796edc9836b81d2d5285454f1903a2 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 23 Feb 2023 09:14:38 +0900 Subject: [PATCH 02/39] Create emoji reactions table --- app/models/emoji_reaction.rb | 44 +++++++++++++++++++ .../20230222232121_create_emoji_reactions.rb | 13 ++++++ ...235218_add_create_at_to_emoji_reactions.rb | 6 +++ db/schema.rb | 15 +++++++ 4 files changed, 78 insertions(+) create mode 100644 app/models/emoji_reaction.rb create mode 100644 db/migrate/20230222232121_create_emoji_reactions.rb create mode 100644 db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb new file mode 100644 index 000000000..9786412fa --- /dev/null +++ b/app/models/emoji_reaction.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: emoji_reactions +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) not null +# status_id :bigint(8) not null +# name :string default(""), not null +# custom_emoji_id :bigint(8) +# uri :string +# created_at :datetime +# updated_at :datetime +# + +class EmojiReaction < ApplicationRecord + include Paginable + + update_index('statuses', :status) + + belongs_to :account, inverse_of: :emoji_reactions + belongs_to :status, inverse_of: :emoji_reactions + belongs_to :custom_emojis, optional: true + + has_one :notification, as: :activity, dependent: :destroy + + validates :status_id, uniqueness: { scope: :account_id } + + before_validation do + self.status = status.reblog if status&.reblog? + end + + after_destroy :invalidate_cleanup_info + + private + + def invalidate_cleanup_info + return unless status&.account_id == account_id && account.local? + + account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav) + end + end + diff --git a/db/migrate/20230222232121_create_emoji_reactions.rb b/db/migrate/20230222232121_create_emoji_reactions.rb new file mode 100644 index 000000000..803c48f8a --- /dev/null +++ b/db/migrate/20230222232121_create_emoji_reactions.rb @@ -0,0 +1,13 @@ +class CreateEmojiReactions < ActiveRecord::Migration[6.1] + def change + create_table :emoji_reactions do |t| + + t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }, index: false + t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade } + t.string :name, null: false, default: '' + t.belongs_to :custom_emoji, foreign_key: { on_delete: :cascade }, index: false + t.string :uri + + end + end +end diff --git a/db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb b/db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb new file mode 100644 index 000000000..f5530c0da --- /dev/null +++ b/db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb @@ -0,0 +1,6 @@ +class AddCreateAtToEmojiReactions < ActiveRecord::Migration[6.1] + def change + add_column :emoji_reactions, :created_at, :timestamp + add_column :emoji_reactions, :updated_at, :timestamp + end +end diff --git a/db/schema.rb b/db/schema.rb index 85b6f5842..7d5e4811d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,6 +11,7 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[7.1].define(version: 2023_12_12_073317) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -438,6 +439,17 @@ ActiveRecord::Schema[7.1].define(version: 2023_12_12_073317) do t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true end + create_table "emoji_reactions", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "status_id", null: false + t.string "name", default: "", null: false + t.bigint "custom_emoji_id" + t.string "uri" + t.datetime "created_at" + t.datetime "updated_at" + t.index ["status_id"], name: "index_emoji_reactions_on_status_id" + end + create_table "encrypted_messages", id: :bigint, default: -> { "timestamp_id('encrypted_messages'::text)" }, force: :cascade do |t| t.bigint "device_id" t.bigint "from_account_id" @@ -1214,6 +1226,9 @@ ActiveRecord::Schema[7.1].define(version: 2023_12_12_073317) do add_foreign_key "devices", "accounts", on_delete: :cascade add_foreign_key "devices", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade + add_foreign_key "emoji_reactions", "accounts", on_delete: :cascade + add_foreign_key "emoji_reactions", "custom_emojis", on_delete: :cascade + add_foreign_key "emoji_reactions", "statuses", on_delete: :cascade add_foreign_key "encrypted_messages", "accounts", column: "from_account_id", on_delete: :cascade add_foreign_key "encrypted_messages", "devices", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade From dca26cc504b2651f1c8fc41cc3923ee3d620bebe Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 23 Feb 2023 20:32:27 +0900 Subject: [PATCH 03/39] Add emoji_reactions property to status api object --- app/models/status.rb | 24 ++++++++ app/models/status_stat.rb | 11 ++++ ...ji_reactions_grouped_by_name_serializer.rb | 31 ++++++++++ app/serializers/rest/status_serializer.rb | 6 +- ...416_add_emoji_reactions_to_status_stats.rb | 5 ++ db/schema.rb | 6 ++ lib/mastodon/cache_cli.rb | 61 +++++++++++++++++++ 7 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb create mode 100644 db/migrate/20230223102416_add_emoji_reactions_to_status_stats.rb create mode 100644 lib/mastodon/cache_cli.rb diff --git a/app/models/status.rb b/app/models/status.rb index 96e47c6e6..d6acee868 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -65,6 +65,7 @@ class Status < ApplicationRecord belongs_to :quote, class_name: 'Status', inverse_of: :quoted, optional: true has_many :favourites, inverse_of: :status, dependent: :destroy + has_many :emoji_reactions, inverse_of: :status, dependent: :destroy has_many :bookmarks, inverse_of: :status, dependent: :destroy has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy has_many :reblogged_by_accounts, through: :reblogs, class_name: 'Account', source: :account @@ -323,6 +324,29 @@ class Status < ApplicationRecord update_status_stat!(key => [public_send(key) - 1, 0].max) end + def emoji_reactions_grouped_by_name(account = nil) + (Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions| + if account.present? + emoji_reactions.each do |emoji_reaction| + emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s) + emoji_reaction['count'] = emoji_reaction['account_ids'].size + emoji_reaction['account_ids'] -= account.excluded_from_timeline_account_ids.map(&:to_s) + end + end + end + end + + def generate_emoji_reactions_grouped_by_name + records = emoji_reactions.group(:name).order(Arel.sql('MIN(created_at) ASC')).select('name, min(custom_emoji_id) as custom_emoji_id, count(*) as count, array_agg(account_id::text order by created_at) as account_ids') + Oj.dump(ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::EmojiReactionsGroupedByNameSerializer, scope: nil, scope_name: :current_user)) + end + + def refresh_emoji_reactions_grouped_by_name! + generate_emoji_reactions_grouped_by_name.tap do |emoji_reactions| + update_status_stat!(emoji_reactions: emoji_reactions) + end + end + def trendable? if attributes['trendable'].nil? account.trendable? diff --git a/app/models/status_stat.rb b/app/models/status_stat.rb index 47aa14477..86dd3980e 100644 --- a/app/models/status_stat.rb +++ b/app/models/status_stat.rb @@ -9,6 +9,7 @@ # replies_count :bigint(8) default(0), not null # reblogs_count :bigint(8) default(0), not null # favourites_count :bigint(8) default(0), not null +# emoji_reactions :string # created_at :datetime not null # updated_at :datetime not null # @@ -27,4 +28,14 @@ class StatusStat < ApplicationRecord def favourites_count [attributes['favourites_count'], 0].max end + + def emoji_reactions + attributes['emoji_reactions'] || '' + end + + private + + def reset_parent_cache + Rails.cache.delete("statuses/#{status_id}") + end end diff --git a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb new file mode 100644 index 000000000..ac8054179 --- /dev/null +++ b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# name: string, +#count: number, +#account_ids: Array, +#me: boolean, +#url: string, +#domain: string + +class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer + attributes :name, :count + + attribute :me, if: :current_user? + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + attribute :account_ids, if: :has_account_ids? + + def current_user? + !current_user.nil? + end + + def custom_emoji? + object.respond_to?(:custom_emoji) + end + + def has_account_ids? + object.respond_to?(:account_ids) + end +end + \ No newline at end of file diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index ce49cb04f..504f8241b 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, :language, :uri, :url, :replies_count, :reblogs_count, - :favourites_count, :edited_at + :favourites_count, :emoji_reactions, :edited_at attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? @@ -103,6 +103,10 @@ class REST::StatusSerializer < ActiveModel::Serializer end end + def emoji_reactions + object.emoji_reactions_grouped_by_name(current_user&.account) + end + def reblogged if relationships relationships.reblogs_map[object.id] || false diff --git a/db/migrate/20230223102416_add_emoji_reactions_to_status_stats.rb b/db/migrate/20230223102416_add_emoji_reactions_to_status_stats.rb new file mode 100644 index 000000000..7f2ec68cc --- /dev/null +++ b/db/migrate/20230223102416_add_emoji_reactions_to_status_stats.rb @@ -0,0 +1,5 @@ +class AddEmojiReactionsToStatusStats < ActiveRecord::Migration[6.1] + def change + add_column :status_stats, :emoji_reactions, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 7d5e4811d..7df9f315e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -965,8 +965,14 @@ ActiveRecord::Schema[7.1].define(version: 2023_12_12_073317) do t.bigint "replies_count", default: 0, null: false t.bigint "reblogs_count", default: 0, null: false t.bigint "favourites_count", default: 0, null: false +<<<<<<< HEAD t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false +======= + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "emoji_reactions" +>>>>>>> 092f9916b0 (Add emoji_reactions property to status api object) t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true end diff --git a/lib/mastodon/cache_cli.rb b/lib/mastodon/cache_cli.rb new file mode 100644 index 000000000..71da96179 --- /dev/null +++ b/lib/mastodon/cache_cli.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require_relative '../../config/boot' +require_relative '../../config/environment' +require_relative 'cli_helper' + +module Mastodon + class CacheCLI < Thor + include CLIHelper + + def self.exit_on_failure? + true + end + + desc 'clear', 'Clear out the cache storage' + def clear + Rails.cache.clear + say('OK', :green) + end + + option :concurrency, type: :numeric, default: 5, aliases: [:c] + option :verbose, type: :boolean, aliases: [:v] + desc 'recount TYPE', 'Update hard-cached counters' + long_desc <<~LONG_DESC + Update hard-cached counters of TYPE by counting referenced + records from scratch. TYPE can be "accounts" or "statuses". + + It may take a very long time to finish, depending on the + size of the database. + LONG_DESC + def recount(type) + case type + when 'accounts' + processed, = parallelize_with_progress(Account.local.includes(:account_stat)) do |account| + account_stat = account.account_stat + account_stat.following_count = account.active_relationships.count + account_stat.followers_count = account.passive_relationships.count + account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count + + account_stat.save if account_stat.changed? + end + when 'statuses' + processed, = parallelize_with_progress(Status.includes(:status_stat)) do |status| + status_stat = status.status_stat + status_stat.replies_count = status.replies.where.not(visibility: :direct).count + status_stat.reblogs_count = status.reblogs.count + status_stat.favourites_count = status.favourites.count + status_stat.emoji_reactions = status.generate_emoji_reactions_grouped_by_name + + status_stat.save if status_stat.changed? + end + else + say("Unknown type: #{type}", :red) + exit(1) + end + + say + say("OK, recounted #{processed} records", :green) + end + end +end From 471540d9aa2c394c089cbfac0be61b54b0328648 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 24 Feb 2023 17:51:57 +0900 Subject: [PATCH 04/39] Add emoji reaction bar into status view --- .../mastodon/actions/importer/normalizer.js | 4 + app/javascript/mastodon/components/status.jsx | 11 +++ .../components/status_emoji_reactions_bar.jsx | 74 +++++++++++++++++++ .../status/components/detailed_status.jsx | 10 ++- .../styles/mastodon/components.scss | 32 ++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 app/javascript/mastodon/components/status_emoji_reactions_bar.jsx diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 0b1f024a4..f652f4c92 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -40,6 +40,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.filtered = status.filtered.map(normalizeFilterResult); } + if (status.emoji_reactions) { + normalStatus.emojiReactions = status.emoji_reactions; + } + // Only calculate these values when status first encountered and // when the underlying values change. Otherwise keep the ones // already in the reducer diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index c50f3395b..72ec4b993 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -1,5 +1,8 @@ import PropTypes from 'prop-types'; +import StatusEmojiReactionsBar from './status_emoji_reactions_bar'; +import AttachmentList from './attachment_list'; + import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; @@ -669,6 +672,12 @@ class Status extends ImmutablePureComponent { const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0; + let emojiReactionsBar = null; + if (status.get('emoji_reactions')) { + const emojiReactions = status.get('emoji_reactions'); + emojiReactionsBar = ; + } + return (
@@ -712,6 +721,8 @@ class Status extends ImmutablePureComponent { {expanded && hashtagBar} + {emojiReactionsBar} +
diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx new file mode 100644 index 000000000..d00833992 --- /dev/null +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { injectIntl } from 'react-intl'; +import emojify from '../features/emoji/emoji'; +import classNames from 'classnames'; + +class EmojiReactionButton extends React.PureComponent { + + static propTypes = { + name: ImmutablePropTypes.map, + url: PropTypes.string, + staticUrl: PropTypes.string, + count: PropTypes.number.isRequired, + me: PropTypes.bool, + onClick: PropTypes.func, + }; + + render () { + const { name, url, staticUrl, count, me } = this.props; + + let emojiHtml = null; + if (url) { + let customEmojis = {}; + customEmojis[name] = { url, static_url: staticUrl }; + emojiHtml = emojify(`:${name}:`, customEmojis); + } else { + emojiHtml = emojify(name); + } + + const classList = { + 'emoji-reactions-bar__button': true, + 'toggled': me, + }; + + return ( + + ); + } + +} + +class StatusEmojiReactionsBar extends React.PureComponent { + + static propTypes = { + emojiReactions: ImmutablePropTypes.map.isRequired, + statusId: PropTypes.string, + }; + + render () { + const { emojiReactions, statusId } = this.props; + + const emojiButtons = React.Children.map(emojiReactions, (emoji) => ( + )); + + return ( +
+ {emojiButtons} +
+ ); + } + +} +export default injectIntl(StatusEmojiReactionsBar); \ No newline at end of file diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index c81d83d71..9e4ac8980 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -6,6 +6,7 @@ import classNames from 'classnames'; import { Link, withRouter } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import StatusEmojiReactionsBar from '../../../components/status_emoji_reactions_bar'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { ReactComponent as AlternateEmailIcon } from '@material-symbols/svg-600/outlined/alternate_email.svg'; @@ -254,9 +255,14 @@ class DetailedStatus extends ImmutablePureComponent { } else if (status.get('spoiler_text').length === 0) { return ; } - } + let emojiReactionsBar = null; + if (status.get('emoji_reactions')) { + const emojiReactions = status.get('emoji_reactions'); + emojiReactionsBar = ; + } + if (status.get('application')) { applicationLink = <> · {status.getIn(['application', 'name'])}; } @@ -351,6 +357,8 @@ class DetailedStatus extends ImmutablePureComponent { {expanded && hashtagBar} + {emojiReactionsBar} +
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index db1d79a07..22df47cec 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1417,6 +1417,38 @@ body > [data-popper-placement] { color: $highlight-text-color; } } +.status__emoji-reactions-bar { + display: flex; + gap: 8px; + margin: 8px 0 2px 4px; + + .emoji-reactions-bar__button { + background: lighten($ui-base-color, 16%); + border: 0; + cursor: pointer; + display: flex; + justify-items: center; + align-items: center; + height: 20px; + + &.toggled { + background: darken($ui-primary-color, 16%); + } + + .emoji { + display: block; + height: 16px; + img { + margin-top: 0; + margin-bottom: 0; + } + } + .count { + display: block; + margin: 0 2px; + } + } +} .status__action-bar { display: flex; From 53e4c6adffa85c13d1c5cce40a0f8e6548f39ec8 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 24 Feb 2023 17:53:12 +0900 Subject: [PATCH 05/39] Fix ruby lint --- app/models/emoji_reaction.rb | 55 +++++++++---------- app/models/status_stat.rb | 2 +- ...ji_reactions_grouped_by_name_serializer.rb | 40 ++++++-------- .../20230222232121_create_emoji_reactions.rb | 8 +-- ...235218_add_create_at_to_emoji_reactions.rb | 6 -- db/schema.rb | 6 +- 6 files changed, 52 insertions(+), 65 deletions(-) delete mode 100644 db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index 9786412fa..30b83e61e 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -10,35 +10,34 @@ # name :string default(""), not null # custom_emoji_id :bigint(8) # uri :string -# created_at :datetime -# updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null # class EmojiReaction < ApplicationRecord - include Paginable - - update_index('statuses', :status) - - belongs_to :account, inverse_of: :emoji_reactions - belongs_to :status, inverse_of: :emoji_reactions - belongs_to :custom_emojis, optional: true - - has_one :notification, as: :activity, dependent: :destroy - - validates :status_id, uniqueness: { scope: :account_id } - - before_validation do - self.status = status.reblog if status&.reblog? - end - - after_destroy :invalidate_cleanup_info - - private - - def invalidate_cleanup_info - return unless status&.account_id == account_id && account.local? - - account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav) - end + include Paginable + + update_index('statuses', :status) + + belongs_to :account, inverse_of: :emoji_reactions + belongs_to :status, inverse_of: :emoji_reactions + belongs_to :custom_emojis, optional: true + + has_one :notification, as: :activity, dependent: :destroy + + after_create :refresh_cache + after_destroy :refresh_cache + after_destroy :invalidate_cleanup_info + + private + + def refresh_cache + status&.refresh_emoji_reactions_grouped_by_name! end - + + def invalidate_cleanup_info + return unless status&.account_id == account_id && account.local? + + account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav) + end +end diff --git a/app/models/status_stat.rb b/app/models/status_stat.rb index 86dd3980e..55e6a431b 100644 --- a/app/models/status_stat.rb +++ b/app/models/status_stat.rb @@ -9,9 +9,9 @@ # replies_count :bigint(8) default(0), not null # reblogs_count :bigint(8) default(0), not null # favourites_count :bigint(8) default(0), not null -# emoji_reactions :string # created_at :datetime not null # updated_at :datetime not null +# emoji_reactions :string # class StatusStat < ApplicationRecord diff --git a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb index ac8054179..c6d21cdaf 100644 --- a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb +++ b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb @@ -1,31 +1,23 @@ # frozen_string_literal: true -# name: string, -#count: number, -#account_ids: Array, -#me: boolean, -#url: string, -#domain: string - class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer - attributes :name, :count - - attribute :me, if: :current_user? - attribute :url, if: :custom_emoji? - attribute :static_url, if: :custom_emoji? - attribute :domain, if: :custom_emoji? - attribute :account_ids, if: :has_account_ids? + attributes :name, :count - def current_user? - !current_user.nil? - end + attribute :me, if: :current_user? + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + attribute :account_ids, if: :account_ids? - def custom_emoji? - object.respond_to?(:custom_emoji) - end + def current_user? + !current_user.nil? + end - def has_account_ids? - object.respond_to?(:account_ids) - end + def custom_emoji? + object.respond_to?(:custom_emoji) + end + + def account_ids? + object.respond_to?(:account_ids) + end end - \ No newline at end of file diff --git a/db/migrate/20230222232121_create_emoji_reactions.rb b/db/migrate/20230222232121_create_emoji_reactions.rb index 803c48f8a..7edd6f289 100644 --- a/db/migrate/20230222232121_create_emoji_reactions.rb +++ b/db/migrate/20230222232121_create_emoji_reactions.rb @@ -1,13 +1,13 @@ class CreateEmojiReactions < ActiveRecord::Migration[6.1] def change create_table :emoji_reactions do |t| - - t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }, index: false + t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade } t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade } t.string :name, null: false, default: '' - t.belongs_to :custom_emoji, foreign_key: { on_delete: :cascade }, index: false + t.belongs_to :custom_emoji, foreign_key: { on_delete: :cascade } t.string :uri - + t.datetime :created_at, null: false + t.datetime :updated_at, null: false end end end diff --git a/db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb b/db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb deleted file mode 100644 index f5530c0da..000000000 --- a/db/migrate/20230222235218_add_create_at_to_emoji_reactions.rb +++ /dev/null @@ -1,6 +0,0 @@ -class AddCreateAtToEmojiReactions < ActiveRecord::Migration[6.1] - def change - add_column :emoji_reactions, :created_at, :timestamp - add_column :emoji_reactions, :updated_at, :timestamp - end -end diff --git a/db/schema.rb b/db/schema.rb index 7df9f315e..4a8d0793a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -445,8 +445,10 @@ ActiveRecord::Schema[7.1].define(version: 2023_12_12_073317) do t.string "name", default: "", null: false t.bigint "custom_emoji_id" t.string "uri" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_emoji_reactions_on_account_id" + t.index ["custom_emoji_id"], name: "index_emoji_reactions_on_custom_emoji_id" t.index ["status_id"], name: "index_emoji_reactions_on_status_id" end From ba989d9db671bf92734680ea971c3441b16112ef Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 24 Feb 2023 20:57:01 +0900 Subject: [PATCH 06/39] Add api for set emoji reactions to toot --- .../v1/statuses/emoji_reactions_controller.rb | 45 +++++++++ .../mastodon/actions/interactions.js | 91 +++++++++++++++++++ app/javascript/mastodon/components/status.jsx | 2 + .../mastodon/components/status_action_bar.jsx | 21 +++-- .../components/status_emoji_reactions_bar.jsx | 10 +- .../mastodon/containers/status_container.jsx | 10 ++ .../containers/notification_container.js | 4 + .../containers/detailed_status_container.js | 4 + app/lib/potential_friendship_tracker.rb | 32 +++++++ app/models/concerns/account/associations.rb | 1 + app/models/custom_emoji.rb | 3 + app/models/emoji_reaction.rb | 2 +- app/models/notification.rb | 8 +- app/policies/status_policy.rb | 4 + ...ji_reactions_grouped_by_name_serializer.rb | 16 +++- app/services/emoji_react_service.rb | 72 +++++++++++++++ config/routes/api.rb | 3 + 17 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 app/controllers/api/v1/statuses/emoji_reactions_controller.rb create mode 100644 app/lib/potential_friendship_tracker.rb create mode 100644 app/services/emoji_react_service.rb diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb new file mode 100644 index 000000000..efeb8e992 --- /dev/null +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::EmojiReactionsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:emoji_reactions' } + before_action :require_user! + before_action :set_status, only: [:update] + + # 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]) + + # if fav + # @status = fav.status + # UnfavouriteWorker.perform_async(current_account.id, @status.id) + # else + # @status = Status.find(params[:status_id]) + # authorize @status, :show? + # end + + # render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }) + # rescue Mastodon::NotPermittedError + not_found + end + + private + + def create_private + EmojiReactService.new.call(current_account, @status, params[:id]) + render json: @status, serializer: REST::StatusSerializer + end + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end +end diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 7d0144438..dde646c16 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -15,6 +15,10 @@ export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST'; export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS'; export const FAVOURITE_FAIL = 'FAVOURITE_FAIL'; +export const EMOJIREACT_REQUEST = 'EMOJIREACT_REQUEST'; +export const EMOJIREACT_SUCCESS = 'EMOJIREACT_SUCCESS'; +export const EMOJIREACT_FAIL = 'EMOJIREACT_FAIL'; + export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; export const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; @@ -23,6 +27,10 @@ export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST'; export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS'; export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL'; +export const UNEMOJIREACT_REQUEST = 'UNEMOJIREACT_REQUEST'; +export const UNEMOJIREACT_SUCCESS = 'UNEMOJIREACT_SUCCESS'; +export const UNEMOJIREACT_FAIL = 'UNEMOJIREACT_FAIL'; + export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST'; export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS'; export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL'; @@ -205,6 +213,89 @@ 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)); + dispatch(emojiReactSuccess(status, emoji)); + }).catch(function (error) { + dispatch(emojiReactFail(status, emoji, error)); + }); + }; +} + +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 => { + dispatch(importFetchedStatus(response.data)); + dispatch(unEmojiReactSuccess(status, emoji)); + }).catch(error => { + dispatch(unEmojiReactFail(status, emoji, error)); + }); + }; +} + +export function emojiReactRequest(status, emoji) { + return { + type: EMOJIREACT_REQUEST, + status: status, + emoji: emoji, + skipLoading: true, + }; +} + +export function emojiReactSuccess(status, emoji) { + return { + type: EMOJIREACT_SUCCESS, + status: status, + emoji: emoji, + skipLoading: true, + }; +} + +export function emojiReactFail(status, emoji, error) { + return { + type: EMOJIREACT_FAIL, + status: status, + emoji: emoji, + error: error, + skipLoading: true, + }; +} + +export function unEmojiReactRequest(status, emoji) { + return { + type: UNEMOJIREACT_REQUEST, + status: status, + emoji: emoji, + skipLoading: true, + }; +} + +export function unEmojiReactSuccess(status, emoji) { + return { + type: UNEMOJIREACT_SUCCESS, + status: status, + emoji: emoji, + skipLoading: true, + }; +} + +export function unEmojiReactFail(status, emoji, error) { + return { + type: UNEMOJIREACT_FAIL, + status: status, + emoji: emoji, + error: error, + skipLoading: true, + }; +} + export function bookmark(status) { return function (dispatch, getState) { dispatch(bookmarkRequest(status)); diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 72ec4b993..8af4b34ff 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -167,6 +167,8 @@ class Status extends ImmutablePureComponent { onClick: PropTypes.func, onReply: PropTypes.func, onFavourite: PropTypes.func, + onEmojiReact: PropTypes.func, + onUnEmojiReact: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index c6323ac9e..eade4c462 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -87,6 +87,7 @@ class StatusActionBar extends ImmutablePureComponent { relationship: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, + onEmojiReact: PropTypes.func, onReblog: PropTypes.func, onQuote: PropTypes.func, onDelete: PropTypes.func, @@ -149,6 +150,16 @@ class StatusActionBar extends ImmutablePureComponent { } }; + handleEmojiPick = (data) => { + const { signedIn } = this.context.identity; + + if (signedIn) { + this.props.onEmojiReact(this.props.status, data); + } else { + this.props.onInteractionModal('favourite', this.props.status); + } + }; + handleReblogClick = e => { const { signedIn } = this.context.identity; @@ -264,16 +275,6 @@ class StatusActionBar extends ImmutablePureComponent { } } - 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); - */ - }; - render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.context.identity; diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx index d00833992..c3c9c917c 100644 --- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; class EmojiReactionButton extends React.PureComponent { static propTypes = { - name: ImmutablePropTypes.map, + name: PropTypes.string, url: PropTypes.string, staticUrl: PropTypes.string, count: PropTypes.number.isRequired, @@ -22,7 +22,7 @@ class EmojiReactionButton extends React.PureComponent { let emojiHtml = null; if (url) { let customEmojis = {}; - customEmojis[name] = { url, static_url: staticUrl }; + customEmojis[`:${name}:`] = { url, static_url: staticUrl }; emojiHtml = emojify(`:${name}:`, customEmojis); } else { emojiHtml = emojify(name); @@ -46,16 +46,16 @@ class EmojiReactionButton extends React.PureComponent { class StatusEmojiReactionsBar extends React.PureComponent { static propTypes = { - emojiReactions: ImmutablePropTypes.map.isRequired, + emojiReactions: ImmutablePropTypes.list.isRequired, statusId: PropTypes.string, }; render () { const { emojiReactions, statusId } = this.props; - const emojiButtons = React.Children.map(emojiReactions, (emoji) => ( + const emojiButtons = Array.from(emojiReactions).map((emoji, index) => ( ({ } }, + onEmojiReact (status, emoji) { + dispatch(emojiReact(status, emoji)); + }, + + onUnEmojiReact (status, emoji) { + dispatch(unEmojiReact(status, emoji)); + }, + onBookmark (status) { if (status.get('bookmarked')) { dispatch(unbookmark(status)); diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 4458fd7bc..dc2e03f8a 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -62,6 +62,10 @@ const mapDispatchToProps = dispatch => ({ } }, + onEmojiReact (status, emoji) { + + }, + onToggleHidden (status) { if (status.get('hidden')) { dispatch(revealStatus(status.get('id'))); 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 44491127f..c6af91738 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -100,6 +100,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onEmojiReact (status, emoji) { + + }, + onPin (status) { if (status.get('pinned')) { dispatch(unpin(status)); diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb new file mode 100644 index 000000000..4fbdec5bb --- /dev/null +++ b/app/lib/potential_friendship_tracker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class PotentialFriendshipTracker + EXPIRE_AFTER = 90.days.seconds + MAX_ITEMS = 80 + + WEIGHTS = { + reply: 1, + emoji_reaction: 5, + favourite: 10, + reblog: 20, + }.freeze + + class << self + include Redisable + + def record(account_id, target_account_id, action) + return if account_id == target_account_id + + key = "interactions:#{account_id}" + weight = WEIGHTS[action] + + redis.zincrby(key, weight, target_account_id) + redis.zremrangebyrank(key, 0, -MAX_ITEMS) + redis.expire(key, EXPIRE_AFTER) + end + + def remove(account_id, target_account_id) + redis.zrem("interactions:#{account_id}", target_account_id) + end + end +end diff --git a/app/models/concerns/account/associations.rb b/app/models/concerns/account/associations.rb index 2bb6fed5a..5c5ef3efa 100644 --- a/app/models/concerns/account/associations.rb +++ b/app/models/concerns/account/associations.rb @@ -13,6 +13,7 @@ module Account::Associations # Timelines has_many :statuses, inverse_of: :account, dependent: :destroy has_many :favourites, inverse_of: :account, dependent: :destroy + has_many :emoji_reactions, inverse_of: :account, dependent: :destroy has_many :bookmarks, inverse_of: :account, dependent: :destroy has_many :mentions, inverse_of: :account, dependent: :destroy has_many :notifications, inverse_of: :account, dependent: :destroy diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 97b1c63bf..eb5f268a5 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -37,6 +37,9 @@ class CustomEmoji < ApplicationRecord belongs_to :category, class_name: 'CustomEmojiCategory', optional: true + has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode + has_many :emoji_reactions, inverse_of: :custom_emoji, dependent: :destroy + has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode, inverse_of: false, dependent: nil has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' } }, validate_media_type: false diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index 30b83e61e..4dbeab194 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -21,7 +21,7 @@ class EmojiReaction < ApplicationRecord belongs_to :account, inverse_of: :emoji_reactions belongs_to :status, inverse_of: :emoji_reactions - belongs_to :custom_emojis, optional: true + belongs_to :custom_emoji, optional: true has_one :notification, as: :activity, dependent: :destroy diff --git a/app/models/notification.rb b/app/models/notification.rb index 54212d675..a1d4e0ec7 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -61,6 +61,8 @@ class Notification < ApplicationRecord belongs_to :follow, inverse_of: :notification belongs_to :follow_request, inverse_of: :notification belongs_to :favourite, inverse_of: :notification + belongs_to :emoji_reaction, inverse_of: :notification + belongs_to :poll, inverse_of: false belongs_to :report, inverse_of: false end @@ -81,6 +83,8 @@ class Notification < ApplicationRecord status&.reblog when :favourite favourite&.status + when :emoji_reaction + emoji_reaction&.status when :mention mention&.status when :poll @@ -130,6 +134,8 @@ class Notification < ApplicationRecord notification.status.reblog = cached_status when :favourite notification.favourite.status = cached_status + when :emoji_reaction + notification.emoji_reaction.status = cached_status when :mention notification.mention.status = cached_status when :poll @@ -150,7 +156,7 @@ class Notification < ApplicationRecord return unless new_record? case activity_type - when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report' + when 'Status', 'Follow', 'Favourite', 'EmojiReaction', 'FollowRequest', 'Poll', 'Report' self.from_account_id = activity&.account_id when 'Mention' self.from_account_id = activity&.status&.account_id diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 322d3aec5..990036565 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -27,6 +27,10 @@ class StatusPolicy < ApplicationPolicy show? && !blocking_author? end + def emoji_reaction? + show? && !blocking_author? + end + def destroy? owned? end diff --git a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb index c6d21cdaf..d4230de61 100644 --- a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb +++ b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer + include RoutingHelper + attributes :name, :count attribute :me, if: :current_user? @@ -14,10 +16,22 @@ class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer 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/services/emoji_react_service.rb b/app/services/emoji_react_service.rb new file mode 100644 index 000000000..617edd44c --- /dev/null +++ b/app/services/emoji_react_service.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class EmojiReactService < BaseService + include Authorization + include Payloadable + + # React a status with emoji and notify remote user + # @param [Account] account + # @param [Status] status + # @param [string] name + # @return [Favourite] + def call(account, status, name) + authorize_with account, status, :emoji_reaction? + + emoji_reaction = EmojiReaction.find_by(account: account, status: status, name: name) + + return emoji_reaction unless emoji_reaction.nil? + + shortcode, domain = name.split('@') + + custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain) + + emoji_reaction = EmojiReaction.create!(account: account, status: status, name: shortcode, custom_emoji: custom_emoji) + + Trends.statuses.register(status) + + create_notification(emoji_reaction) + notify_to_followers(emoji_reaction) + bump_potential_friendship(account, status) + + emoji_reaction + end + + private + + def create_notification(emoji_reaction) + status = emoji_reaction.status + + if status.account.local? + # TODO: Change favourite event to notify + LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'Favourite', 'favourite') + elsif status.account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url) + end + end + + def notify_to_followers(emoji_reaction) + status = emoji_reaction.status + + return unless status.account.local? + + ActivityPub::RawDistributionWorker.perform_async(emoji_reaction, status.account_id) + end + + def broadcast_updates!(emoji_reaction) + status = emoji_reaction.status + + DistributionWorker.perform_async(status.id, { 'update' => true }) + end + + def bump_potential_friendship(account, status) + ActivityTracker.increment('activity:interactions') + return if account.following?(status.account_id) + + PotentialFriendshipTracker.record(account.id, status.account_id, :emoji_reaction) + end + + def build_json(emoji_reaction) + # TODO: change to original serializer for other servers + Oj.dump(serialize_payload(emoji_reaction, ActivityPub::LikeSerializer)) + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb index 0fe9f69ab..29444e8d8 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -29,6 +29,9 @@ namespace :api, format: false do resource :source, only: :show post :translate, to: 'translations#create' + + resources :emoji_reactions, only: :update, constraints: { id: /[^\/]+/ } + post :emoji_unreaction, to: 'emoji_reactions#destroy' end member do From 374740952638f8a5371086390c2735f311e92585 Mon Sep 17 00:00:00 2001 From: KMY Date: Sat, 25 Feb 2023 10:42:19 +0900 Subject: [PATCH 07/39] Add emoji reaction deletion api --- .../v1/statuses/emoji_reactions_controller.rb | 32 +++++++++------ .../mastodon/actions/interactions.js | 3 +- app/javascript/mastodon/components/status.jsx | 2 +- .../components/status_emoji_reactions_bar.jsx | 37 ++++++++++++++++-- .../containers/notification_container.js | 3 +- .../features/status/components/action_bar.jsx | 9 +---- .../status/components/detailed_status.jsx | 2 +- .../containers/detailed_status_container.js | 3 +- app/lib/potential_friendship_tracker.rb | 2 +- app/models/status.rb | 4 ++ .../status_relationships_presenter.rb | 16 ++++---- app/services/emoji_react_service.rb | 2 +- app/services/un_emoji_react_service.rb | 39 +++++++++++++++++++ app/workers/un_emoji_react_worker.rb | 19 +++++++++ 14 files changed, 135 insertions(+), 38 deletions(-) create mode 100644 app/services/un_emoji_react_service.rb create mode 100644 app/workers/un_emoji_react_worker.rb 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 From 88e9b8d4fa88db4e41559ed63ff0cd3659cf69b1 Mon Sep 17 00:00:00 2001 From: KMY Date: Sat, 25 Feb 2023 11:14:16 +0900 Subject: [PATCH 08/39] Add emoji react api POST method (same PUT) --- .../api/v1/statuses/emoji_reactions_controller.rb | 12 ++++++++---- app/javascript/mastodon/actions/interactions.js | 2 +- config/routes/api.rb | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index 790ea0adc..2214b016f 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -5,12 +5,16 @@ 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, only: [:create, :update] before_action :set_status_without_authorize, only: [:destroy] + def create + create_private(params[:emoji]) + end + # For compatible with Fedibird API def update - create_private + create_private(params[:id]) end def destroy @@ -35,8 +39,8 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController private - def create_private - EmojiReactService.new.call(current_account, @status, params[:id]) + def create_private(emoji) + EmojiReactService.new.call(current_account, @status, emoji) render json: @status, serializer: REST::StatusSerializer end diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index fb5be8a4e..53e4384e8 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -217,7 +217,7 @@ export function emojiReact(status, emoji) { return function (dispatch, getState) { dispatch(emojiReactRequest(status, emoji)); - api(getState).put(`/api/v1/statuses/${status.get('id')}/emoji_reactions/${emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native}`).then(function (response) { + api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native }).then(function (response) { dispatch(importFetchedStatus(response.data)); dispatch(emojiReactSuccess(status, emoji)); }).catch(function (error) { diff --git a/config/routes/api.rb b/config/routes/api.rb index 29444e8d8..ca6ab1a29 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -30,7 +30,7 @@ namespace :api, format: false do post :translate, to: 'translations#create' - resources :emoji_reactions, only: :update, constraints: { id: /[^\/]+/ } + resources :emoji_reactions, only: [:create, :update], constraints: { id: /[^\/]+/ } post :emoji_unreaction, to: 'emoji_reactions#destroy' end From 5b9031f36e2e5fb9c2531eedf833cf8fd85e5bab Mon Sep 17 00:00:00 2001 From: KMY Date: Sat, 25 Feb 2023 11:17:04 +0900 Subject: [PATCH 09/39] Fix rubocop lint --- app/controllers/api/v1/statuses/emoji_reactions_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index 2214b016f..41523c945 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:emoji_reactions' } before_action :require_user! - before_action :set_status, only: [:create, :update] + before_action :set_status, only: %i(create update) before_action :set_status_without_authorize, only: [:destroy] def create From 432b68c5d187eb05dcb6ff3d6fe16719d7aa8582 Mon Sep 17 00:00:00 2001 From: KMY Date: Sun, 26 Feb 2023 09:45:22 +0900 Subject: [PATCH 10/39] Create new stream event type: emoji_reaction --- .../mastodon/actions/interactions.js | 6 ++-- .../mastodon/actions/notifications.js | 10 ++++++ app/javascript/mastodon/actions/statuses.js | 7 ++++ app/javascript/mastodon/actions/streaming.js | 7 +++- .../components/status_emoji_reactions_bar.jsx | 1 - app/javascript/mastodon/locales/en.json | 3 +- app/javascript/mastodon/locales/ja.json | 1 + app/javascript/mastodon/reducers/statuses.js | 27 ++++++++++++++ .../styles/mastodon/components.scss | 3 +- app/lib/inline_renderer.rb | 2 ++ .../activitypub/emoji_reaction_serializer.rb | 35 +++++++++++++++++++ .../undo_emoji_reaction_serializer.rb | 23 ++++++++++++ .../rest/emoji_reaction_serializer.rb | 18 ++++++++++ app/services/emoji_react_service.rb | 20 ++++++++--- app/services/un_emoji_react_service.rb | 25 +++++++++++-- 15 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 app/serializers/activitypub/emoji_reaction_serializer.rb create mode 100644 app/serializers/activitypub/undo_emoji_reaction_serializer.rb create mode 100644 app/serializers/rest/emoji_reaction_serializer.rb diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 53e4384e8..99921d80d 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -217,8 +217,7 @@ export function emojiReact(status, emoji) { return function (dispatch, getState) { dispatch(emojiReactRequest(status, emoji)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native }).then(function (response) { - dispatch(importFetchedStatus(response.data)); + api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native }).then(function () { dispatch(emojiReactSuccess(status, emoji)); }).catch(function (error) { dispatch(emojiReactFail(status, emoji, error)); @@ -230,8 +229,7 @@ export function unEmojiReact(status, emoji) { return (dispatch, getState) => { dispatch(unEmojiReactRequest(status, emoji)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreaction`, { emoji }).then(response => { - dispatch(importFetchedStatus(response.data)); + api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreaction`, { emoji }).then(() => { dispatch(unEmojiReactSuccess(status, emoji)); }).catch(error => { dispatch(unEmojiReactFail(status, emoji, error)); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index eafbf42d1..e914e6036 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -21,6 +21,7 @@ import { submitMarkers } from './markers'; import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; import { saveSettings } from './settings'; +import { STATUS_EMOJI_REACTION_UPDATE } from './statuses'; export * from "./notifications_typed"; @@ -61,6 +62,15 @@ export const loadPending = () => ({ type: NOTIFICATIONS_LOAD_PENDING, }); +export function updateEmojiReactions(emoji_reaction, accountId) { + return (dispatch) => + dispatch({ + type: STATUS_EMOJI_REACTION_UPDATE, + emoji_reaction, + accountId, + }); +} + export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index cc28df31f..70a57f227 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -42,6 +42,8 @@ export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS'; export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; +export const STATUS_EMOJI_REACTION_UPDATE = 'STATUS_EMOJI_REACTION_UPDATE'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -373,3 +375,8 @@ export function revealQuote(ids) { ids, }; }; + +export const updateEmojiReaction = (emoji_reaction) => ({ + type: STATUS_EMOJI_REACTION_UPDATE, + emoji_reaction, +}); \ No newline at end of file diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 9daeb3c60..5647050e0 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -10,7 +10,7 @@ import { deleteAnnouncement, } from './announcements'; import { updateConversations } from './conversations'; -import { updateNotifications, expandNotifications } from './notifications'; +import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -24,6 +24,8 @@ import { fillListTimelineGaps, } from './timelines'; +const { messages } = getLocale(); + /** * @param {number} max * @returns {number} @@ -102,6 +104,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti // @ts-expect-error dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; + case 'emoji_reaction': + dispatch(updateEmojiReactions(JSON.parse(data.payload), getState().getIn(['meta', 'me']))); + break; case 'conversation': // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx index 57f5ebe02..224eed615 100644 --- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -14,7 +14,6 @@ class EmojiReactionButton extends React.PureComponent { staticUrl: PropTypes.string, count: PropTypes.number.isRequired, me: PropTypes.bool, - status: PropTypes.map, onEmojiReact: PropTypes.func, onUnEmojiReact: PropTypes.func, }; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index b4b5886c0..2694dda3d 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -430,7 +430,8 @@ "not_signed_in_indicator.not_signed_in": "You need to login to access this resource.", "notification.admin.report": "{name} reported {target}", "notification.admin.sign_up": "{name} signed up", - "notification.favourite": "{name} favorited your post", + "notification.emoji_reaction": "{name} reacted your post with emoji", + "notification.favourite": "{name} favourited your post", "notification.follow": "{name} followed you", "notification.follow_request": "{name} has requested to follow you", "notification.mention": "{name} mentioned you", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 66b87322e..55895eef2 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -431,6 +431,7 @@ "not_signed_in_indicator.not_signed_in": "この機能を使うにはログインする必要があります。", "notification.admin.report": "{name}さんが{target}さんを通報しました", "notification.admin.sign_up": "{name}さんがサインアップしました", + "notification.emoji_reaction": "{name}さんがあなたの投稿に絵文字をつけました", "notification.favourite": "{name}さんがあなたのトゥートに╰( ^o^)╮-=ニ=一=三★しました", "notification.follow": "{name}さんにフォローされました", "notification.follow_request": "{name}さんがあなたにフォローリクエストしました", diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index e3cf12965..1436a6007 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -26,6 +26,7 @@ import { STATUS_TRANSLATE_UNDO, STATUS_FETCH_REQUEST, STATUS_FETCH_FAIL, + STATUS_EMOJI_REACTION_UPDATE, QUOTE_REVEAL, QUOTE_HIDE, } from '../actions/statuses'; @@ -65,6 +66,30 @@ const statusTranslateUndo = (state, id) => { }); }; +const updateStatusEmojiReaction = (state, emoji_reaction, myId) => { + emoji_reaction.me = emoji_reaction.account_ids ? emoji_reaction.account_ids.indexOf(myId) >= 0 : false; + + const status = state.get(emoji_reaction.status_id); + if (!status) return state; + + let emoji_reactions = Array.from(status.get('emoji_reactions') || []); + + if (emoji_reaction.count > 0) { + const old_emoji = emoji_reactions.find((er) => er.name === emoji_reaction.name && er.url === emoji_reaction.url); + if (old_emoji) { + old_emoji.account_ids = emoji_reaction.account_ids; + old_emoji.count = emoji_reaction.count; + old_emoji.me = emoji_reaction.me; + } else { + emoji_reactions.push(ImmutableMap(emoji_reaction)); + } + } else { + emoji_reactions = emoji_reactions.filter((er) => er.get('name') !== emoji_reaction.name || er.get('domain') !== emoji_reaction.domain); + } + + return state.setIn([emoji_reaction.status_id, 'emoji_reactions'], ImmutableList(emoji_reactions)); +}; + const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -137,6 +162,8 @@ export default function statuses(state = initialState, action) { return statusTranslateSuccess(state, action.id, action.translation); case STATUS_TRANSLATE_UNDO: return statusTranslateUndo(state, action.id); + case STATUS_EMOJI_REACTION_UPDATE: + return updateStatusEmojiReaction(state, action.emoji_reaction, action.accountId); default: return state; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 22df47cec..236e616e7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1419,7 +1419,8 @@ body > [data-popper-placement] { } .status__emoji-reactions-bar { display: flex; - gap: 8px; + flex-wrap: wrap; + gap: 4px 8px; margin: 8px 0 2px 4px; .emoji-reactions-bar__button { diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 0aebb13fc..fae713704 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -14,6 +14,8 @@ class InlineRenderer preload_associations_for_status when :notification serializer = REST::NotificationSerializer + when :emoji_reaction + serializer = REST::EmojiReactionSerializer when :conversation serializer = REST::ConversationSerializer when :announcement diff --git a/app/serializers/activitypub/emoji_reaction_serializer.rb b/app/serializers/activitypub/emoji_reaction_serializer.rb new file mode 100644 index 000000000..15017f645 --- /dev/null +++ b/app/serializers/activitypub/emoji_reaction_serializer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class ActivityPub::EmojiReactionSerializer < ActivityPub::Serializer + attributes :id, :type, :actor, :content + attribute :virtual_object, key: :object + + has_many :virtual_tags, key: :tag, unless: -> { object.custom_emoji.nil? } + + def id + [ActivityPub::TagManager.instance.uri_for(object.account), '#likes/', object.id].join + end + + def type + 'Like' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def virtual_object + ActivityPub::TagManager.instance.uri_for(object.status) + end + + def content + object.custom_emoji.nil? ? object.name : ":#{object.name}:" + end + + def virtual_tags + [object.custom_emoji] + end + + class CustomEmojiSerializer < ActivityPub::EmojiSerializer + end +end diff --git a/app/serializers/activitypub/undo_emoji_reaction_serializer.rb b/app/serializers/activitypub/undo_emoji_reaction_serializer.rb new file mode 100644 index 000000000..7bba3c88b --- /dev/null +++ b/app/serializers/activitypub/undo_emoji_reaction_serializer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ActivityPub::UndoEmojiReactionSerializer < ActivityPub::Serializer + attributes :id, :type, :actor, :content + + has_one :object, serializer: ActivityPub::EmojiReactionSerializer + + def id + [ActivityPub::TagManager.instance.uri_for(object.account), '#emoji_reactions/', object.id, '/undo'].join + end + + def type + 'Undo' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def content + object.custom_emoji.nil? ? object.name : ":#{object.name}:" + end +end diff --git a/app/serializers/rest/emoji_reaction_serializer.rb b/app/serializers/rest/emoji_reaction_serializer.rb new file mode 100644 index 000000000..1d8f90368 --- /dev/null +++ b/app/serializers/rest/emoji_reaction_serializer.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class REST::EmojiReactionSerializer < ActiveModel::Serializer + attributes :name, :count + + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + attribute :account_ids, if: :account_ids? + + def custom_emoji? + object.url.present? + end + + def account_ids? + object.respond_to?(:account_ids) + end +end diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index 6a5e3167d..0f45d9d86 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -3,6 +3,7 @@ class EmojiReactService < BaseService include Authorization include Payloadable + include Redisable # React a status with emoji and notify remote user # @param [Account] account @@ -27,6 +28,7 @@ class EmojiReactService < BaseService create_notification(emoji_reaction) notify_to_followers(emoji_reaction) bump_potential_friendship(account, status) + write_stream(emoji_reaction) emoji_reaction end @@ -37,8 +39,7 @@ class EmojiReactService < BaseService status = emoji_reaction.status if status.account.local? - # TODO: Change favourite event to notify - LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'Favourite', 'favourite') + LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'emoji_reaction') elsif status.account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url) end @@ -58,6 +59,13 @@ class EmojiReactService < BaseService DistributionWorker.perform_async(status.id, { 'update' => true }) end + def write_stream(emoji_reaction) + emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } + emoji_group['status_id'] = emoji_reaction.status_id.to_s + redis.publish("timeline:#{emoji_reaction.status.account_id}", render_emoji_reaction(emoji_group)) + end + def bump_potential_friendship(account, status) ActivityTracker.increment('activity:interactions') return if account.following?(status.account_id) @@ -66,7 +74,11 @@ class EmojiReactService < BaseService end def build_json(emoji_reaction) - # TODO: change to original serializer for other servers - Oj.dump(serialize_payload(emoji_reaction, ActivityPub::LikeSerializer)) + Oj.dump(serialize_payload(emoji_reaction, ActivityPub::EmojiReactionSerializer)) + end + + def render_emoji_reaction(emoji_group) + # @rendered_emoji_reaction ||= InlineRenderer.render(HashObject.new(emoji_group), nil, :emoji_reaction) + @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) end end diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb index 24f7c83b2..384eacefb 100644 --- a/app/services/un_emoji_react_service.rb +++ b/app/services/un_emoji_react_service.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true class UnEmojiReactService < BaseService + include Redisable include Payloadable def call(account, status, emoji_reaction = nil) if emoji_reaction - emoji_reaction.destroy! + 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) end @@ -32,8 +34,25 @@ class UnEmojiReactService < BaseService ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), status.account_id) end + def write_stream(emoji_reaction) + emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } + if emoji_group + emoji_group['status_id'] = emoji_reaction.status_id.to_s + else + # name: emoji_reaction.name, count: 0, domain: emoji_reaction.domain + emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => emoji_reaction.status_id.to_s } + emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji + end + redis.publish("timeline:#{emoji_reaction.status.account_id}", render_emoji_reaction(emoji_reaction, emoji_group)) + end + def build_json(emoji_reaction) - # TODO: change to original serializer for other servers - Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoLikeSerializer)) + Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoEmojiReactionSerializer)) + end + + def render_emoji_reaction(_emoji_reaction, emoji_group) + # @rendered_emoji_reaction ||= InlineRenderer.render(emoji_group, nil, :emoji_reaction) + Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) end end From 68c7def8d3646e1e334ea956fda9800615534eaa Mon Sep 17 00:00:00 2001 From: KMY Date: Sun, 26 Feb 2023 11:37:48 +0900 Subject: [PATCH 11/39] Add activitypub emoji_reaction receiving --- app/lib/activitypub/activity/like.rb | 90 ++++++++++++++++++++++++++-- app/lib/activitypub/activity/undo.rb | 55 ++++++++++++++++- app/services/emoji_react_service.rb | 6 -- 3 files changed, 139 insertions(+), 12 deletions(-) diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index aa1dc3040..db3cb91b3 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -2,13 +2,93 @@ class ActivityPub::Activity::Like < ActivityPub::Activity def perform - original_status = status_from_uri(object_uri) + @original_status = status_from_uri(object_uri) - return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status) + return if @original_status.nil? || !@original_status.account.local? || delete_arrived_first?(@json['id']) - favourite = original_status.favourites.create!(account: @account) + lock_or_fail("like:#{object_uri}") do + if shortcode.nil? + process_favourite + else + process_emoji_reaction + end + end + end - LocalNotificationWorker.perform_async(original_status.account_id, favourite.id, 'Favourite', 'favourite') - Trends.statuses.register(original_status) + private + + def process_favourite + return if @account.favourited?(@original_status) + + favourite = @original_status.favourites.create!(account: @account) + + LocalNotificationWorker.perform_async(@original_status.account_id, favourite.id, 'Favourite', 'favourite') + Trends.statuses.register(@original_status) + end + + def process_emoji_reaction + if emoji_tag.present? + return if emoji_tag['id'].blank? || emoji_tag['name'].blank? || emoji_tag['icon'].blank? || emoji_tag['icon']['url'].blank? + + image_url = emoji_tag['icon']['url'] + uri = emoji_tag['id'] + domain = URI.split(uri)[2] + + emoji = CustomEmoji.find_or_create_by!(shortcode: shortcode, domain: domain) do |emoji| + emoji.uri = uri + emoji.image_remote_url = image_url + end + end + + return if @account.reacted?(@original_status, shortcode, emoji) + + EmojiReaction.find_by(account: @account, status: @original_status)&.destroy + reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id']) + + if @original_status.account.local? + NotifyService.new.call(@original_status.account, :emoji_reaction, reaction) + forward_for_emoji_reaction + relay_for_emoji_reaction + end + rescue Seahorse::Client::NetworkingError + nil + end + + def forward_for_emoji_reaction + return unless @json['signature'].present? + + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), @original_status.account.id, [@account.preferred_inbox_url]) + end + + def relay_for_emoji_reaction + return unless @json['signature'].present? && @original_status.public_visibility? + + ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url| + [Oj.dump(@json), @original_status.account.id, inbox_url] + end + end + + def shortcode + return @shortcode if defined?(@shortcode) + + @shortcode = begin + if @json['_misskey_reaction'] == '⭐' + nil + else + @json['content']&.delete(':') + end + end + end + + def misskey_favourite? + misskey_shortcode = @json['_misskey_reaction']&.delete(':') + + return misskey_shortcode == shortcode && misskey_shortcode == '⭐' + end + + def emoji_tag + return @emoji_tag if defined?(@emoji_tag) + + @emoji_tag = @json['tag'].is_a?(Array) ? @json['tag']&.first : @json['tag'] end end diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index 9eff1b71c..b36ff0daf 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -100,7 +100,7 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity end end - def undo_like + def undo_like_original status = status_from_uri(target_uri) return if status.nil? || !status.account.local? @@ -113,6 +113,59 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity end end + def undo_like + @original_status = status_from_uri(target_uri) + + return if @original_status.nil? + + if shortcode.present? + emoji_tag = @object['tag'].is_a?(Array) ? @object['tag']&.first : @object['tag'] + + if emoji_tag.present? && emoji_tag['id'].present? + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain) + end + + if @account.reacted?(@original_status, shortcode, emoji) + @original_status.emoji_reactions.where(account: @account, name: shortcode, custom_emoji: emoji).first&.destroy + + if @original_status.account.local? + forward_for_undo_emoji_reaction + relay_for_undo_emoji_reaction + end + else + delete_later!(object_uri) + end + else + undo_like_original + end + end + + def forward_for_undo_emoji_reaction + return unless @json['signature'].present? + + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), @original_status.account.id, [@account.preferred_inbox_url]) + end + + def relay_for_undo_emoji_reaction + return unless @json['signature'].present? && @original_status.public_visibility? + + ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url| + [Oj.dump(@json), @original_status.account.id, inbox_url] + end + end + + def shortcode + return @shortcode if defined?(@shortcode) + + @shortcode = begin + if @object['_misskey_reaction'] == '⭐' + nil + else + @object['content']&.delete(':') + end + end + end + def undo_block target_account = account_from_uri(target_uri) diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index 0f45d9d86..2beffff33 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -53,12 +53,6 @@ class EmojiReactService < BaseService ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), status.account_id) end - def broadcast_updates!(emoji_reaction) - status = emoji_reaction.status - - DistributionWorker.perform_async(status.id, { 'update' => true }) - end - def write_stream(emoji_reaction) emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } From 546c2417e99ddfe7f7a909f81bfb7202c337b28b Mon Sep 17 00:00:00 2001 From: KMY Date: Sun, 26 Feb 2023 15:56:50 +0900 Subject: [PATCH 12/39] Change activitypub type Like to EmojiReact --- app/lib/activitypub/activity.rb | 2 +- app/lib/activitypub/activity/undo.rb | 2 +- app/lib/activitypub/tag_manager.rb | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 51384ef98..45ce7252f 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -37,7 +37,7 @@ class ActivityPub::Activity ActivityPub::Activity::Delete when 'Follow' ActivityPub::Activity::Follow - when 'Like' + when 'Like', 'EmojiReaction', 'EmojiReact' ActivityPub::Activity::Like when 'Block' ActivityPub::Activity::Block diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index b36ff0daf..09a7ac7a3 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -127,7 +127,7 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity if @account.reacted?(@original_status, shortcode, emoji) @original_status.emoji_reactions.where(account: @account, name: shortcode, custom_emoji: emoji).first&.destroy - + if @original_status.account.local? forward_for_undo_emoji_reaction relay_for_undo_emoji_reaction diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 864328631..0cd211d1f 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -45,8 +45,12 @@ class ActivityPub::TagManager account_status_url(target.account, target) when :emoji emoji_url(target) + when :flag target.uri + + when :emoji_reaction + emoji_reaction_url(target) end end From 90b9b5d8931de595b9e7e558605e7b6fc6e35bbc Mon Sep 17 00:00:00 2001 From: KMY Date: Sun, 26 Feb 2023 15:57:35 +0900 Subject: [PATCH 13/39] Fix streaming emoji reaction self only --- .../activitypub/emoji_reaction_serializer.rb | 2 +- app/services/concerns/account_limitable.rb | 38 +++++++++++++++++++ app/services/emoji_react_service.rb | 2 +- app/services/un_emoji_react_service.rb | 2 +- app/workers/feed_any_json_worker.rb | 25 ++++++++++++ 5 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 app/services/concerns/account_limitable.rb create mode 100644 app/workers/feed_any_json_worker.rb diff --git a/app/serializers/activitypub/emoji_reaction_serializer.rb b/app/serializers/activitypub/emoji_reaction_serializer.rb index 15017f645..01ff4e172 100644 --- a/app/serializers/activitypub/emoji_reaction_serializer.rb +++ b/app/serializers/activitypub/emoji_reaction_serializer.rb @@ -11,7 +11,7 @@ class ActivityPub::EmojiReactionSerializer < ActivityPub::Serializer end def type - 'Like' + 'EmojiReact' end def actor diff --git a/app/services/concerns/account_limitable.rb b/app/services/concerns/account_limitable.rb new file mode 100644 index 000000000..8a55d857c --- /dev/null +++ b/app/services/concerns/account_limitable.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module AccountLimitable + def scope_status(status) + case status.visibility.to_sym + when :public, :unlisted + #scope_local.merge(scope_list_following_account(status.account)) + scope_local + when :private + scope_account_local_followers(status.account) + when :limited + scope_status_mentioned(status) + else + scope_status_mentioned(status) + end + end + + def scope_local + Account.local.select(:id) + end + + def scope_account_local_followers(account) + account.followers_for_local_distribution.select(:id).reorder(nil) + end + + def scope_status_mentioned(status) + status.active_mentions.where.joins(:account).merge(Account.local).select('account_id AS id').reorder(nil) + end + + # TODO: not work + def scope_list_following_account(account) + account.lists_for_local_distribution.select(:id).reorder(nil) + end + + def scope_tag_following_account(status) + TagFollow.where(tag_id: @status.tags.map(&:id)).select('account_id AS id').reorder(nil) + end +end diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index 2beffff33..cf746bb0e 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -57,7 +57,7 @@ class EmojiReactService < BaseService emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } emoji_group['status_id'] = emoji_reaction.status_id.to_s - redis.publish("timeline:#{emoji_reaction.status.account_id}", render_emoji_reaction(emoji_group)) + FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id) end def bump_potential_friendship(account, status) diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb index 384eacefb..308b3f73d 100644 --- a/app/services/un_emoji_react_service.rb +++ b/app/services/un_emoji_react_service.rb @@ -44,7 +44,7 @@ class UnEmojiReactService < BaseService emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => emoji_reaction.status_id.to_s } emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji end - redis.publish("timeline:#{emoji_reaction.status.account_id}", render_emoji_reaction(emoji_reaction, emoji_group)) + FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id) end def build_json(emoji_reaction) diff --git a/app/workers/feed_any_json_worker.rb b/app/workers/feed_any_json_worker.rb new file mode 100644 index 000000000..0492c52f2 --- /dev/null +++ b/app/workers/feed_any_json_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class FeedAnyJsonWorker + include Sidekiq::Worker + include Redisable + include Lockable + include AccountLimitable + + def perform(payload_json, status_id, my_account_id = nil) + redis.publish("timeline:#{my_account_id}", payload_json) if my_account_id.present? + + status = Status.find(status_id.to_i) + + if status.present? + scope_status(status).find_each do |account_id| + p account_id if redis.exists?("subscribed:timeline:#{account_id}") + redis.publish("timeline:#{account_id}", payload_json) if redis.exists?("subscribed:timeline:#{account_id}") + end + end + + true + rescue ActiveRecord::RecordNotFound + true + end +end From 0ea43929b71ca6965c6e282bb49b72f9706492a6 Mon Sep 17 00:00:00 2001 From: KMY Date: Sun, 26 Feb 2023 18:09:25 +0900 Subject: [PATCH 14/39] Add emoji reaction notification support --- .../mastodon/actions/interactions.js | 4 ++- .../mastodon/actions/notifications.js | 1 + .../components/column_settings.jsx | 11 ++++++ .../notifications/components/filter_bar.jsx | 7 ++++ .../notifications/components/notification.jsx | 35 +++++++++++++++++++ app/javascript/mastodon/reducers/statuses.js | 7 ++-- app/models/notification.rb | 3 ++ .../rest/notification_serializer.rb | 2 +- app/services/un_emoji_react_service.rb | 30 +++++++++------- app/workers/feed_any_json_worker.rb | 21 +++++++++-- app/workers/un_emoji_react_worker.rb | 2 +- 11 files changed, 100 insertions(+), 23 deletions(-) diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 99921d80d..a578cef03 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -217,7 +217,9 @@ export function emojiReact(status, emoji) { return function (dispatch, getState) { dispatch(emojiReactRequest(status, emoji)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native }).then(function () { + const api_emoji = typeof emoji !== 'string' ? (emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native) : emoji; + + api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: api_emoji }).then(function () { dispatch(emojiReactSuccess(status, emoji)); }).catch(function (error) { dispatch(emojiReactFail(status, emoji, error)); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index e914e6036..2c63a10e5 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -140,6 +140,7 @@ const excludeTypesFromFilter = filter => { 'follow', 'follow_request', 'favourite', + 'emoji_reaction', 'reblog', 'mention', 'poll', diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index 09154f257..c9b990e62 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -119,6 +119,17 @@ export default class ColumnSettings extends PureComponent {
+
+ + +
+ + {showPushSettings && } + + +
+
+
diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx index 84bd4791c..7f9acef1c 100644 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx @@ -82,6 +82,13 @@ class FilterBar extends PureComponent { > +
- {!minimal && ( -
- {buttons} -
- )} +
+ {children} +
+ +
+ {buttons} +
{withBio && (account.get('note').length > 0 ? ( diff --git a/app/javascript/mastodon/components/emoji_view.jsx b/app/javascript/mastodon/components/emoji_view.jsx new file mode 100644 index 000000000..ac6e92a2e --- /dev/null +++ b/app/javascript/mastodon/components/emoji_view.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { injectIntl } from 'react-intl'; +import emojify from '../features/emoji/emoji'; +import classNames from 'classnames'; + +export default class EmojiView extends React.PureComponent { + + static propTypes = { + name: PropTypes.string, + url: PropTypes.string, + staticUrl: PropTypes.string, + }; + + render () { + const { name, url, staticUrl } = this.props; + + let emojiHtml = null; + if (url) { + let customEmojis = {}; + customEmojis[`:${name}:`] = { url, static_url: staticUrl }; + emojiHtml = emojify(`:${name}:`, customEmojis); + } else { + emojiHtml = emojify(name); + } + + return ( + + ); + } + +} diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx index 224eed615..ce0741c0c 100644 --- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { injectIntl } from 'react-intl'; import emojify from '../features/emoji/emoji'; import classNames from 'classnames'; +import EmojiView from './emoji_view'; class EmojiReactionButton extends React.PureComponent { @@ -32,15 +33,6 @@ class EmojiReactionButton extends React.PureComponent { render () { const { name, url, staticUrl, count, me } = this.props; - let emojiHtml = null; - if (url) { - let customEmojis = {}; - customEmojis[`:${name}:`] = { url, static_url: staticUrl }; - emojiHtml = emojify(`:${name}:`, customEmojis); - } else { - emojiHtml = emojify(name); - } - const classList = { 'emoji-reactions-bar__button': true, 'toggled': me, @@ -48,7 +40,9 @@ class EmojiReactionButton extends React.PureComponent { return ( ); diff --git a/app/javascript/mastodon/features/emoji_reactions/index.jsx b/app/javascript/mastodon/features/emoji_reactions/index.jsx new file mode 100644 index 000000000..4f5c3987f --- /dev/null +++ b/app/javascript/mastodon/features/emoji_reactions/index.jsx @@ -0,0 +1,121 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { ReactComponent as RefreshIcon } from '@material-symbols/svg-600/outlined/refresh.svg'; +import { debounce } from 'lodash'; + +import { fetchEmojiReactions, expandEmojiReactions } from 'mastodon/actions/interactions'; +import ColumnHeader from 'mastodon/components/column_header'; +import { Icon } from 'mastodon/components/icon'; +import ScrollableList from 'mastodon/components/scrollable_list'; +import AccountContainer from 'mastodon/containers/account_container'; +import Column from 'mastodon/features/ui/components/column'; + + + +import EmojiView from '../../components/emoji_view'; +import { LoadingIndicator } from '../../components/loading_indicator'; + +const messages = defineMessages({ + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, +}); + +const mapStateToProps = (state, props) => { + return { + accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'items']), + hasMore: !!state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'next']), + isLoading: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'isLoading'], true), + }; +}; + +class EmojiReactions extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + multiColumn: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + componentWillMount () { + if (!this.props.accountIds) { + this.props.dispatch(fetchEmojiReactions(this.props.params.statusId)); + } + } + + handleRefresh = () => { + this.props.dispatch(fetchEmojiReactions(this.props.params.statusId)); + }; + + handleLoadMore = debounce(() => { + this.props.dispatch(expandEmojiReactions(this.props.params.statusId)); + }, 300, { leading: true }); + + render () { + const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props; + + if (!accountIds) { + return ( + + + + ); + } + + let groups = {}; + for (const emoji_reaction of accountIds) { + const key = emoji_reaction.account_id; + const value = emoji_reaction; + if (!groups[key]) groups[key] = [value]; + else groups[key].push(value); + } + + const emptyMessage = ; + + return ( + + + )} + /> + + + {Object.keys(groups).map((key) =>( + +
+ {groups[key].map((value, index2) => )} +
+
+ ))} +
+ + + + +
+ ); + } + +} + +export default connect(mapStateToProps)(injectIntl(EmojiReactions)); \ No newline at end of file diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 2cff3de86..b4dd18213 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -55,6 +55,8 @@ class DetailedStatus extends ImmutablePureComponent { onToggleMediaVisibility: PropTypes.func, onQuoteToggleMediaVisibility: PropTypes.func, ...WithRouterPropTypes, + onEmojiReact: PropTypes.func, + onUnEmojiReact: PropTypes.func, }; state = { @@ -260,7 +262,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 cd76f0251..a472a085c 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -18,6 +18,7 @@ import { pin, unpin, emojiReact, + unEmojiReact, } from '../../../actions/interactions'; import { openModal } from '../../../actions/modal'; import { initMuteModal } from '../../../actions/mutes'; @@ -105,6 +106,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(emojiReact(status, emoji)); }, + onUnEmojiReact (status, emoji) { + dispatch(unEmojiReact(status, emoji)); + }, + onPin (status) { if (status.get('pinned')) { dispatch(unpin(status)); diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 9e82a3dcc..7847c98ca 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -41,6 +41,8 @@ import { import { favourite, unfavourite, + emojiReact, + unEmojiReact, bookmark, unbookmark, reblog, @@ -279,6 +281,16 @@ class Status extends ImmutablePureComponent { } }; + handleEmojiReact = (status, emoji) => { + const { dispatch } = this.props; + dispatch(emojiReact(status, emoji)); + }; + + handleUnEmojiReact = (status, emoji) => { + const { dispatch } = this.props; + dispatch(unEmojiReact(status, emoji)); + }; + handlePin = (status) => { if (status.get('pinned')) { this.props.dispatch(unpin(status)); @@ -746,6 +758,8 @@ class Status extends ImmutablePureComponent { onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility} pictureInPicture={pictureInPicture} + onEmojiReact={this.handleEmojiReact} + onUnEmojiReact={this.handleUnEmojiReact} /> + @@ -224,6 +226,7 @@ class SwitchingColumnsArea extends PureComponent { + {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 7b968204b..2dba7de7d 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -82,6 +82,10 @@ export function Favourites () { return import(/* webpackChunkName: "features/favourites" */'../../favourites'); } +export function EmojiReactions () { + return import(/* webpackChunkName: "features/favourites" */'../../emoji_reactions'); +} + export function FollowRequests () { return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); } diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 2f17fed5f..ad626c1b2 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -1,5 +1,4 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; - import { DIRECTORY_FETCH_REQUEST, DIRECTORY_FETCH_SUCCESS, @@ -57,6 +56,7 @@ import { FAVOURITES_EXPAND_REQUEST, FAVOURITES_EXPAND_SUCCESS, FAVOURITES_EXPAND_FAIL, + EMOJI_REACTIONS_FETCH_SUCCESS, } from '../actions/interactions'; import { MUTES_FETCH_REQUEST, @@ -79,6 +79,7 @@ const initialState = ImmutableMap({ following: initialListState, reblogged_by: initialListState, favourited_by: initialListState, + emoji_reactioned_by: initialListState, follow_requests: initialListState, blocks: initialListState, mutes: initialListState, @@ -161,6 +162,10 @@ export default function userLists(state = initialState, action) { return state.setIn(['favourited_by', action.id, 'isLoading'], false); case notificationsUpdate.type: return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state; + case EMOJI_REACTIONS_FETCH_SUCCESS: + console.log('===================') + console.dir(state); + return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts)); case FOLLOW_REQUESTS_FETCH_SUCCESS: return normalizeList(state, ['follow_requests'], action.accounts, action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index db3cb91b3..df8cea913 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -42,8 +42,11 @@ class ActivityPub::Activity::Like < ActivityPub::Activity return if @account.reacted?(@original_status, shortcode, emoji) + return if EmojiReaction.where(account: @account, status: @original_status).count >= BaseController::DEFAULT_EMOJI_REACTION_LIMIT + EmojiReaction.find_by(account: @account, status: @original_status)&.destroy reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id']) + write_stream(reaction) if @original_status.account.local? NotifyService.new.call(@original_status.account, :emoji_reaction, reaction) @@ -91,4 +94,15 @@ class ActivityPub::Activity::Like < ActivityPub::Activity @emoji_tag = @json['tag'].is_a?(Array) ? @json['tag']&.first : @json['tag'] end + + def write_stream(emoji_reaction) + emoji_group = @original_status.emoji_reactions_grouped_by_name + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } + emoji_group['status_id'] = @original_status.id.to_s + FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id) + end + + def render_emoji_reaction(emoji_group) + @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + end end diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index 09a7ac7a3..a8558f457 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -140,6 +140,23 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity end end + def write_stream(emoji_reaction) + emoji_group = @original_status.emoji_reactions_grouped_by_name + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } + if emoji_group + emoji_group['status_id'] = @original_status.id.to_s + else + # name: emoji_reaction.name, count: 0, domain: emoji_reaction.domain + 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), @original_status.id, emoji_reaction.account_id) + end + + def render_emoji_reaction(emoji_group) + @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + end + def forward_for_undo_emoji_reaction return unless @json['signature'].present? diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index 4dbeab194..b66751ffe 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -40,4 +40,11 @@ class EmojiReaction < ApplicationRecord account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav) end + + def paginate_by_max_id(limit, max_id = nil, since_id = nil) + query = order(arel_table[:id].desc).limit(limit) + query = query.where(arel_table[:id].lt(max_id)) if max_id.present? + query = query.where(arel_table[:id].gt(since_id)) if since_id.present? + query + end end diff --git a/app/serializers/rest/emoji_reaction_account_serializer.rb b/app/serializers/rest/emoji_reaction_account_serializer.rb new file mode 100644 index 000000000..192575407 --- /dev/null +++ b/app/serializers/rest/emoji_reaction_account_serializer.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class REST::EmojiReactionAccountSerializer < ActiveModel::Serializer + include RoutingHelper + include FormattingHelper + + attributes :id, :name + + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + + belongs_to :account, serializer: REST::AccountSerializer + + def id + object.id.to_s + 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 + + def custom_emoji? + object.custom_emoji.present? + end +end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 7c7cb97df..9dda1631e 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -147,6 +147,7 @@ class DeleteAccountService < BaseService purge_polls! purge_generated_notifications! purge_favourites! + purge_emoji_reactions! purge_bookmarks! purge_feeds! purge_other_associations! @@ -193,6 +194,16 @@ class DeleteAccountService < BaseService end end + def purge_emoji_reactions! + @account.emoji_reactions.in_batches do |reactions| + reactions.each do |reaction| + reaction.status.refresh_emoji_reactions_grouped_by_name + end + Chewy.strategy.current.update(StatusesIndex, reactions.pluck(:status_id)) if Chewy.enabled? + reactions.delete_all + end + end + def purge_bookmarks! @account.bookmarks.in_batches do |bookmarks| Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled? diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index cf746bb0e..d8f51548a 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -55,7 +55,7 @@ class EmojiReactService < BaseService def write_stream(emoji_reaction) emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name - .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } emoji_group['status_id'] = emoji_reaction.status_id.to_s FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id) end diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb index ca58c3c1d..5a2e9eaf1 100644 --- a/app/services/un_emoji_react_service.rb +++ b/app/services/un_emoji_react_service.rb @@ -40,7 +40,7 @@ class UnEmojiReactService < BaseService def write_stream(emoji_reaction) emoji_group = @status.emoji_reactions_grouped_by_name - .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } if emoji_group emoji_group['status_id'] = @status.id.to_s else diff --git a/config/routes.rb b/config/routes.rb index 85c3b1855..414fb81a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,7 @@ Rails.application.routes.draw do /lists/(*any) /notifications /favourites + /emoji_reactions /bookmarks /pinned /start/(*any) diff --git a/config/routes/api.rb b/config/routes/api.rb index ca6ab1a29..1ea8f4712 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -10,6 +10,7 @@ namespace :api, format: false do scope module: :statuses do resources :reblogged_by, controller: :reblogged_by_accounts, only: :index resources :favourited_by, controller: :favourited_by_accounts, only: :index + resources :emoji_reactioned_by, controller: :emoji_reactioned_by_accounts, only: :index resource :reblog, only: :create post :unreblog, to: 'reblogs#destroy' From 837c572d8be7ab0fe44b00e3357b111f2af0b828 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 08:45:11 +0900 Subject: [PATCH 16/39] Add emoji_reactioned_by_slim api (not work) --- ..._reactioned_by_accounts_slim_controller.rb | 28 +++++++++++++++++++ .../mastodon/reducers/user_lists.js | 2 -- app/models/status.rb | 7 +++++ ...oji_reactions_grouped_by_account_object.rb | 14 ++++++++++ ...oji_reacted_by_slim_reaction_serializer.rb | 27 ++++++++++++++++++ .../rest/emoji_reacted_by_slim_serializer.rb | 21 ++++++++++++++ config/routes/api.rb | 1 + 7 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_slim_controller.rb create mode 100644 app/models/status_emoji_reactions_grouped_by_account_object.rb create mode 100644 app/serializers/rest/emoji_reacted_by_slim_reaction_serializer.rb create mode 100644 app/serializers/rest/emoji_reacted_by_slim_serializer.rb 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 new file mode 100644 index 000000000..222017452 --- /dev/null +++ b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_slim_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::EmojiReactionedByAccountsSlimController < Api::BaseController + include Authorization + + before_action -> { authorize_if_got_token! :read, :'read:accounts' } + before_action :set_status + + def index + @accounts = load_emoji_reactions + + # TODO for serialize hash object + render json: @accounts, each_serializer: REST::EmojiReactedBySlimSerializer + end + + private + + def load_emoji_reactions + @status.generate_emoji_reactions_grouped_by_account + end + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end +end diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index ad626c1b2..0b8dc69e9 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -163,8 +163,6 @@ export default function userLists(state = initialState, action) { case notificationsUpdate.type: return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state; case EMOJI_REACTIONS_FETCH_SUCCESS: - console.log('===================') - console.dir(state); return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts)); case FOLLOW_REQUESTS_FETCH_SUCCESS: return normalizeList(state, ['follow_requests'], action.accounts, action.next); diff --git a/app/models/status.rb b/app/models/status.rb index 5610ae6f4..81b24c9db 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -30,6 +30,8 @@ # ordered_media_attachment_ids :bigint(8) is an Array # +require 'ostruct' + class Status < ApplicationRecord include Cacheable include Discard::Model @@ -347,6 +349,11 @@ class Status < ApplicationRecord end end + def generate_emoji_reactions_grouped_by_account + # TODO for serializer + EmojiReaction.where(status_id: id).group_by(&:account) + end + def trendable? if attributes['trendable'].nil? account.trendable? diff --git a/app/models/status_emoji_reactions_grouped_by_account_object.rb b/app/models/status_emoji_reactions_grouped_by_account_object.rb new file mode 100644 index 000000000..4dd33d805 --- /dev/null +++ b/app/models/status_emoji_reactions_grouped_by_account_object.rb @@ -0,0 +1,14 @@ +class StatusEmojiReactionsGroupedByAccountObject + def initialize(account, emoji_reactions) + @account = account + @emoji_reactions = emoji_reactions + end + + def read_attribute_for_serialization(_) + REST::EmojiReactedBySlimSerializer + end + + def model_name + 'EmojiReaction' + end +end diff --git a/app/serializers/rest/emoji_reacted_by_slim_reaction_serializer.rb b/app/serializers/rest/emoji_reacted_by_slim_reaction_serializer.rb new file mode 100644 index 000000000..234218f2e --- /dev/null +++ b/app/serializers/rest/emoji_reacted_by_slim_reaction_serializer.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class REST::EmojiReactedBySlimReactionSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name + + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + + 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 + + def custom_emoji? + object.custom_emoji.present? + end +end diff --git a/app/serializers/rest/emoji_reacted_by_slim_serializer.rb b/app/serializers/rest/emoji_reacted_by_slim_serializer.rb new file mode 100644 index 000000000..6a0888d11 --- /dev/null +++ b/app/serializers/rest/emoji_reacted_by_slim_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class REST::EmojiReactedBySlimSerializer < ActiveModel::Serializer + + belongs_to :account, serializer: REST::AccountSerializer + has_many :emoji_reactions, serializer: REST::EmojiReactedBySlimReactionSerializer + + def attributes + { account => emoji_reactions } + end + + private + + def account + object.first + end + + def emoji_reactions + object.last + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb index 1ea8f4712..5086f1cdd 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -11,6 +11,7 @@ namespace :api, format: false do resources :reblogged_by, controller: :reblogged_by_accounts, only: :index resources :favourited_by, controller: :favourited_by_accounts, only: :index resources :emoji_reactioned_by, controller: :emoji_reactioned_by_accounts, only: :index + resources :emoji_reactioned_by_slim, controller: :emoji_reactioned_by_accounts_slim, only: :index resource :reblog, only: :create post :unreblog, to: 'reblogs#destroy' From 3b5d2b3b358527f6ae02fd2d43158447081ba643 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 08:53:27 +0900 Subject: [PATCH 17/39] Add emoji link on detailed status --- .../status/components/detailed_status.jsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index b4dd18213..944479e19 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -176,6 +176,7 @@ class DetailedStatus extends ImmutablePureComponent { const reblogIcon = 'retweet'; const reblogIconComponent = RepeatIcon; let favouriteLink = ''; + let emojiReactionsLink = ''; let edited = ''; if (this.props.measureHeight) { @@ -319,6 +320,22 @@ class DetailedStatus extends ImmutablePureComponent { ); } + if (this.context.router) { + emojiReactionsLink = ( + + + + + ); + } else { + emojiReactionsLink = ( +
+ + + + ); + } + if (status.get('edited_at')) { edited = ( <> @@ -364,7 +381,7 @@ class DetailedStatus extends ImmutablePureComponent {
- {edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} + {edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink}
From 5baee4e1ca8d1df2ebcaad19b7deca8fb05fe522 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 14:16:29 +0900 Subject: [PATCH 18/39] Add emoji icon support on notification (expects custom) --- .../features/notifications/components/notification.jsx | 6 +++++- app/javascript/mastodon/reducers/notifications.js | 1 + app/serializers/rest/emoji_reaction_serializer.rb | 9 +++++++-- app/serializers/rest/notification_serializer.rb | 5 +++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index a39d1b234..523d4401c 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import classNames from 'classnames'; + import { Link, withRouter } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -27,6 +28,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import FollowRequestContainer from '../containers/follow_request_container'; import Report from './report'; +import EmojiView from '../../../components/emoji_view'; const messages = defineMessages({ favourite: { id: 'notification.favourite', defaultMessage: '{name} favorited your status' }, @@ -219,14 +221,16 @@ class Notification extends ImmutablePureComponent { } renderEmojiReaction (notification, link) { + console.dir(notification) const { intl, unread } = this.props; + const emoji_reaction = notification.get('emoji_reaction'); return (
- +
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 2ca301b19..8673e51ec 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -52,6 +52,7 @@ const notificationToMap = notification => ImmutableMap({ id: notification.id, type: notification.type, account: notification.account.id, + emoji_reaction: ImmutableMap(notification.emoji_reaction), created_at: notification.created_at, status: notification.status ? notification.status.id : null, report: notification.report ? fromJS(notification.report) : null, diff --git a/app/serializers/rest/emoji_reaction_serializer.rb b/app/serializers/rest/emoji_reaction_serializer.rb index 1d8f90368..fd58a125f 100644 --- a/app/serializers/rest/emoji_reaction_serializer.rb +++ b/app/serializers/rest/emoji_reaction_serializer.rb @@ -1,15 +1,20 @@ # frozen_string_literal: true class REST::EmojiReactionSerializer < ActiveModel::Serializer - attributes :name, :count + attributes :name + attribute :count, if: :count? attribute :url, if: :custom_emoji? attribute :static_url, if: :custom_emoji? attribute :domain, if: :custom_emoji? attribute :account_ids, if: :account_ids? + def count? + object.respond_to?(:count) + end + def custom_emoji? - object.url.present? + object.respond_to?(:url) end def account_ids? diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 2d9ab53ef..15c62476d 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -6,6 +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 def id object.id.to_s @@ -18,4 +19,8 @@ class REST::NotificationSerializer < ActiveModel::Serializer def report_type? object.type == :'admin.report' end + + def emoji_reaction_type? + object.type == :emoji_reaction + end end From 2a912a06983684b18fb211a1fbeda8ced5c7cb8a Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 14:19:51 +0900 Subject: [PATCH 19/39] Fix emoji count color --- app/javascript/styles/mastodon/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 236e616e7..28a6ad559 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1447,6 +1447,7 @@ body > [data-popper-placement] { .count { display: block; margin: 0 2px; + color: $ui-secondary-color; } } } From 5fc29d03ddc81c1cbbd520492740414868e0ce95 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 14:53:30 +0900 Subject: [PATCH 20/39] Fix custom emoji in notification --- .../rest/notification_serializer.rb | 2 +- .../rest/notify_emoji_reaction_serializer.rb | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 app/serializers/rest/notify_emoji_reaction_serializer.rb diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 15c62476d..f3235b3b1 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 000000000..a78740d56 --- /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 From fb1c2abc7e379bc9e41751796d82434084a28d99 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 16:59:30 +0900 Subject: [PATCH 21/39] Fix remove emoji reactions bulk --- app/services/un_emoji_react_service.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb index 5a2e9eaf1..69b1a9392 100644 --- a/app/services/un_emoji_react_service.rb +++ b/app/services/un_emoji_react_service.rb @@ -9,10 +9,11 @@ class UnEmojiReactService < BaseService @account = Account.find(account_id) @status = Status.find(status_id) + p '==================================== DEBUG' + p emoji_reaction + if emoji_reaction - p '================================ DEBUG2 G' - emoji_reaction.destroy - p '================================ DEBUG2 H' + emoji_reaction.destroy! create_notification(emoji_reaction) if !@account.local? && @account.activitypub? notify_to_followers(emoji_reaction) if @account.local? write_stream(emoji_reaction) @@ -25,8 +26,8 @@ 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 From d61e7dbdc11e3d1b8631c2cb95aa5eeff8ecdde2 Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 28 Feb 2023 07:05:20 +0900 Subject: [PATCH 22/39] Fix emoji reaction push notifications --- config/locales/en.yml | 10 +++++++--- config/locales/ja.yml | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 15d682d17..48595d5a5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1458,9 +1458,13 @@ en: sign_up: subject: "%{name} signed up" favourite: - body: 'Your post was favorited by %{name}:' - subject: "%{name} favorited your post" - title: New favorite + 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 ebce3178b..4bd027e5e 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1435,6 +1435,10 @@ ja: body: "%{name}さんにお気に入り登録された、あなたのトゥートがあります:" subject: "%{name}さんにお気に入りに登録されました" title: 新たなお気に入り登録 + emoji_reaction: + body: "%{name}さんに絵文字リアクションされた、あなたの投稿があります:" + subject: "%{name}さんに絵文字リアクションされました" + title: 新たな絵文字リアクション follow: body: "%{name}さんにフォローされています!" subject: "%{name}さんにフォローされています" From f8ca193b89265dc78a1f65ead424c69ec417927c Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 20:17:25 +0900 Subject: [PATCH 23/39] Fix third party app emoji reaction feature --- app/serializers/rest/instance_serializer.rb | 14 +++++++++++++- app/serializers/rest/v1/instance_serializer.rb | 13 ++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index eeab951d2..f8f51985d 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, :feature_quote + :registrations, :feature_quote, :fedibird_capabilities has_one :contact, serializer: ContactSerializer has_many :rules, serializer: REST::RuleSerializer @@ -93,8 +93,20 @@ class REST::InstanceSerializer < ActiveModel::Serializer } end +<<<<<<< HEAD def feature_quote true +======= + # for third party apps + def fedibird_capabilities + capabilities = [ + :emoji_reaction, + ] + + capabilities << :profile_search unless Chewy.enabled? + + capabilities +>>>>>>> 5c5b42d42c (Fix third party app emoji reaction feature) end private diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index 99d1b2bd6..a96d2adf5 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 From fed10012e466187d8560fa558e4604d7331795ac Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 17:31:45 +0900 Subject: [PATCH 24/39] Remove emoji reactions view by notification --- app/javascript/mastodon/actions/notifications.js | 2 ++ app/javascript/mastodon/components/status.jsx | 3 ++- .../features/notifications/components/notification.jsx | 8 +++++++- app/serializers/rest/instance_serializer.rb | 4 +--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 2c63a10e5..2674405d2 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -142,9 +142,11 @@ const excludeTypesFromFilter = filter => { 'favourite', 'emoji_reaction', 'reblog', + 'status_reference', 'mention', 'poll', 'status', + 'list_status', 'update', 'admin.sign_up', 'admin.report', diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index d2258c5a8..5081f057b 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -204,6 +204,7 @@ class Status extends ImmutablePureComponent { }), contextType: PropTypes.string, ...WithOptionalRouterPropTypes, + withoutEmojiReactions: PropTypes.bool, }; // Avoid checking props that are functions (and whose equality will always @@ -675,7 +676,7 @@ class Status extends ImmutablePureComponent { const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0; 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 523d4401c..c503a0a0c 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -186,6 +186,7 @@ class Notification extends ImmutablePureComponent { cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} unread={this.props.unread} + withoutEmojiReactions={true} /> ); } @@ -214,6 +215,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} />
@@ -221,7 +223,6 @@ class Notification extends ImmutablePureComponent { } renderEmojiReaction (notification, link) { - console.dir(notification) const { intl, unread } = this.props; const emoji_reaction = notification.get('emoji_reaction'); @@ -248,6 +249,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} />
@@ -278,6 +280,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} />
@@ -313,6 +316,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> @@ -348,6 +352,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> @@ -389,6 +394,7 @@ class Notification extends ImmutablePureComponent { updateScrollBottom={this.props.updateScrollBottom} cachedMediaWidth={this.props.cachedMediaWidth} cacheMediaWidth={this.props.cacheMediaWidth} + withoutEmojiReactions={true} /> diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index f8f51985d..df17ef9d9 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -93,10 +93,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer } end -<<<<<<< HEAD def feature_quote true -======= + end # for third party apps def fedibird_capabilities capabilities = [ @@ -106,7 +105,6 @@ class REST::InstanceSerializer < ActiveModel::Serializer capabilities << :profile_search unless Chewy.enabled? capabilities ->>>>>>> 5c5b42d42c (Fix third party app emoji reaction feature) end private From b9f8e7731badc532a19eedcc5c345a6e83dc7ca9 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 27 Feb 2023 20:17:25 +0900 Subject: [PATCH 25/39] Fix third party app emoji reaction feature --- app/serializers/rest/instance_serializer.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index df17ef9d9..4def393a5 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -11,7 +11,11 @@ class REST::InstanceSerializer < ActiveModel::Serializer attributes :domain, :title, :version, :source_url, :description, :usage, :thumbnail, :languages, :configuration, +<<<<<<< HEAD :registrations, :feature_quote, :fedibird_capabilities +======= + :registrations, :fedibird_capabilities +>>>>>>> 5c5b42d42c (Fix third party app emoji reaction feature) has_one :contact, serializer: ContactSerializer has_many :rules, serializer: REST::RuleSerializer @@ -96,6 +100,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer def feature_quote true end + # for third party apps def fedibird_capabilities capabilities = [ From 0190688af0b008922e7949f94674ea960b9d4029 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 6 Mar 2023 10:31:50 +0900 Subject: [PATCH 26/39] Follow fedibird delete emoji-reaction api --- app/controllers/api/v1/statuses/emoji_reactions_controller.rb | 4 ++-- config/routes/api.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index 88a3ac464..da68f5813 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:emoji_reactions' } before_action :require_user! - before_action :set_status, only: %i(create update) + before_action :set_status, only: %i(create update destroy) before_action :set_status_without_authorize, only: [:destroy] def create @@ -18,7 +18,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController end def destroy - emoji = params[:emoji] + emoji = params[:emoji] || params[:id] if emoji shortcode, domain = emoji.split('@') diff --git a/config/routes/api.rb b/config/routes/api.rb index 5086f1cdd..9785fa107 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -32,7 +32,7 @@ namespace :api, format: false do post :translate, to: 'translations#create' - resources :emoji_reactions, only: [:create, :update], constraints: { id: /[^\/]+/ } + resources :emoji_reactions, only: [:create, :update, :destroy], constraints: { id: /[^\/]+/ } post :emoji_unreaction, to: 'emoji_reactions#destroy' end From 57eed5696e7db89e29b910f5fc46c9f3866deb4b Mon Sep 17 00:00:00 2001 From: YoheiZuho Date: Sun, 31 Dec 2023 11:46:54 +0000 Subject: [PATCH 27/39] fix conflict --- app/serializers/rest/instance_serializer.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 4def393a5..498a1cc68 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -11,11 +11,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer attributes :domain, :title, :version, :source_url, :description, :usage, :thumbnail, :languages, :configuration, -<<<<<<< HEAD :registrations, :feature_quote, :fedibird_capabilities -======= - :registrations, :fedibird_capabilities ->>>>>>> 5c5b42d42c (Fix third party app emoji reaction feature) has_one :contact, serializer: ContactSerializer has_many :rules, serializer: REST::RuleSerializer From 3ad062a886bdc5d3a8146791a92211dac0bfb4b6 Mon Sep 17 00:00:00 2001 From: YoheiZuho Date: Mon, 1 Jan 2024 09:10:17 +0000 Subject: [PATCH 28/39] fix notification 500 --- app/javascript/mastodon/actions/streaming.js | 5 +++-- .../features/notifications/components/filter_bar.jsx | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 5647050e0..89e71c893 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -105,8 +105,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; case 'emoji_reaction': - dispatch(updateEmojiReactions(JSON.parse(data.payload), getState().getIn(['meta', 'me']))); - break; + // @ts-expect-error + dispatch(updateEmojiReactions(JSON.parse(data.payload), getState().getIn(['meta', 'me']))); + break; case 'conversation': // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx index 7f9acef1c..887051ca2 100644 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx @@ -5,6 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { ReactComponent as HomeIcon } from '@material-symbols/svg-600/outlined/home-fill.svg'; import { ReactComponent as InsertChartIcon } from '@material-symbols/svg-600/outlined/insert_chart.svg'; +import { ReactComponent as EmojiReactionIcon } from '@material-symbols/svg-600/outlined/mood.svg'; import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outlined/person_add.svg'; import { ReactComponent as RepeatIcon } from '@material-symbols/svg-600/outlined/repeat.svg'; import { ReactComponent as ReplyAllIcon } from '@material-symbols/svg-600/outlined/reply_all.svg'; @@ -15,6 +16,7 @@ import { Icon } from 'mastodon/components/icon'; const tooltips = defineMessages({ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' }, + emojiReactions: { id: 'notifications.filter.emoji_reactions', defaultMessage: 'Stamps' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, @@ -87,7 +89,7 @@ class FilterBar extends PureComponent { onClick={this.onClick('emoji_reaction')} title={intl.formatMessage(tooltips.emojiReactions)} > - + ); } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 28a6ad559..0ab0a132b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1421,11 +1421,13 @@ body > [data-popper-placement] { display: flex; flex-wrap: wrap; gap: 4px 8px; - margin: 8px 0 2px 4px; + margin: 8px 0 2px 0; .emoji-reactions-bar__button { - background: lighten($ui-base-color, 16%); - border: 0; + background: $white; + border: 1px solid lighten($ui-base-color, 8%); + border-radius: 3px; + padding: 0 6px; cursor: pointer; display: flex; justify-items: center; @@ -1433,7 +1435,8 @@ body > [data-popper-placement] { height: 20px; &.toggled { - background: darken($ui-primary-color, 16%); + background-color: mix($white, $ui-highlight-color, 80%); + border-color: mix(lighten($ui-base-color, 8%), $ui-highlight-color, 80%); } .emoji { @@ -1457,7 +1460,7 @@ body > [data-popper-placement] { justify-content: space-between; align-items: center; gap: 18px; - margin-top: 16px; + //margin-top: 16px; } .detailed-status__action-bar-dropdown { From 2b2851b05b38170397c69a513db4ea6e6a0f07d3 Mon Sep 17 00:00:00 2001 From: YoheiZuho Date: Mon, 1 Jan 2024 11:08:57 +0000 Subject: [PATCH 39/39] fix css --- app/javascript/styles/mastodon/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0ab0a132b..b148ff1fe 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1420,7 +1420,7 @@ body > [data-popper-placement] { .status__emoji-reactions-bar { display: flex; flex-wrap: wrap; - gap: 4px 8px; + // gap: 4px 8px; margin: 8px 0 2px 0; .emoji-reactions-bar__button {