From 871d6c594e43a3a3efc25b220f1c2de63d98ef98 Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 24 Nov 2022 11:50:32 +0000 Subject: [PATCH 01/91] add backend support for status emoji reactions turns out we can just reuse the code for announcement reactions. --- .../api/v1/statuses/reactions_controller.rb | 29 +++++++++++++++++++ app/models/status.rb | 16 ++++++++++ app/models/status_reaction.rb | 29 +++++++++++++++++++ app/serializers/rest/status_serializer.rb | 5 ++++ app/validators/status_reaction_validator.rb | 28 ++++++++++++++++++ .../20221124114030_create_status_reactions.rb | 14 +++++++++ .../fabricators/status_reaction_fabricator.rb | 6 ++++ spec/models/status_reaction_spec.rb | 5 ++++ 8 files changed, 132 insertions(+) create mode 100644 app/controllers/api/v1/statuses/reactions_controller.rb create mode 100644 app/models/status_reaction.rb create mode 100644 app/validators/status_reaction_validator.rb create mode 100644 db/migrate/20221124114030_create_status_reactions.rb create mode 100644 spec/fabricators/status_reaction_fabricator.rb create mode 100644 spec/models/status_reaction_spec.rb diff --git a/app/controllers/api/v1/statuses/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb new file mode 100644 index 0000000000..9a1bf57079 --- /dev/null +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::ReactionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :write, :'write:favourites' } + before_action :require_user! + + before_action :set_status + before_action :set_reaction, except: :update + + def update + @status.status_reactions.create!(account: current_account, name: params[:id]) + render_empty + end + + def destroy + @reaction.destroy! + render_empty + end + + private + + def set_reaction + @reaction = @status.status_reactions.where(account: current_account).find_by!(name: params[:id]) + end + + def set_status + @status = Status.find(params[:status_id]) + end +end diff --git a/app/models/status.rb b/app/models/status.rb index a52098b602..b0cc45c6dc 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -71,6 +71,7 @@ class Status < ApplicationRecord has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account' has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status has_many :media_attachments, dependent: :nullify + has_many :status_reactions, dependent: :destroy has_and_belongs_to_many :tags has_and_belongs_to_many :preview_cards @@ -292,6 +293,21 @@ class Status < ApplicationRecord @emojis = CustomEmoji.from_text(fields.join(' '), account.domain) end + def reactions(account = nil) + records = begin + scope = status_reactions.group(:status_id, :name, :custom_emoji_id).order(Arel.sql('MIN(created_at) ASC')) + + if account.nil? + scope.select('name, custom_emoji_id, count(*) as count, false as me') + else + scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from status_reactions r where r.account_id = #{account.id} and r.status_id = status_reactions.status_id and r.name = status_reactions.name) as me") + end + end + + ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji) + records + end + def ordered_media_attachments if ordered_media_attachment_ids.nil? media_attachments diff --git a/app/models/status_reaction.rb b/app/models/status_reaction.rb new file mode 100644 index 0000000000..32cb9edd4a --- /dev/null +++ b/app/models/status_reaction.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: status_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) +# created_at :datetime not null +# updated_at :datetime not null +# +class StatusReaction < ApplicationRecord + belongs_to :account + belongs_to :status, inverse_of: :status_reactions + belongs_to :custom_emoji, optional: true + + validates :name, presence: true + validates_with StatusReactionValidator + + before_validation :set_custom_emoji + + private + + def set_custom_emoji + self.custom_emoji = CustomEmoji.local.find_by(disabled: false, shortcode: name) if name.present? + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index eb5f3c3eaf..f8c6d3a116 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -28,6 +28,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :ordered_mentions, key: :mentions has_many :tags has_many :emojis, serializer: REST::CustomEmojiSerializer + has_many :reactions, serializer: REST::ReactionSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer @@ -148,6 +149,10 @@ class REST::StatusSerializer < ActiveModel::Serializer object.active_mentions.to_a.sort_by(&:id) end + def reactions + object.reactions(current_user&.account) + end + class ApplicationSerializer < ActiveModel::Serializer attributes :name, :website diff --git a/app/validators/status_reaction_validator.rb b/app/validators/status_reaction_validator.rb new file mode 100644 index 0000000000..7c1c6983bb --- /dev/null +++ b/app/validators/status_reaction_validator.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class StatusReactionValidator < ActiveModel::Validator + SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze + + LIMIT = 8 + + def validate(reaction) + return if reaction.name.blank? + + reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name) + reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if new_reaction?(reaction) && limit_reached?(reaction) + end + + private + + def unicode_emoji?(name) + SUPPORTED_EMOJIS.include?(name) + end + + def new_reaction?(reaction) + !reaction.status.status_reactions.where(name: reaction.name).exists? + end + + def limit_reached?(reaction) + reaction.status.status_reactions.where.not(name: reaction.name).count('distinct name') >= LIMIT + end +end diff --git a/db/migrate/20221124114030_create_status_reactions.rb b/db/migrate/20221124114030_create_status_reactions.rb new file mode 100644 index 0000000000..bbf1f3376a --- /dev/null +++ b/db/migrate/20221124114030_create_status_reactions.rb @@ -0,0 +1,14 @@ +class CreateStatusReactions < ActiveRecord::Migration[6.1] + def change + create_table :status_reactions do |t| + t.references :account, null: false, foreign_key: true + t.references :status, null: false, foreign_key: true + t.string :name, null: false, default: '' + t.references :custom_emoji, null: true, foreign_key: true + + t.timestamps + end + + add_index :status_reactions, [:account_id, :status_id, :name], unique: true, name: :index_status_reactions_on_account_id_and_status_id + end +end diff --git a/spec/fabricators/status_reaction_fabricator.rb b/spec/fabricators/status_reaction_fabricator.rb new file mode 100644 index 0000000000..3d4b93efe0 --- /dev/null +++ b/spec/fabricators/status_reaction_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:status_reaction) do + account nil + status nil + name "MyString" + custom_emoji nil +end \ No newline at end of file diff --git a/spec/models/status_reaction_spec.rb b/spec/models/status_reaction_spec.rb new file mode 100644 index 0000000000..18860318cc --- /dev/null +++ b/spec/models/status_reaction_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe StatusReaction, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 44154d2c0788dcf26a5304d8d686236722fad7a2 Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 24 Nov 2022 17:30:52 +0000 Subject: [PATCH 02/91] add frontend for emoji reactions this is still pretty bare bones but hey, it works. --- .../flavours/glitch/actions/interactions.js | 87 ++++++++- .../flavours/glitch/components/status.jsx | 12 ++ .../glitch/components/status_reactions_bar.js | 177 ++++++++++++++++++ .../glitch/containers/status_container.js | 18 ++ .../flavours/glitch/reducers/statuses.js | 44 +++++ 5 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 app/javascript/flavours/glitch/components/status_reactions_bar.js diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index c7b552a656..08987ad664 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -41,6 +41,16 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; +export const STATUS_REACTION_UPDATE = 'STATUS_REACTION_UPDATE'; + +export const STATUS_REACTION_ADD_REQUEST = 'STATUS_REACTION_ADD_REQUEST'; +export const STATUS_REACTION_ADD_SUCCESS = 'STATUS_REACTION_ADD_SUCCESS'; +export const STATUS_REACTION_ADD_FAIL = 'STATUS_REACTION_ADD_FAIL'; + +export const STATUS_REACTION_REMOVE_REQUEST = 'STATUS_REACTION_REMOVE_REQUEST'; +export const STATUS_REACTION_REMOVE_SUCCESS = 'STATUS_REACTION_REMOVE_SUCCESS'; +export const STATUS_REACTION_REMOVE_FAIL = 'STATUS_REACTION_REMOVE_FAIL'; + export function reblog(status, visibility) { return function (dispatch, getState) { dispatch(reblogRequest(status)); @@ -391,4 +401,79 @@ export function unpinFail(status, error) { status, error, }; -} +}; + +export const statusAddReaction = (statusId, name) => (dispatch, getState) => { + const status = getState().get('statuses').get(statusId); + let alreadyAdded = false; + if (status) { + const reaction = status.get('reactions').find(x => x.get('name') === name); + if (reaction && reaction.get('me')) { + alreadyAdded = true; + } + } + if (!alreadyAdded) { + dispatch(statusAddReactionRequest(statusId, name, alreadyAdded)); + } + + api(getState).put(`/api/v1/statuses/${statusId}/reactions/${name}`).then(() => { + dispatch(statusAddReactionSuccess(statusId, name, alreadyAdded)); + }).catch(err => { + if (!alreadyAdded) { + dispatch(statusAddReactionFail(statusId, name, err)); + } + }); +}; + +export const statusAddReactionRequest = (statusId, name) => ({ + type: STATUS_REACTION_ADD_REQUEST, + id: statusId, + name, + skipLoading: true, +}); + +export const statusAddReactionSuccess = (statusId, name) => ({ + type: STATUS_REACTION_ADD_SUCCESS, + id: statusId, + name, + skipLoading: true, +}); + +export const statusAddReactionFail = (statusId, name, error) => ({ + type: STATUS_REACTION_ADD_FAIL, + id: statusId, + name, + error, + skipLoading: true, +}); + +export const statusRemoveReaction = (statusId, name) => (dispatch, getState) => { + dispatch(statusRemoveReactionRequest(statusId, name)); + + api(getState).delete(`/api/v1/statuses/${statusId}/reactions/${name}`).then(() => { + dispatch(statusRemoveReactionSuccess(statusId, name)); + }).catch(err => { + dispatch(statusRemoveReactionFail(statusId, name, err)); + }); +}; + +export const statusRemoveReactionRequest = (statusId, name) => ({ + type: STATUS_REACTION_REMOVE_REQUEST, + id: statusId, + name, + skipLoading: true, +}); + +export const statusRemoveReactionSuccess = (statusId, name) => ({ + type: STATUS_REACTION_REMOVE_SUCCESS, + id: statusId, + name, + skipLoading: true, +}); + +export const statusRemoveReactionFail = (statusId, name) => ({ + type: STATUS_REACTION_REMOVE_FAIL, + id: statusId, + name, + skipLoading: true, +}); diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 441936a431..e104c5e5c5 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -6,6 +6,7 @@ import StatusHeader from './status_header'; import StatusIcons from './status_icons'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; +import StatusReactionsBar from './status_reactions_bar'; import AttachmentList from './attachment_list'; import Card from '../features/status/components/card'; import { injectIntl, FormattedMessage } from 'react-intl'; @@ -77,6 +78,8 @@ class Status extends ImmutablePureComponent { onDelete: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, + onReactionAdd: PropTypes.func, + onReactionRemove: PropTypes.func, onPin: PropTypes.func, onOpenMedia: PropTypes.func, onOpenVideo: PropTypes.func, @@ -104,6 +107,7 @@ class Status extends ImmutablePureComponent { scrollKey: PropTypes.string, deployPictureInPicture: PropTypes.func, settings: ImmutablePropTypes.map.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, pictureInPicture: ImmutablePropTypes.contains({ inUse: PropTypes.bool, available: PropTypes.bool, @@ -815,6 +819,14 @@ class Status extends ImmutablePureComponent { rewriteMentions={settings.get('rewrite_mentions')} /> + + {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? ( { + const { addReaction, statusId } = this.props; + addReaction(statusId, data.native.replace(/:/g, '')); + } + + willEnter() { + return { scale: reduceMotion ? 1 : 0 }; + } + + willLeave() { + return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) }; + } + + render() { + const { reactions } = this.props; + const visibleReactions = reactions.filter(x => x.get('count') > 0); + + const styles = visibleReactions.map(reaction => ({ + key: reaction.get('name'), + data: reaction, + style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) }, + })).toArray(); + + return ( + + {items => ( +
+ {items.map(({ key, data, style }) => ( + + ))} + + {visibleReactions.size < 8 && } />} +
+ )} +
+ ); + } + +} + +class Reaction extends ImmutablePureComponent { + + static propTypes = { + statusId: PropTypes.string, + reaction: ImmutablePropTypes.map.isRequired, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + style: PropTypes.object, + }; + + state = { + hovered: false, + }; + + handleClick = () => { + const { reaction, statusId, addReaction, removeReaction } = this.props; + + if (reaction.get('me')) { + removeReaction(statusId, reaction.get('name')); + } else { + addReaction(statusId, reaction.get('name')); + } + } + + handleMouseEnter = () => this.setState({ hovered: true }) + + handleMouseLeave = () => this.setState({ hovered: false }) + + render() { + const { reaction } = this.props; + + let shortCode = reaction.get('name'); + + if (unicodeMapping[shortCode]) { + shortCode = unicodeMapping[shortCode].shortCode; + } + + return ( + + ); + } + +} + +class Emoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.string.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + hovered: PropTypes.bool.isRequired, + }; + + render() { + const { emoji, emojiMap, hovered } = this.props; + + if (unicodeMapping[emoji]) { + const { filename, shortCode } = unicodeMapping[this.props.emoji]; + const title = shortCode ? `:${shortCode}:` : ''; + + return ( + {emoji} + ); + } else if (emojiMap.get(emoji)) { + const filename = (autoPlayGif || hovered) + ? emojiMap.getIn([emoji, 'url']) + : emojiMap.getIn([emoji, 'static_url']); + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } else { + return null; + } + } + +} diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 27ba4e4260..0a32e99990 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -15,6 +15,8 @@ import { unbookmark, pin, unpin, + statusAddReaction, + statusRemoveReaction, } from 'flavours/glitch/actions/interactions'; import { muteStatus, @@ -39,6 +41,13 @@ import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { defineMessages, injectIntl } from 'react-intl'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state'; import { showAlertForError } from '../actions/alerts'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import Spoilers from '../components/spoilers'; +import Icon from 'flavours/glitch/components/icon'; +import { createSelector } from 'reselect'; +import { Map as ImmutableMap } from 'immutable'; + +const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -82,6 +91,7 @@ const makeMapStateToProps = () => { account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, + emojiMap: customEmojiMap(state), pictureInPicture: getPictureInPicture(state, props), }; }; @@ -161,6 +171,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onReactionAdd (statusId, name) { + dispatch(statusAddReaction(statusId, name)); + }, + + onReactionRemove (statusId, name) { + dispatch(statusRemoveReaction(statusId, name)); + }, + onEmbed (status) { dispatch(openModal('EMBED', { url: status.get('url'), diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index ca220c54d6..9e5533c91c 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -6,6 +6,11 @@ import { UNFAVOURITE_SUCCESS, BOOKMARK_REQUEST, BOOKMARK_FAIL, + STATUS_REACTION_UPDATE, + STATUS_REACTION_ADD_FAIL, + STATUS_REACTION_REMOVE_FAIL, + STATUS_REACTION_ADD_REQUEST, + STATUS_REACTION_REMOVE_REQUEST, } from 'flavours/glitch/actions/interactions'; import { STATUS_MUTE_SUCCESS, @@ -37,6 +42,37 @@ const deleteStatus = (state, id, references) => { return state.delete(id); }; +const updateReaction = (state, id, name, updater) => state.update( + id, + status => status.update( + 'reactions', + reactions => { + const index = reactions.findIndex(reaction => reaction.get('name') === name); + if (index > -1) { + return reactions.update(index, reaction => updater(reaction)); + } else { + return reactions.push(updater(fromJS({ name, count: 0 }))); + } + }, + ), +); + +const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count)); + +const addReaction = (state, id, name) => updateReaction( + state, + id, + name, + x => x.set('me', true).update('count', n => n + 1), +); + +const removeReaction = (state, id, name) => updateReaction( + state, + id, + name, + x => x.set('me', false).update('count', n => n - 1), +); + const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -63,6 +99,14 @@ export default function statuses(state = initialState, action) { return state.setIn([action.status.get('id'), 'reblogged'], true); case REBLOG_FAIL: return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false); + case STATUS_REACTION_UPDATE: + return updateReactionCount(state, action.reaction); + case STATUS_REACTION_ADD_REQUEST: + case STATUS_REACTION_REMOVE_FAIL: + return addReaction(state, action.id, action.name); + case STATUS_REACTION_REMOVE_REQUEST: + case STATUS_REACTION_ADD_FAIL: + return removeReaction(state, action.id, action.name); case STATUS_MUTE_SUCCESS: return state.setIn([action.id, 'muted'], true); case STATUS_UNMUTE_SUCCESS: From 9c7ddeedbbca9e6e911e0d8e756e7845b1f29394 Mon Sep 17 00:00:00 2001 From: fef Date: Fri, 25 Nov 2022 23:02:40 +0000 Subject: [PATCH 03/91] show reactions in detailed status view --- .../glitch/containers/status_container.js | 7 ++--- .../status/components/detailed_status.jsx | 12 +++++++ .../flavours/glitch/features/status/index.jsx | 31 +++++++++++++++++++ .../flavours/glitch/utils/emoji_map.js | 11 +++++++ 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 app/javascript/flavours/glitch/utils/emoji_map.js diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 0a32e99990..cda69e3ad0 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -44,10 +44,7 @@ import { showAlertForError } from '../actions/alerts'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Spoilers from '../components/spoilers'; import Icon from 'flavours/glitch/components/icon'; -import { createSelector } from 'reselect'; -import { Map as ImmutableMap } from 'immutable'; - -const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); +import buildCustomEmojiMap from '../utils/emoji_map'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -91,7 +88,7 @@ const makeMapStateToProps = () => { account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, - emojiMap: customEmojiMap(state), + emojiMap: buildCustomEmojiMap(state), pictureInPicture: getPictureInPicture(state, props), }; }; diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx index e36ab818fc..f87f2c9f3b 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx @@ -20,6 +20,7 @@ import { Icon } from 'flavours/glitch/components/icon'; import { AnimatedNumber } from 'flavours/glitch/components/animated_number'; import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; import EditedTimestamp from 'flavours/glitch/components/edited_timestamp'; +import StatusReactionsBar from '../../../components/status_reactions_bar'; class DetailedStatus extends ImmutablePureComponent { @@ -45,6 +46,9 @@ class DetailedStatus extends ImmutablePureComponent { available: PropTypes.bool, }), onToggleMediaVisibility: PropTypes.func, + onReactionAdd: PropTypes.func.isRequired, + onReactionRemove: PropTypes.func.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, intl: PropTypes.object.isRequired, }; @@ -319,6 +323,14 @@ class DetailedStatus extends ImmutablePureComponent { disabled /> + +
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index 39a7780456..d7491893d4 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -29,6 +29,8 @@ import { unreblog, pin, unpin, + statusAddReaction, + statusRemoveReaction, } from 'flavours/glitch/actions/interactions'; import { replyCompose, @@ -55,6 +57,7 @@ import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/com import { Icon } from 'flavours/glitch/components/icon'; import { Helmet } from 'react-helmet'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; +import buildCustomEmojiMap from '../../utils/emoji_map'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -148,6 +151,7 @@ const makeMapStateToProps = () => { askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0, domain: state.getIn(['meta', 'domain']), pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), + emojiMap: buildCustomEmojiMap(state), }; }; @@ -295,6 +299,30 @@ class Status extends ImmutablePureComponent { } }; + handleReactionAdd = (statusId, name) => { + const { dispatch } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + dispatch(statusAddReaction(statusId, name)); + } else { + dispatch(openModal('INTERACTION', { + type: 'reaction_add', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); + } + } + + handleReactionRemove = (statusId, name) => { + const { dispatch } = this.props; + const { signedIn } = this.context.identity; + + if (signedIn) { + dispatch(statusRemoveReaction(statusId, name)); + } + } + handlePin = (status) => { if (status.get('pinned')) { this.props.dispatch(unpin(status)); @@ -680,6 +708,8 @@ class Status extends ImmutablePureComponent { settings={settings} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} + onReactionAdd={this.handleReactionAdd} + onReactionRemove={this.handleReactionRemove} expanded={isExpanded} onToggleHidden={this.handleToggleHidden} onTranslate={this.handleTranslate} @@ -687,6 +717,7 @@ class Status extends ImmutablePureComponent { showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} pictureInPicture={pictureInPicture} + emojiMap={this.props.emojiMap} /> state.get('custom_emojis')], + items => items.reduce( + (map, emoji) => map.set(emoji.get('shortcode'), emoji), + ImmutableMap(), + ), +); +export default buildCustomEmojiMap; From 7f0e61fb8d336ef374042c9f90bef62681c3336f Mon Sep 17 00:00:00 2001 From: fef Date: Mon, 28 Nov 2022 22:23:13 +0000 Subject: [PATCH 04/91] federate emoji reactions this is kind of experimental, but it should work in theory. at least i tested it with a remove akkoma instance and it didn't crash. --- .../api/v1/statuses/reactions_controller.rb | 4 +-- app/lib/activitypub/activity.rb | 2 ++ app/lib/activitypub/activity/emoji_react.rb | 14 ++++++++ app/models/concerns/account_interactions.rb | 4 +++ app/models/status.rb | 2 +- .../activitypub/emoji_reaction_serializer.rb | 36 +++++++++++++++++++ .../undo_emoji_reaction_serializer.rb | 19 ++++++++++ .../undo_emoji_reaction_serializer.rb | 0 app/services/status_reaction_service.rb | 27 ++++++++++++++ app/services/status_unreaction_service.rb | 21 +++++++++++ 10 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 app/lib/activitypub/activity/emoji_react.rb 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/undo_emoji_reaction_serializer.rb create mode 100644 app/services/status_reaction_service.rb create mode 100644 app/services/status_unreaction_service.rb diff --git a/app/controllers/api/v1/statuses/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb index 9a1bf57079..f7dc2f99ce 100644 --- a/app/controllers/api/v1/statuses/reactions_controller.rb +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -8,12 +8,12 @@ class Api::V1::Statuses::ReactionsController < Api::BaseController before_action :set_reaction, except: :update def update - @status.status_reactions.create!(account: current_account, name: params[:id]) + StatusReactionService.new.call(current_account, @status, params[:id]) render_empty end def destroy - @reaction.destroy! + StatusUnreactionService.new.call(current_account, @status) render_empty end diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 5d95962548..9baac1d012 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -39,6 +39,8 @@ class ActivityPub::Activity ActivityPub::Activity::Follow when 'Like' ActivityPub::Activity::Like + when 'EmojiReact' + ActivityPub::Activity::EmojiReact when 'Block' ActivityPub::Activity::Block when 'Update' diff --git a/app/lib/activitypub/activity/emoji_react.rb b/app/lib/activitypub/activity/emoji_react.rb new file mode 100644 index 0000000000..82c098f56e --- /dev/null +++ b/app/lib/activitypub/activity/emoji_react.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::EmojiReact < ActivityPub::Activity + def perform + original_status = status_from_uri(object_uri) + name = @json['content'] + return if original_status.nil? || + !original_status.account.local? || + delete_arrived_first?(@json['id']) || + @account.reacted?(original_status, name) + + original_status.status_reactions.create!(account: @account, name: name) + end +end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 3c64ebd9fa..e3d2449733 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -243,6 +243,10 @@ module AccountInteractions status.proper.favourites.where(account: self).exists? end + def reacted?(status, name, custom_emoji = nil) + status.proper.status_reactions.where(account: self, name: name, custom_emoji: custom_emoji).exists? + end + def bookmarked?(status) status.proper.bookmarks.where(account: self).exists? end diff --git a/app/models/status.rb b/app/models/status.rb index b0cc45c6dc..a8d600ef1a 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -71,7 +71,7 @@ class Status < ApplicationRecord has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account' has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status has_many :media_attachments, dependent: :nullify - has_many :status_reactions, dependent: :destroy + has_many :status_reactions, inverse_of: :status, dependent: :destroy has_and_belongs_to_many :tags has_and_belongs_to_many :preview_cards diff --git a/app/serializers/activitypub/emoji_reaction_serializer.rb b/app/serializers/activitypub/emoji_reaction_serializer.rb new file mode 100644 index 0000000000..b4111150a4 --- /dev/null +++ b/app/serializers/activitypub/emoji_reaction_serializer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class ActivityPub::EmojiReactionSerializer < ActivityPub::Serializer + attributes :id, :type, :actor, :content + attribute :virtual_object, key: :object + + has_one :custom_emoji, key: :tag, serializer: ActivityPub::EmojiSerializer, unless: -> { object.custom_emoji.nil? } + + def id + [ActivityPub::TagManager.instance.uri_for(object.account), '#emoji_reactions/', object.id].join + end + + def type + 'EmojiReact' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def virtual_object + ActivityPub::TagManager.instance.uri_for(object.status) + end + + def content + if object.custom_emoji.nil? + object.name + else + ":#{object.name}:" + end + end + + def reaction + content + 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 0000000000..49f0c1c8fd --- /dev/null +++ b/app/serializers/activitypub/undo_emoji_reaction_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ActivityPub::UndoEmojiReactionSerializer < ActivityPub::Serializer + attributes :id, :type, :actor + + 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 +end diff --git a/app/serializers/undo_emoji_reaction_serializer.rb b/app/serializers/undo_emoji_reaction_serializer.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/services/status_reaction_service.rb b/app/services/status_reaction_service.rb new file mode 100644 index 0000000000..17acfe7488 --- /dev/null +++ b/app/services/status_reaction_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class StatusReactionService < BaseService + include Authorization + include Payloadable + + def call(account, status, emoji) + reaction = StatusReaction.find_by(account: account, status: status) + return reaction unless reaction.nil? + + name, domain = emoji.split("@") + + custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) + reaction = StatusReaction.create!(account: account, status: status, name: name, custom_emoji: custom_emoji) + + json = Oj.dump(serialize_payload(reaction, ActivityPub::EmojiReactionSerializer)) + if status.account.local? + ActivityPub::RawDistributionWorker.perform_async(json, status.account.id) + else + ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url) + end + + ActivityTracker.increment('activity:interactions') + + reaction + end +end diff --git a/app/services/status_unreaction_service.rb b/app/services/status_unreaction_service.rb new file mode 100644 index 0000000000..13c3c428db --- /dev/null +++ b/app/services/status_unreaction_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class StatusUnreactionService < BaseService + include Payloadable + + def call(account, status) + reaction = StatusReaction.find_by(account: account, status: status) + return if reaction.nil? + + reaction.destroy! + + json = Oj.dump(serialize_payload(reaction, ActivityPub::UndoEmojiReactionSerializer)) + if status.account.local? + ActivityPub::RawDistributionWorker.perform_async(json, status.account.id) + else + ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url) + end + + reaction + end +end From 1bbfad0512d228b188835cdd28d2e379cb7f50e7 Mon Sep 17 00:00:00 2001 From: fef Date: Mon, 28 Nov 2022 22:25:12 +0000 Subject: [PATCH 05/91] remove accidentally created file --- app/serializers/undo_emoji_reaction_serializer.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/serializers/undo_emoji_reaction_serializer.rb diff --git a/app/serializers/undo_emoji_reaction_serializer.rb b/app/serializers/undo_emoji_reaction_serializer.rb deleted file mode 100644 index e69de29bb2..0000000000 From c369bd31545592b54395141880e7f1646c1b9ac8 Mon Sep 17 00:00:00 2001 From: fef Date: Mon, 28 Nov 2022 23:16:56 +0000 Subject: [PATCH 06/91] make status reaction count limit configurable --- .env.production.sample | 3 +++ app/validators/status_reaction_validator.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.env.production.sample b/.env.production.sample index 7bcce0f7e5..a2f5bdd123 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -269,6 +269,9 @@ MAX_POLL_OPTIONS=5 # Maximum allowed poll option characters MAX_POLL_OPTION_CHARS=100 +# Maximum number of emoji reactions per toot and user (minimum 1) +MAX_STATUS_REACTIONS=8 + # Maximum image and video/audio upload sizes # Units are in bytes # 1048576 bytes equals 1 megabyte diff --git a/app/validators/status_reaction_validator.rb b/app/validators/status_reaction_validator.rb index 7c1c6983bb..113e9342ba 100644 --- a/app/validators/status_reaction_validator.rb +++ b/app/validators/status_reaction_validator.rb @@ -3,7 +3,7 @@ class StatusReactionValidator < ActiveModel::Validator SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze - LIMIT = 8 + LIMIT = [1, (ENV['MAX_STATUS_REACTIONS'] || 1).to_i].max def validate(reaction) return if reaction.name.blank? From f1952244d1ee53780b6ed4695d46fe869ae4337f Mon Sep 17 00:00:00 2001 From: fef Date: Tue, 29 Nov 2022 00:39:40 +0000 Subject: [PATCH 07/91] make frontend fetch reaction limit the maximum number of reactions was previously hardcoded to 8. this commit also fixes an incorrect query in StatusReactionValidator where it didn't count per-user reactions but the total amount of different ones. --- .env.production.sample | 2 +- .../flavours/glitch/components/status_reactions_bar.js | 5 +++-- app/javascript/flavours/glitch/initial_state.js | 3 +++ app/serializers/initial_state_serializer.rb | 6 +++++- app/services/status_reaction_service.rb | 7 +++---- app/validators/status_reaction_validator.rb | 6 +++--- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.env.production.sample b/.env.production.sample index a2f5bdd123..326c2cc409 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -270,7 +270,7 @@ MAX_POLL_OPTIONS=5 MAX_POLL_OPTION_CHARS=100 # Maximum number of emoji reactions per toot and user (minimum 1) -MAX_STATUS_REACTIONS=8 +MAX_REACTIONS=8 # Maximum image and video/audio upload sizes # Units are in bytes diff --git a/app/javascript/flavours/glitch/components/status_reactions_bar.js b/app/javascript/flavours/glitch/components/status_reactions_bar.js index db1905be4f..ac57341bcc 100644 --- a/app/javascript/flavours/glitch/components/status_reactions_bar.js +++ b/app/javascript/flavours/glitch/components/status_reactions_bar.js @@ -11,13 +11,14 @@ import React from 'react'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; import AnimatedNumber from './animated_number'; import { assetHost } from '../utils/config'; -import { autoPlayGif } from '../initial_state'; +import { autoPlayGif, maxReactions } from '../initial_state'; export default class StatusReactionsBar extends ImmutablePureComponent { static propTypes = { statusId: PropTypes.string.isRequired, reactions: ImmutablePropTypes.list.isRequired, + reactionLimit: PropTypes.number.isRequired, addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, emojiMap: ImmutablePropTypes.map.isRequired, @@ -62,7 +63,7 @@ export default class StatusReactionsBar extends ImmutablePureComponent { /> ))} - {visibleReactions.size < 8 && } />} + {visibleReactions.size < maxReactions && } />}
)} diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index e230af1c7e..ae79cca704 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -159,4 +159,7 @@ export const pollLimits = (initialState && initialState.poll_limits); export const defaultContentType = getMeta('default_content_type'); export const useSystemEmojiFont = getMeta('system_emoji_font'); +// nyastodon-specific settings +export const maxReactions = (initialState && initialState.max_reactions) || 8; + export default initialState; diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 45ee06e12c..91567dcfdb 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -6,7 +6,7 @@ class InitialStateSerializer < ActiveModel::Serializer attributes :meta, :compose, :accounts, :media_attachments, :settings, :max_toot_chars, :poll_limits, - :languages + :languages, :max_reactions has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer has_one :role, serializer: REST::RoleSerializer @@ -15,6 +15,10 @@ class InitialStateSerializer < ActiveModel::Serializer StatusLengthValidator::MAX_CHARS end + def max_reactions + StatusReactionValidator::LIMIT + end + def poll_limits { max_options: PollValidator::MAX_OPTIONS, diff --git a/app/services/status_reaction_service.rb b/app/services/status_reaction_service.rb index 17acfe7488..e823f6bd88 100644 --- a/app/services/status_reaction_service.rb +++ b/app/services/status_reaction_service.rb @@ -5,12 +5,11 @@ class StatusReactionService < BaseService include Payloadable def call(account, status, emoji) - reaction = StatusReaction.find_by(account: account, status: status) + name, domain = emoji.split('@') + custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) + reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) return reaction unless reaction.nil? - name, domain = emoji.split("@") - - custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) reaction = StatusReaction.create!(account: account, status: status, name: name, custom_emoji: custom_emoji) json = Oj.dump(serialize_payload(reaction, ActivityPub::EmojiReactionSerializer)) diff --git a/app/validators/status_reaction_validator.rb b/app/validators/status_reaction_validator.rb index 113e9342ba..fa6fb2e765 100644 --- a/app/validators/status_reaction_validator.rb +++ b/app/validators/status_reaction_validator.rb @@ -3,13 +3,13 @@ class StatusReactionValidator < ActiveModel::Validator SUPPORTED_EMOJIS = Oj.load_file(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json').to_s).keys.freeze - LIMIT = [1, (ENV['MAX_STATUS_REACTIONS'] || 1).to_i].max + LIMIT = [1, (ENV['MAX_REACTIONS'] || 8).to_i].max def validate(reaction) return if reaction.name.blank? reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name) - reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if new_reaction?(reaction) && limit_reached?(reaction) + reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if limit_reached?(reaction) end private @@ -23,6 +23,6 @@ class StatusReactionValidator < ActiveModel::Validator end def limit_reached?(reaction) - reaction.status.status_reactions.where.not(name: reaction.name).count('distinct name') >= LIMIT + reaction.status.status_reactions.where(status: reaction.status, account: reaction.account).count >= LIMIT end end From f9b72463b44c9c799d02e3c9b7ddedd82d96a16f Mon Sep 17 00:00:00 2001 From: fef Date: Tue, 29 Nov 2022 04:31:22 +0100 Subject: [PATCH 08/91] cherry-pick emoji reaction changes --- .../flavours/glitch/actions/notifications.js | 1 + .../flavours/glitch/components/status.jsx | 1 + .../glitch/components/status_prepend.jsx | 11 ++++++++++ .../glitch/components/status_reactions_bar.js | 1 - .../components/column_settings.jsx | 11 ++++++++++ .../notifications/components/filter_bar.jsx | 8 +++++++ .../notifications/components/notification.jsx | 22 +++++++++++++++++++ .../flavours/glitch/locales/de.json | 3 +++ .../flavours/glitch/locales/en.json | 3 +++ .../flavours/glitch/locales/fr.json | 3 +++ .../flavours/glitch/reducers/settings.js | 3 +++ app/models/notification.rb | 12 +++++++++- .../rest/notification_serializer.rb | 2 +- app/services/status_reaction_service.rb | 1 + config/locales/de.yml | 4 ++++ config/locales/en.yml | 4 ++++ config/locales/fr.yml | 4 ++++ 17 files changed, 91 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index c044ea927f..0460d4ea28 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -139,6 +139,7 @@ const excludeTypesFromFilter = filter => { 'follow', 'follow_request', 'favourite', + 'reaction', 'reblog', 'mention', 'poll', diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index e104c5e5c5..89a5ab808f 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -738,6 +738,7 @@ class Status extends ImmutablePureComponent { if (this.props.prepend && account) { const notifKind = { favourite: 'favourited', + reaction: 'reacted', reblog: 'boosted', reblogged_by: 'boosted', status: 'posted', diff --git a/app/javascript/flavours/glitch/components/status_prepend.jsx b/app/javascript/flavours/glitch/components/status_prepend.jsx index 127636c51d..63ad94562f 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.jsx +++ b/app/javascript/flavours/glitch/components/status_prepend.jsx @@ -56,6 +56,14 @@ export default class StatusPrepend extends React.PureComponent { values={{ name : link }} /> ); + case 'reaction': + return ( + + ); case 'reblog': return ( +
+ + +
+ + {showPushSettings && } + + +
+
+
diff --git a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx index cc83fa5cfd..24928bc124 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.jsx @@ -6,6 +6,7 @@ import { Icon } from 'flavours/glitch/components/icon'; const tooltips = defineMessages({ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' }, + reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, @@ -73,6 +74,13 @@ class FilterBar extends React.PureComponent { > +
); - } else if (placeholder) { + } else if (placeholder || number) { return (
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index aee9cc19e8..acabdc34e5 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -9,6 +9,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../initial_state'; import classNames from 'classnames'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; +import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container'; +import { maxReactions } from '../../flavours/glitch/initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -27,6 +29,7 @@ const messages = defineMessages({ cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + react: { id: 'status.react', defaultMessage: 'React' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, open: { id: 'status.open', defaultMessage: 'Expand this status' }, @@ -65,6 +68,7 @@ class StatusActionBar extends ImmutablePureComponent { relationship: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, + onReactionAdd: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, @@ -126,6 +130,16 @@ class StatusActionBar extends ImmutablePureComponent { } }; + handleEmojiPick = data => { + const { signedIn } = this.context.identity; + + if (signedIn) { + this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, '')); + } else { + this.props.onInteractionModal('favourite', this.props.status); + } + } + handleReblogClick = e => { const { signedIn } = this.context.identity; @@ -229,6 +243,8 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onFilter(); }; + nop = () => {} + render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.context.identity; @@ -355,11 +371,23 @@ class StatusActionBar extends ImmutablePureComponent { ); + const canReact = status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; + const reactButton = ( + + ); + return (
+ {shareButton} diff --git a/app/javascript/mastodon/components/status_reactions.js b/app/javascript/mastodon/components/status_reactions.js new file mode 100644 index 0000000000..39956270a4 --- /dev/null +++ b/app/javascript/mastodon/components/status_reactions.js @@ -0,0 +1,179 @@ +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { reduceMotion } from '../initial_state'; +import spring from 'react-motion/lib/spring'; +import TransitionMotion from 'react-motion/lib/TransitionMotion'; +import classNames from 'classnames'; +import React from 'react'; +import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; +import AnimatedNumber from './animated_number'; +import { assetHost } from '../utils/config'; +import { autoPlayGif } from '../initial_state'; + +export default class StatusReactions extends ImmutablePureComponent { + + static propTypes = { + statusId: PropTypes.string.isRequired, + reactions: ImmutablePropTypes.list.isRequired, + numVisible: PropTypes.number, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + }; + + willEnter() { + return { scale: reduceMotion ? 1 : 0 }; + } + + willLeave() { + return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) }; + } + + render() { + const { reactions, numVisible } = this.props; + let visibleReactions = reactions + .filter(x => x.get('count') > 0) + .sort((a, b) => b.get('count') - a.get('count')); + + // numVisible might be NaN because it's pulled from local settings + // which doesn't do a whole lot of input validation, but that's okay + // because NaN >= 0 evaluates false. + // Still, this should be improved at some point. + if (numVisible >= 0) { + visibleReactions = visibleReactions.filter((_, i) => i < numVisible); + } + + const styles = visibleReactions.map(reaction => ({ + key: reaction.get('name'), + data: reaction, + style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) }, + })).toArray(); + + return ( + + {items => ( +
+ {items.map(({ key, data, style }) => ( + + ))} +
+ )} +
+ ); + } + +} + +class Reaction extends ImmutablePureComponent { + + static propTypes = { + statusId: PropTypes.string, + reaction: ImmutablePropTypes.map.isRequired, + addReaction: PropTypes.func.isRequired, + removeReaction: PropTypes.func.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + style: PropTypes.object, + }; + + state = { + hovered: false, + }; + + handleClick = () => { + const { reaction, statusId, addReaction, removeReaction } = this.props; + + if (reaction.get('me')) { + removeReaction(statusId, reaction.get('name')); + } else { + addReaction(statusId, reaction.get('name')); + } + } + + handleMouseEnter = () => this.setState({ hovered: true }) + + handleMouseLeave = () => this.setState({ hovered: false }) + + render() { + const { reaction } = this.props; + + let shortCode = reaction.get('name'); + + if (unicodeMapping[shortCode]) { + shortCode = unicodeMapping[shortCode].shortCode; + } + + return ( + + ); + } + +} + +class Emoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.string.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, + hovered: PropTypes.bool.isRequired, + }; + + render() { + const { emoji, emojiMap, hovered } = this.props; + + if (unicodeMapping[emoji]) { + const { filename, shortCode } = unicodeMapping[this.props.emoji]; + const title = shortCode ? `:${shortCode}:` : ''; + + return ( + {emoji} + ); + } else if (emojiMap.get(emoji)) { + const filename = (autoPlayGif || hovered) + ? emojiMap.getIn([emoji, 'url']) + : emojiMap.getIn([emoji, 'static_url']); + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } else { + return null; + } + } + +} diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index b54501f009..5f711c26a7 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import Status from '../components/status'; -import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; +import { makeGetStatus, makeGetPictureInPicture, makeCustomEmojiMap } from '../selectors'; import { replyCompose, mentionCompose, @@ -16,6 +16,8 @@ import { unbookmark, pin, unpin, + addReaction, + removeReaction, } from '../actions/interactions'; import { muteStatus, @@ -69,6 +71,7 @@ const makeMapStateToProps = () => { status: getStatus(state, props), nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null, pictureInPicture: getPictureInPicture(state, props), + emojiMap: makeCustomEmojiMap(state), }); return mapStateToProps; @@ -132,6 +135,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onReactionAdd (statusId, name) { + dispatch(addReaction(statusId, name)); + }, + + onReactionRemove (statusId, name) { + dispatch(removeReaction(statusId, name)); + }, + onEmbed (status) { dispatch(openModal('EMBED', { url: status.get('url'), diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx index c12d56e4a9..d9e85a3e9c 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx @@ -316,6 +316,7 @@ class EmojiPickerDropdown extends React.PureComponent { onSkinTone: PropTypes.func.isRequired, skinTone: PropTypes.number.isRequired, button: PropTypes.node, + disabled: PropTypes.bool, }; state = { @@ -349,7 +350,7 @@ class EmojiPickerDropdown extends React.PureComponent { }; onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { + if (!this.state.disabled && !this.state.loading && (!e.key || e.key === 'Enter')) { if (this.state.active) { this.onHideDropdown(); } else { diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index 9251847bad..c93b47d263 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -6,6 +6,7 @@ import ClearColumnButton from './clear_column_button'; import GrantPermissionButton from './grant_permission_button'; import SettingToggle from './setting_toggle'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'mastodon/permissions'; +import PillBarButton from '../../../../flavours/glitch/features/notifications/components/pill_bar_button'; export default class ColumnSettings extends React.PureComponent { @@ -115,6 +116,17 @@ export default class ColumnSettings extends React.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 f16c84f97a..583ae67f4c 100644 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.jsx +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.jsx @@ -6,6 +6,7 @@ import { Icon } from 'mastodon/components/icon'; const tooltips = defineMessages({ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' }, + reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, @@ -73,6 +74,13 @@ class FilterBar extends React.PureComponent { > +
); - } else if (placeholder || number) { + } else if (placeholder) { return (
@@ -75,7 +73,6 @@ class Reaction extends ImmutablePureComponent { reaction: ImmutablePropTypes.map.isRequired, addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, style: PropTypes.object, }; @@ -86,10 +83,12 @@ class Reaction extends ImmutablePureComponent { handleClick = () => { const { reaction, statusId, addReaction, removeReaction } = this.props; - if (reaction.get('me')) { - removeReaction(statusId, reaction.get('name')); - } else { - addReaction(statusId, reaction.get('name')); + if (!reaction.get('extern')) { + if (reaction.get('me')) { + removeReaction(statusId, reaction.get('name')); + } else { + addReaction(statusId, reaction.get('name')); + } } } @@ -109,7 +108,12 @@ class Reaction extends ImmutablePureComponent { style={this.props.style} > - + @@ -124,12 +128,13 @@ class Emoji extends React.PureComponent { static propTypes = { emoji: PropTypes.string.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, hovered: PropTypes.bool.isRequired, + url: PropTypes.string, + staticUrl: PropTypes.string, }; render() { - const { emoji, emojiMap, hovered } = this.props; + const { emoji, hovered, url, staticUrl } = this.props; if (unicodeMapping[emoji]) { const { filename, shortCode } = unicodeMapping[this.props.emoji]; @@ -144,10 +149,8 @@ class Emoji extends React.PureComponent { src={`${assetHost}/emoji/${filename}.svg`} /> ); - } else if (emojiMap.get(emoji)) { - const filename = (autoPlayGif || hovered) - ? emojiMap.getIn([emoji, 'url']) - : emojiMap.getIn([emoji, 'static_url']); + } else { + const filename = (autoPlayGif || hovered) ? url : staticUrl; const shortCode = `:${emoji}:`; return ( @@ -159,8 +162,6 @@ class Emoji extends React.PureComponent { src={filename} /> ); - } else { - return null; } } diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 48e9e10797..5f77c54640 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -44,7 +44,6 @@ import { showAlertForError } from '../actions/alerts'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Spoilers from '../components/spoilers'; import Icon from 'flavours/glitch/components/icon'; -import { makeCustomEmojiMap } from '../selectors'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -88,7 +87,6 @@ const makeMapStateToProps = () => { account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, - emojiMap: makeCustomEmojiMap(state), pictureInPicture: getPictureInPicture(state, props), }; }; diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index 3471d23aa4..c32904a4ab 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -203,7 +203,7 @@ class ActionBar extends React.PureComponent { } } - const canReact = status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; + const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; const reactButton = (
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index fe0988ab07..f9d6b45fa5 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -42,7 +42,11 @@ import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { initBoostModal } from 'flavours/glitch/actions/boosts'; +<<<<<<< HEAD import { makeCustomEmojiMap, makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; +======= +import { makeGetStatus } from 'flavours/glitch/selectors'; +>>>>>>> f0197c80d (display external custom emoji reactions properly) import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ColumnHeader from '../../components/column_header'; import StatusContainer from 'flavours/glitch/containers/status_container'; @@ -151,7 +155,6 @@ const makeMapStateToProps = () => { askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0, domain: state.getIn(['meta', 'domain']), pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), - emojiMap: makeCustomEmojiMap(state), }; }; @@ -711,8 +714,12 @@ class Status extends ImmutablePureComponent { domain={domain} showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} +<<<<<<< HEAD pictureInPicture={pictureInPicture} emojiMap={this.props.emojiMap} +======= + usingPiP={usingPiP} +>>>>>>> f0197c80d (display external custom emoji reactions properly) /> { return hidden && !(isSelf || followingOrRequested); }); - -export const makeCustomEmojiMap = createSelector( - [state => state.get('custom_emojis')], - items => items.reduce( - (map, emoji) => map.set(emoji.get('shortcode'), emoji), - ImmutableMap(), - ), -); diff --git a/app/serializers/rest/reaction_serializer.rb b/app/serializers/rest/reaction_serializer.rb index 1a5dca0183..007d3b5015 100644 --- a/app/serializers/rest/reaction_serializer.rb +++ b/app/serializers/rest/reaction_serializer.rb @@ -3,7 +3,7 @@ class REST::ReactionSerializer < ActiveModel::Serializer include RoutingHelper - attributes :name, :count + attributes :name, :count, :extern attribute :me, if: :current_user? attribute :url, if: :custom_emoji? @@ -21,6 +21,14 @@ class REST::ReactionSerializer < ActiveModel::Serializer object.custom_emoji.present? end + def extern + if custom_emoji? + object.custom_emoji.domain.present? + else + false + end + end + def url full_asset_url(object.custom_emoji.image.url) end From 75cccfb53eeb5aed02f279a35476a67278c48b9e Mon Sep 17 00:00:00 2001 From: fef Date: Sat, 3 Dec 2022 14:23:55 +0000 Subject: [PATCH 34/91] run i18n-tasks normalize --- config/locales/de.yml | 8 ++++---- config/locales/fr.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 3e9eb7a306..97b5d9fc81 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1340,10 +1340,6 @@ de: body: 'Dein Beitrag wurde von %{name} favorisiert:' subject: "%{name} favorisierte deinen Beitrag" title: Neue Favorisierung - reaction: - body: '%{name} hat auf deinen Beitrag reagiert:' - subject: '%{name} hat auf deinen Beitrag reagiert' - title: Neue Reaktion follow: body: "%{name} folgt dir jetzt!" subject: "%{name} folgt dir jetzt" @@ -1360,6 +1356,10 @@ de: title: Neue Erwähnung poll: subject: Eine Umfrage von %{name} ist beendet + reaction: + body: "%{name} hat auf deinen Beitrag reagiert:" + subject: "%{name} hat auf deinen Beitrag reagiert" + title: Neue Reaktion reblog: body: 'Deinen Beitrag hat %{name} geteilt:' subject: "%{name} teilte deinen Beitrag" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 8680080e56..d14aadbeab 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1324,10 +1324,6 @@ fr: body: "%{name} a ajouté votre message à ses favoris :" subject: "%{name} a ajouté votre message à ses favoris" title: Nouveau favori - reaction: - body: '%{name} a réagi·e à votre message:' - subject: '%{name} a réagi·e à votre message' - title: Nouvelle réaction follow: body: "%{name} vous suit !" subject: "%{name} vous suit" @@ -1344,6 +1340,10 @@ fr: title: Nouvelle mention poll: subject: Un sondage de %{name} est terminé + reaction: + body: "%{name} a réagi·e à votre message:" + subject: "%{name} a réagi·e à votre message" + title: Nouvelle réaction reblog: body: 'Votre message été partagé par %{name} :' subject: "%{name} a partagé votre message" From 4454aa7e99b91acdadbb417fbe76972614f23fa5 Mon Sep 17 00:00:00 2001 From: fef Date: Sat, 3 Dec 2022 16:20:29 +0000 Subject: [PATCH 35/91] fix image for new custom emoji reactions --- .../flavours/glitch/actions/interactions.js | 7 ++++--- .../flavours/glitch/components/status_action_bar.jsx | 2 +- .../flavours/glitch/containers/status_container.js | 4 ++-- .../glitch/features/status/components/action_bar.jsx | 2 +- .../flavours/glitch/features/status/index.jsx | 4 ++-- app/javascript/flavours/glitch/reducers/statuses.js | 12 +++++++++--- app/javascript/mastodon/actions/interactions.js | 7 ++++--- .../mastodon/containers/status_container.jsx | 4 ++-- app/javascript/mastodon/features/status/index.jsx | 4 ++-- app/javascript/mastodon/reducers/statuses.js | 12 +++++++++--- 10 files changed, 36 insertions(+), 22 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index 061e0c252c..da06afadf1 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -403,7 +403,7 @@ export function unpinFail(status, error) { }; }; -export const addReaction = (statusId, name) => (dispatch, getState) => { +export const addReaction = (statusId, name, url) => (dispatch, getState) => { const status = getState().get('statuses').get(statusId); let alreadyAdded = false; if (status) { @@ -413,7 +413,7 @@ export const addReaction = (statusId, name) => (dispatch, getState) => { } } if (!alreadyAdded) { - dispatch(addReactionRequest(statusId, name)); + dispatch(addReactionRequest(statusId, name, url)); } api(getState).post(`/api/v1/statuses/${statusId}/react/${name}`).then(() => { @@ -425,10 +425,11 @@ export const addReaction = (statusId, name) => (dispatch, getState) => { }); }; -export const addReactionRequest = (statusId, name) => ({ +export const addReactionRequest = (statusId, name, url) => ({ type: REACTION_ADD_REQUEST, id: statusId, name, + url, }); export const addReactionSuccess = (statusId, name) => ({ diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index 395840901d..999312fd02 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -118,7 +118,7 @@ class StatusActionBar extends ImmutablePureComponent { }; handleEmojiPick = data => { - this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, '')); + this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl); } handleReblogClick = e => { diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 5f77c54640..02a4d8350d 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -166,8 +166,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, - onReactionAdd (statusId, name) { - dispatch(addReaction(statusId, name)); + onReactionAdd (statusId, name, url) { + dispatch(addReaction(statusId, name, url)); }, onReactionRemove (statusId, name) { diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index c32904a4ab..2bf5225e54 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -81,7 +81,7 @@ class ActionBar extends React.PureComponent { }; handleEmojiPick = data => { - this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, '')); + this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl); } handleBookmarkClick = (e) => { diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index f9d6b45fa5..52967333f4 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -302,12 +302,12 @@ class Status extends ImmutablePureComponent { } }; - handleReactionAdd = (statusId, name) => { + handleReactionAdd = (statusId, name, url) => { const { dispatch } = this.props; const { signedIn } = this.context.identity; if (signedIn) { - dispatch(addReaction(statusId, name)); + dispatch(addReaction(statusId, name, url)); } else { dispatch(openModal('INTERACTION', { type: 'reaction_add', diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index e12dfb1795..4f7a6fc45c 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -59,11 +59,17 @@ const updateReaction = (state, id, name, updater) => state.update( const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count)); -const addReaction = (state, id, name) => updateReaction( +// The url parameter is only used when adding a new custom emoji reaction +// (one that wasn't in the reactions list before) because we don't have its +// URL yet. In all other cases, it's undefined. +const addReaction = (state, id, name, url) => updateReaction( state, id, name, - x => x.set('me', true).update('count', n => n + 1), + x => x.set('me', true) + .update('count', n => n + 1) + .update('url', old => old ? old : url) + .update('static_url', old => old ? old : url), ); const removeReaction = (state, id, name) => updateReaction( @@ -103,7 +109,7 @@ export default function statuses(state = initialState, action) { return updateReactionCount(state, action.reaction); case REACTION_ADD_REQUEST: case REACTION_REMOVE_FAIL: - return addReaction(state, action.id, action.name); + return addReaction(state, action.id, action.name, action.url); case REACTION_REMOVE_REQUEST: case REACTION_ADD_FAIL: return removeReaction(state, action.id, action.name); diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index fc9a8fdd5b..4f80b933d6 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -423,7 +423,7 @@ export function unpinFail(status, error) { }; } -export const addReaction = (statusId, name) => (dispatch, getState) => { +export const addReaction = (statusId, name, url) => (dispatch, getState) => { const status = getState().get('statuses').get(statusId); let alreadyAdded = false; if (status) { @@ -433,7 +433,7 @@ export const addReaction = (statusId, name) => (dispatch, getState) => { } } if (!alreadyAdded) { - dispatch(addReactionRequest(statusId, name)); + dispatch(addReactionRequest(statusId, name, url)); } api(getState).post(`/api/v1/statuses/${statusId}/react/${name}`).then(() => { @@ -445,10 +445,11 @@ export const addReaction = (statusId, name) => (dispatch, getState) => { }); }; -export const addReactionRequest = (statusId, name) => ({ +export const addReactionRequest = (statusId, name, url) => ({ type: REACTION_ADD_REQUEST, id: statusId, name, + url, }); export const addReactionSuccess = (statusId, name) => ({ diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 5f711c26a7..865d4fabac 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -135,8 +135,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, - onReactionAdd (statusId, name) { - dispatch(addReaction(statusId, name)); + onReactionAdd (statusId, name, url) { + dispatch(addReaction(statusId, name, url)); }, onReactionRemove (statusId, name) { diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index b1eeb86349..c604ef769e 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -257,12 +257,12 @@ class Status extends ImmutablePureComponent { } }; - handleReactionAdd = (statusId, name) => { + handleReactionAdd = (statusId, name, url) => { const { dispatch } = this.props; const { signedIn } = this.context.identity; if (signedIn) { - dispatch(addReaction(statusId, name)); + dispatch(addReaction(statusId, name, url)); } else { dispatch(openModal('INTERACTION', { type: 'reaction_add', diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 873f48356e..fe8542b945 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -57,11 +57,17 @@ const updateReaction = (state, id, name, updater) => state.update( const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count)); -const addReaction = (state, id, name) => updateReaction( +// The url parameter is only used when adding a new custom emoji reaction +// (one that wasn't in the reactions list before) because we don't have its +// URL yet. In all other cases, it's undefined. +const addReaction = (state, id, name, url) => updateReaction( state, id, name, - x => x.set('me', true).update('count', n => n + 1), + x => x.set('me', true) + .update('count', n => n + 1) + .update('url', old => old ? old : url) + .update('static_url', old => old ? old : url), ); const removeReaction = (state, id, name) => updateReaction( @@ -101,7 +107,7 @@ export default function statuses(state = initialState, action) { return updateReactionCount(state, action.reaction); case REACTION_ADD_REQUEST: case REACTION_REMOVE_FAIL: - return addReaction(state, action.id, action.name); + return addReaction(state, action.id, action.name, action.url); case REACTION_REMOVE_REQUEST: case REACTION_ADD_FAIL: return removeReaction(state, action.id, action.name); From 804bf4aa38ee8988cad6d967e0d14b2b4c04b253 Mon Sep 17 00:00:00 2001 From: fef Date: Sat, 3 Dec 2022 16:55:37 +0000 Subject: [PATCH 36/91] disable reaction button when not signed in --- .../flavours/glitch/containers/status_container.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 02a4d8350d..267939aab3 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -167,7 +167,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onReactionAdd (statusId, name, url) { - dispatch(addReaction(statusId, name, url)); + const { signedIn } = this.context.identity; + + if (signedIn) { + dispatch(addReaction(statusId, name, url)); + } }, onReactionRemove (statusId, name) { From 2a64c4d028d86650fee77c5516e24085f7b9c928 Mon Sep 17 00:00:00 2001 From: fef Date: Sun, 4 Dec 2022 08:47:24 +0000 Subject: [PATCH 37/91] also disable reaction buttons in vanilla flavour --- .../mastodon/components/status_action_bar.jsx | 2 +- .../mastodon/components/status_reactions.js | 11 +++++++---- .../features/status/components/action_bar.jsx | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 503558fe23..8f328f1d7e 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -370,7 +370,7 @@ class StatusActionBar extends ImmutablePureComponent { ); - const canReact = status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; + const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; const reactButton = ( { const { reaction, statusId, addReaction, removeReaction } = this.props; + const { signedIn } = this.context.identity; - if (reaction.get('me')) { - removeReaction(statusId, reaction.get('name')); - } else { - addReaction(statusId, reaction.get('name')); + if (signedIn) { + if (reaction.get('me')) { + removeReaction(statusId, reaction.get('name')); + } else { + addReaction(statusId, reaction.get('name')); + } } } diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 4ab5cdee14..989037c475 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -268,7 +268,7 @@ class ActionBar extends React.PureComponent { } } - const canReact = status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; + const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; const reactButton = ( Date: Sun, 4 Dec 2022 10:52:02 +0000 Subject: [PATCH 38/91] serialize custom emoji reactions properly for AP Akkoma and possibly others expect the `tag` field in an EmojiReact activity to be an array, not just a single object, so it's being wrapped into one now. I'm not entirely sure whether this is the idiomatic way of doing it tbh, but it works fine. --- app/serializers/activitypub/emoji_reaction_serializer.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/serializers/activitypub/emoji_reaction_serializer.rb b/app/serializers/activitypub/emoji_reaction_serializer.rb index 50f3efa3cb..f8887f15b7 100644 --- a/app/serializers/activitypub/emoji_reaction_serializer.rb +++ b/app/serializers/activitypub/emoji_reaction_serializer.rb @@ -3,8 +3,7 @@ class ActivityPub::EmojiReactionSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :content attribute :virtual_object, key: :object - - has_one :custom_emoji, key: :tag, serializer: ActivityPub::EmojiSerializer, unless: -> { object.custom_emoji.nil? } + attribute :custom_emoji, key: :tag, unless: -> { object.custom_emoji.nil? } def id [ActivityPub::TagManager.instance.uri_for(object.account), '#emoji_reactions/', object.id].join @@ -31,4 +30,10 @@ class ActivityPub::EmojiReactionSerializer < ActivityPub::Serializer end alias reaction content + + # Akkoma (and possibly others) expect `tag` to be an array, so we can't just + # use the has_one shorthand because we need to wrap it into an array manually + def custom_emoji + [ActivityPub::EmojiSerializer.new(object.custom_emoji).serializable_hash] + end end From e688fac3ec1b2cbc0fc91ec0822f819fad3afc6a Mon Sep 17 00:00:00 2001 From: fef Date: Sun, 4 Dec 2022 12:33:47 +0000 Subject: [PATCH 39/91] properly disable reactions when not logged in --- .../flavours/glitch/components/status.jsx | 2 + .../glitch/components/status_action_bar.jsx | 6 ++- .../glitch/components/status_reactions.js | 14 ++++--- .../glitch/containers/status_container.js | 6 +-- .../features/status/components/action_bar.jsx | 8 +++- .../status/components/detailed_status.jsx | 2 + .../flavours/glitch/features/status/index.jsx | 6 --- app/javascript/mastodon/components/status.jsx | 3 +- .../mastodon/components/status_action_bar.jsx | 8 +--- .../mastodon/components/status_reactions.js | 38 +++++++++---------- .../features/status/components/action_bar.jsx | 8 +++- .../status/components/detailed_status.jsx | 4 +- .../mastodon/features/status/index.jsx | 6 --- 13 files changed, 56 insertions(+), 55 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index a0a8cf8d1a..522ef6a19c 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -61,6 +61,7 @@ class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -825,6 +826,7 @@ class Status extends ImmutablePureComponent { numVisible={visibleReactions} addReaction={this.props.onReactionAdd} removeReaction={this.props.onReactionRemove} + canReact={this.context.identity.signedIn} /> {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? ( diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index 999312fd02..637984c336 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -329,7 +329,11 @@ class StatusActionBar extends ImmutablePureComponent { /> - + { + signedIn + ? + : reactButton + } {shareButton} diff --git a/app/javascript/flavours/glitch/components/status_reactions.js b/app/javascript/flavours/glitch/components/status_reactions.js index e263a64809..ff025e8d28 100644 --- a/app/javascript/flavours/glitch/components/status_reactions.js +++ b/app/javascript/flavours/glitch/components/status_reactions.js @@ -17,6 +17,7 @@ export default class StatusReactions extends ImmutablePureComponent { reactions: ImmutablePropTypes.list.isRequired, numVisible: PropTypes.number, addReaction: PropTypes.func.isRequired, + canReact: PropTypes.bool.isRequired, removeReaction: PropTypes.func.isRequired, }; @@ -56,6 +57,7 @@ export default class StatusReactions extends ImmutablePureComponent { style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }} addReaction={this.props.addReaction} removeReaction={this.props.removeReaction} + canReact={this.props.canReact} /> ))}
@@ -73,6 +75,7 @@ class Reaction extends ImmutablePureComponent { reaction: ImmutablePropTypes.map.isRequired, addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, + canReact: PropTypes.bool.isRequired, style: PropTypes.object, }; @@ -83,12 +86,10 @@ class Reaction extends ImmutablePureComponent { handleClick = () => { const { reaction, statusId, addReaction, removeReaction } = this.props; - if (!reaction.get('extern')) { - if (reaction.get('me')) { - removeReaction(statusId, reaction.get('name')); - } else { - addReaction(statusId, reaction.get('name')); - } + if (reaction.get('me')) { + removeReaction(statusId, reaction.get('name')); + } else { + addReaction(statusId, reaction.get('name')); } } @@ -105,6 +106,7 @@ class Reaction extends ImmutablePureComponent { onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} + disabled={!this.props.canReact} style={this.props.style} > diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 267939aab3..02a4d8350d 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -167,11 +167,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onReactionAdd (statusId, name, url) { - const { signedIn } = this.context.identity; - - if (signedIn) { - dispatch(addReaction(statusId, name, url)); - } + dispatch(addReaction(statusId, name, url)); }, onReactionRemove (statusId, name) { diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index 2bf5225e54..716253697b 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -236,7 +236,13 @@ class ActionBar extends React.PureComponent {
-
+
+ { + signedIn + ? + : reactButton + } +
{shareButton}
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx index 84def8e5e3..53b65b3ec7 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx @@ -26,6 +26,7 @@ class DetailedStatus extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -327,6 +328,7 @@ class DetailedStatus extends ImmutablePureComponent { reactions={status.get('reactions')} addReaction={this.props.onReactionAdd} removeReaction={this.props.onReactionRemove} + canReact={this.context.identity.signedIn} />
diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index 52967333f4..faf010faa2 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -308,12 +308,6 @@ class Status extends ImmutablePureComponent { if (signedIn) { dispatch(addReaction(statusId, name, url)); - } else { - dispatch(openModal('INTERACTION', { - type: 'reaction_add', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), - })); } } diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index b9fff79512..b32d254c9f 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -64,6 +64,7 @@ class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -563,7 +564,7 @@ class Status extends ImmutablePureComponent { numVisible={visibleReactions} addReaction={this.props.onReactionAdd} removeReaction={this.props.onReactionRemove} - emojiMap={this.props.emojiMap} + canReact={this.context.identity.signedIn} /> diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 8f328f1d7e..28303ee81e 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -130,13 +130,7 @@ class StatusActionBar extends ImmutablePureComponent { }; handleEmojiPick = data => { - const { signedIn } = this.context.identity; - - if (signedIn) { - this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, '')); - } else { - this.props.onInteractionModal('favourite', this.props.status); - } + this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl); } handleReblogClick = e => { diff --git a/app/javascript/mastodon/components/status_reactions.js b/app/javascript/mastodon/components/status_reactions.js index c16b7e8260..ff025e8d28 100644 --- a/app/javascript/mastodon/components/status_reactions.js +++ b/app/javascript/mastodon/components/status_reactions.js @@ -17,8 +17,8 @@ export default class StatusReactions extends ImmutablePureComponent { reactions: ImmutablePropTypes.list.isRequired, numVisible: PropTypes.number, addReaction: PropTypes.func.isRequired, + canReact: PropTypes.bool.isRequired, removeReaction: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, }; willEnter() { @@ -57,7 +57,7 @@ export default class StatusReactions extends ImmutablePureComponent { style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }} addReaction={this.props.addReaction} removeReaction={this.props.removeReaction} - emojiMap={this.props.emojiMap} + canReact={this.props.canReact} /> ))}
@@ -75,7 +75,7 @@ class Reaction extends ImmutablePureComponent { reaction: ImmutablePropTypes.map.isRequired, addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, + canReact: PropTypes.bool.isRequired, style: PropTypes.object, }; @@ -85,14 +85,11 @@ class Reaction extends ImmutablePureComponent { handleClick = () => { const { reaction, statusId, addReaction, removeReaction } = this.props; - const { signedIn } = this.context.identity; - if (signedIn) { - if (reaction.get('me')) { - removeReaction(statusId, reaction.get('name')); - } else { - addReaction(statusId, reaction.get('name')); - } + if (reaction.get('me')) { + removeReaction(statusId, reaction.get('name')); + } else { + addReaction(statusId, reaction.get('name')); } } @@ -109,10 +106,16 @@ class Reaction extends ImmutablePureComponent { onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} + disabled={!this.props.canReact} style={this.props.style} > - + @@ -127,12 +130,13 @@ class Emoji extends React.PureComponent { static propTypes = { emoji: PropTypes.string.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, hovered: PropTypes.bool.isRequired, + url: PropTypes.string, + staticUrl: PropTypes.string, }; render() { - const { emoji, emojiMap, hovered } = this.props; + const { emoji, hovered, url, staticUrl } = this.props; if (unicodeMapping[emoji]) { const { filename, shortCode } = unicodeMapping[this.props.emoji]; @@ -147,10 +151,8 @@ class Emoji extends React.PureComponent { src={`${assetHost}/emoji/${filename}.svg`} /> ); - } else if (emojiMap.get(emoji)) { - const filename = (autoPlayGif || hovered) - ? emojiMap.getIn([emoji, 'url']) - : emojiMap.getIn([emoji, 'static_url']); + } else { + const filename = (autoPlayGif || hovered) ? url : staticUrl; const shortCode = `:${emoji}:`; return ( @@ -162,8 +164,6 @@ class Emoji extends React.PureComponent { src={filename} /> ); - } else { - return null; } } diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 989037c475..11f4d7aeb2 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -308,7 +308,13 @@ class ActionBar extends React.PureComponent {
-
+
+ { + canReact + ? + : reactButton + } +
{shareButton} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index ae913895bd..d0af14c6a7 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -30,6 +30,7 @@ class DetailedStatus extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -50,7 +51,6 @@ class DetailedStatus extends ImmutablePureComponent { onToggleMediaVisibility: PropTypes.func, onReactionAdd: PropTypes.func.isRequired, onReactionRemove: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, }; state = { @@ -292,7 +292,7 @@ class DetailedStatus extends ImmutablePureComponent { reactions={status.get('reactions')} addReaction={this.props.onReactionAdd} removeReaction={this.props.onReactionRemove} - emojiMap={this.props.emojiMap} + canReact={this.context.identity.signedIn} />
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index c604ef769e..0c29af8117 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -263,12 +263,6 @@ class Status extends ImmutablePureComponent { if (signedIn) { dispatch(addReaction(statusId, name, url)); - } else { - dispatch(openModal('INTERACTION', { - type: 'reaction_add', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), - })); } } From f87de8770b51f64985bb911255371fd55a8de09a Mon Sep 17 00:00:00 2001 From: fef Date: Wed, 7 Dec 2022 12:19:36 +0000 Subject: [PATCH 40/91] support reacting with foreign custom emojis --- app/models/status_reaction.rb | 7 +------ app/serializers/rest/reaction_serializer.rb | 16 +++++++++++----- app/validators/status_reaction_validator.rb | 4 ---- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/models/status_reaction.rb b/app/models/status_reaction.rb index 4793ff9aa3..0833a7eafd 100644 --- a/app/models/status_reaction.rb +++ b/app/models/status_reaction.rb @@ -24,11 +24,6 @@ class StatusReaction < ApplicationRecord private def set_custom_emoji - return if name.blank? - self.custom_emoji = if account.local? - CustomEmoji.local.find_by(disabled: false, shortcode: name) - else - CustomEmoji.find_by(shortcode: name, domain: account.domain) - end + self.custom_emoji = CustomEmoji.find_by(shortcode: name, domain: account.domain) if name.blank? end end diff --git a/app/serializers/rest/reaction_serializer.rb b/app/serializers/rest/reaction_serializer.rb index 007d3b5015..b0f0732bf7 100644 --- a/app/serializers/rest/reaction_serializer.rb +++ b/app/serializers/rest/reaction_serializer.rb @@ -3,7 +3,7 @@ class REST::ReactionSerializer < ActiveModel::Serializer include RoutingHelper - attributes :name, :count, :extern + attributes :name, :count attribute :me, if: :current_user? attribute :url, if: :custom_emoji? @@ -21,11 +21,11 @@ class REST::ReactionSerializer < ActiveModel::Serializer object.custom_emoji.present? end - def extern - if custom_emoji? - object.custom_emoji.domain.present? + def name + if extern? + [object.name, '@', object.custom_emoji.domain].join else - false + object.name end end @@ -36,4 +36,10 @@ class REST::ReactionSerializer < ActiveModel::Serializer def static_url full_asset_url(object.custom_emoji.image.url(:static)) end + + private + + def extern? + custom_emoji? && object.custom_emoji.domain.present? + end end diff --git a/app/validators/status_reaction_validator.rb b/app/validators/status_reaction_validator.rb index a60271dd84..d85d48e4c7 100644 --- a/app/validators/status_reaction_validator.rb +++ b/app/validators/status_reaction_validator.rb @@ -18,10 +18,6 @@ class StatusReactionValidator < ActiveModel::Validator SUPPORTED_EMOJIS.include?(name) end - def new_reaction?(reaction) - !reaction.status.status_reactions.where(name: reaction.name).exists? - end - def limit_reached?(reaction) reaction.status.status_reactions.where(status: reaction.status, account: reaction.account).count >= LIMIT end From 6fa408f1a0bf49ba45b7982f9aa99fd30f2e2cb5 Mon Sep 17 00:00:00 2001 From: fef Date: Wed, 7 Dec 2022 12:47:03 +0000 Subject: [PATCH 41/91] delete reaction notifications when deleting status --- app/models/status_reaction.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/status_reaction.rb b/app/models/status_reaction.rb index 0833a7eafd..00be17e231 100644 --- a/app/models/status_reaction.rb +++ b/app/models/status_reaction.rb @@ -16,6 +16,8 @@ class StatusReaction < ApplicationRecord belongs_to :status, inverse_of: :status_reactions belongs_to :custom_emoji, optional: true + has_one :notification, as: :activity, dependent: :destroy + validates :name, presence: true validates_with StatusReactionValidator From 4226a5ddc88b907ab04bedbad8dbc00ee09f07b8 Mon Sep 17 00:00:00 2001 From: fef Date: Wed, 7 Dec 2022 21:52:53 +0100 Subject: [PATCH 42/91] fix schema after rebase --- db/schema.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/db/schema.rb b/db/schema.rb index e834d7e09b..da6c611a87 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -924,6 +924,19 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do t.index ["status_id"], name: "index_status_pins_on_status_id" end + create_table "status_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.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["account_id", "status_id", "name"], name: "index_status_reactions_on_account_id_and_status_id", unique: true + t.index ["account_id"], name: "index_status_reactions_on_account_id" + t.index ["custom_emoji_id"], name: "index_status_reactions_on_custom_emoji_id" + t.index ["status_id"], name: "index_status_reactions_on_status_id" + end + create_table "status_stats", force: :cascade do |t| t.bigint "status_id", null: false t.bigint "replies_count", default: 0, null: false @@ -1242,6 +1255,9 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do add_foreign_key "status_edits", "statuses", on_delete: :cascade add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade add_foreign_key "status_pins", "statuses", on_delete: :cascade + add_foreign_key "status_reactions", "accounts", on_delete: :cascade + add_foreign_key "status_reactions", "custom_emojis", on_delete: :cascade + add_foreign_key "status_reactions", "statuses", on_delete: :cascade add_foreign_key "status_stats", "statuses", on_delete: :cascade add_foreign_key "status_trends", "accounts", on_delete: :cascade add_foreign_key "status_trends", "statuses", on_delete: :cascade From 8d9105e4c2b1e1c9da59cb09a0e12361615cfe1d Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 8 Dec 2022 09:48:55 +0000 Subject: [PATCH 43/91] fix status action bar after upstream changes --- app/javascript/mastodon/components/status_action_bar.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 28303ee81e..286cca0a71 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -380,7 +380,11 @@ class StatusActionBar extends ImmutablePureComponent { - + { + signedIn + ? + : reactButton + } {shareButton} From 5ec5a782d415a683be9d78cdc79bfa7501f0c50f Mon Sep 17 00:00:00 2001 From: fef Date: Sun, 11 Dec 2022 13:26:23 +0000 Subject: [PATCH 44/91] fix 404 when reacting with Keycap Number Sign The Unicode sequence for this emoji starts with an ASCII # character, which the browser's URI parser truncates before sending the request to the backend. --- app/javascript/flavours/glitch/actions/interactions.js | 6 ++++-- app/javascript/mastodon/actions/interactions.js | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index da06afadf1..8f515c990c 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -416,7 +416,9 @@ export const addReaction = (statusId, name, url) => (dispatch, getState) => { dispatch(addReactionRequest(statusId, name, url)); } - api(getState).post(`/api/v1/statuses/${statusId}/react/${name}`).then(() => { + // encodeURIComponent is required for the Keycap Number Sign emoji, see: + // + api(getState).post(`/api/v1/statuses/${statusId}/react/${encodeURIComponent(name)}`).then(() => { dispatch(addReactionSuccess(statusId, name)); }).catch(err => { if (!alreadyAdded) { @@ -448,7 +450,7 @@ export const addReactionFail = (statusId, name, error) => ({ export const removeReaction = (statusId, name) => (dispatch, getState) => { dispatch(removeReactionRequest(statusId, name)); - api(getState).post(`/api/v1/statuses/${statusId}/unreact/${name}`).then(() => { + api(getState).post(`/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(name)}`).then(() => { dispatch(removeReactionSuccess(statusId, name)); }).catch(err => { dispatch(removeReactionFail(statusId, name, err)); diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 4f80b933d6..28c728d61e 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -436,7 +436,9 @@ export const addReaction = (statusId, name, url) => (dispatch, getState) => { dispatch(addReactionRequest(statusId, name, url)); } - api(getState).post(`/api/v1/statuses/${statusId}/react/${name}`).then(() => { + // encodeURIComponent is required for the Keycap Number Sign emoji, see: + // + api(getState).post(`/api/v1/statuses/${statusId}/react/${encodeURIComponent(name)}`).then(() => { dispatch(addReactionSuccess(statusId, name)); }).catch(err => { if (!alreadyAdded) { @@ -468,7 +470,7 @@ export const addReactionFail = (statusId, name, error) => ({ export const removeReaction = (statusId, name) => (dispatch, getState) => { dispatch(removeReactionRequest(statusId, name)); - api(getState).post(`/api/v1/statuses/${statusId}/unreact/${name}`).then(() => { + api(getState).post(`/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(name)}`).then(() => { dispatch(removeReactionSuccess(statusId, name)); }).catch(err => { dispatch(removeReactionFail(statusId, name, err)); From 5e46bec4854d94d6807c6b5f49a82c9a5b0606ad Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 15 Dec 2022 15:27:54 +0000 Subject: [PATCH 45/91] bypass reaction limit for foreign accounts --- app/validators/status_reaction_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/validators/status_reaction_validator.rb b/app/validators/status_reaction_validator.rb index d85d48e4c7..8c623c823d 100644 --- a/app/validators/status_reaction_validator.rb +++ b/app/validators/status_reaction_validator.rb @@ -9,7 +9,7 @@ class StatusReactionValidator < ActiveModel::Validator return if reaction.name.blank? reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name) - reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if limit_reached?(reaction) + reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if reaction.account.local? && limit_reached?(reaction) end private From e8c9054e74f1a0145a37ba78bbc04c49dd03d03b Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sun, 18 Dec 2022 04:23:42 +0100 Subject: [PATCH 46/91] Fix status reactions preventing an on_cascade delete --- db/migrate/20221124114030_create_status_reactions.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/migrate/20221124114030_create_status_reactions.rb b/db/migrate/20221124114030_create_status_reactions.rb index bbf1f3376a..5f010c4a0b 100644 --- a/db/migrate/20221124114030_create_status_reactions.rb +++ b/db/migrate/20221124114030_create_status_reactions.rb @@ -1,10 +1,10 @@ class CreateStatusReactions < ActiveRecord::Migration[6.1] def change create_table :status_reactions do |t| - t.references :account, null: false, foreign_key: true - t.references :status, null: false, foreign_key: true + t.references :account, null: false, foreign_key: { on_delete: :cascade } + t.references :status, null: false, foreign_key: { on_delete: :cascade } t.string :name, null: false, default: '' - t.references :custom_emoji, null: true, foreign_key: true + t.references :custom_emoji, null: true, foreign_key: { on_delete: :cascade } t.timestamps end From 9c69772dc6e42d8ebf7733ccf2efabba113cf823 Mon Sep 17 00:00:00 2001 From: fef Date: Tue, 20 Dec 2022 17:19:56 +0000 Subject: [PATCH 47/91] move emoji reaction strings to locales-glitch --- config/locales-glitch/de.yml | 5 +++++ config/locales-glitch/en.yml | 5 +++++ config/locales-glitch/fr.yml | 5 +++++ config/locales-glitch/simple_form.de.yml | 2 ++ config/locales-glitch/simple_form.en.yml | 1 + config/locales-glitch/simple_form.fr.yml | 2 ++ config/locales/de.yml | 4 ---- config/locales/en.yml | 4 ---- config/locales/fr.yml | 4 ---- config/locales/simple_form.de.yml | 1 - config/locales/simple_form.en.yml | 1 - config/locales/simple_form.fr.yml | 1 - 12 files changed, 20 insertions(+), 15 deletions(-) diff --git a/config/locales-glitch/de.yml b/config/locales-glitch/de.yml index 233bf91b38..acef54ecd3 100644 --- a/config/locales-glitch/de.yml +++ b/config/locales-glitch/de.yml @@ -40,3 +40,8 @@ de: use_this: Benutze das settings: flavours: Varianten + notification_mailer: + reaction: + body: "%{name} hat auf deinen Beitrag reagiert:" + subject: "%{name} hat auf deinen Beitrag reagiert" + title: Neue Reaktion diff --git a/config/locales-glitch/en.yml b/config/locales-glitch/en.yml index 1485ee174d..e88e98191e 100644 --- a/config/locales-glitch/en.yml +++ b/config/locales-glitch/en.yml @@ -40,3 +40,8 @@ en: use_this: Use this settings: flavours: Flavours + notification_mailer: + reaction: + body: "%{name} reacted to your post:" + subject: "%{name} reacted to your post" + title: New reaction diff --git a/config/locales-glitch/fr.yml b/config/locales-glitch/fr.yml index 15c3f8ce52..770f5d4d9e 100644 --- a/config/locales-glitch/fr.yml +++ b/config/locales-glitch/fr.yml @@ -40,3 +40,8 @@ fr: use_this: Utiliser ceci settings: flavours: Thèmes + notification_mailer: + reaction: + body: "%{name} a réagi·e à votre message:" + subject: "%{name} a réagi·e à votre message" + title: Nouvelle réaction diff --git a/config/locales-glitch/simple_form.de.yml b/config/locales-glitch/simple_form.de.yml index 0d92038c58..293f851263 100644 --- a/config/locales-glitch/simple_form.de.yml +++ b/config/locales-glitch/simple_form.de.yml @@ -1,6 +1,7 @@ --- de: simple_form: +<<<<<<< HEAD glitch_only: glitch-soc hints: defaults: @@ -21,6 +22,7 @@ de: setting_hide_followers_count: Anzahl der Follower verbergen setting_skin: Skin setting_system_emoji_font: Systemschriftart für Emojis verwenden (nur für Glitch-Variante) + setting_visible_reactions: Anzahl der sichtbaren Emoji-Reaktionen notification_emails: trending_link: Neuer angesagter Link muss überprüft werden trending_status: Neuer angesagter Post muss überprüft werden diff --git a/config/locales-glitch/simple_form.en.yml b/config/locales-glitch/simple_form.en.yml index 6930c09a63..a67702f935 100644 --- a/config/locales-glitch/simple_form.en.yml +++ b/config/locales-glitch/simple_form.en.yml @@ -21,6 +21,7 @@ en: setting_hide_followers_count: Hide your followers count setting_skin: Skin setting_system_emoji_font: Use system's default font for emojis (applies to Glitch flavour only) + setting_visible_reactions: Number of visible emoji reactions notification_emails: trending_link: New trending link requires review trending_status: New trending post requires review diff --git a/config/locales-glitch/simple_form.fr.yml b/config/locales-glitch/simple_form.fr.yml index bc6302cf89..98489f9466 100644 --- a/config/locales-glitch/simple_form.fr.yml +++ b/config/locales-glitch/simple_form.fr.yml @@ -21,7 +21,9 @@ fr: setting_hide_followers_count: Cacher votre nombre d'abonné·e·s setting_skin: Thème setting_system_emoji_font: Utiliser la police par défaut du système pour les émojis (s'applique uniquement au mode Glitch) + setting_visible_reactions: Nombre de réactions emoji visibles notification_emails: trending_link: Un nouveau lien en tendances nécessite un examen trending_status: Un nouveau post en tendances nécessite un examen trending_tag: Un nouveau tag en tendances nécessite un examen + setting_visible_reactions: Nombre de réactions emoji visibles diff --git a/config/locales/de.yml b/config/locales/de.yml index 97b5d9fc81..5b8f7effe5 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1356,10 +1356,6 @@ de: title: Neue Erwähnung poll: subject: Eine Umfrage von %{name} ist beendet - reaction: - body: "%{name} hat auf deinen Beitrag reagiert:" - subject: "%{name} hat auf deinen Beitrag reagiert" - title: Neue Reaktion reblog: body: 'Deinen Beitrag hat %{name} geteilt:' subject: "%{name} teilte deinen Beitrag" diff --git a/config/locales/en.yml b/config/locales/en.yml index be4cfd89ae..76198763a4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1400,10 +1400,6 @@ en: title: New mention poll: subject: A poll by %{name} has ended - reaction: - body: '%{name} reacted to your post:' - subject: '%{name} reacted to your post' - title: New reaction reblog: body: 'Your post was boosted by %{name}:' subject: "%{name} boosted your post" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d14aadbeab..5efc1e585b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1340,10 +1340,6 @@ fr: title: Nouvelle mention poll: subject: Un sondage de %{name} est terminé - reaction: - body: "%{name} a réagi·e à votre message:" - subject: "%{name} a réagi·e à votre message" - title: Nouvelle réaction reblog: body: 'Votre message été partagé par %{name} :' subject: "%{name} a partagé votre message" diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 8e7b4ce4fc..d471b3a896 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -217,7 +217,6 @@ de: setting_unfollow_modal: Bestätigungsdialog anzeigen, bevor jemandem entfolgt wird setting_use_blurhash: Farbverlauf für verborgene Medien anzeigen setting_use_pending_items: Langsamer Modus - setting_visible_reactions: Anzahl der sichtbaren Emoji-Reaktionen severity: Einschränkung sign_in_token_attempt: Sicherheitsschlüssel title: Titel diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 96e78e9e6c..b646a15e26 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -217,7 +217,6 @@ en: setting_unfollow_modal: Show confirmation dialog before unfollowing someone setting_use_blurhash: Show colorful gradients for hidden media setting_use_pending_items: Slow mode - setting_visible_reactions: Number of visible emoji reactions severity: Severity sign_in_token_attempt: Security code title: Title diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index b3e6e5bf9a..d4697fa1e9 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -216,7 +216,6 @@ fr: setting_unfollow_modal: Demander confirmation avant de vous désabonner d’un compte setting_use_blurhash: Afficher des dégradés colorés pour les médias cachés setting_use_pending_items: Mode lent - setting_visible_reactions: Nombre de réactions emoji visibles severity: Sévérité sign_in_token_attempt: Code de sécurité title: Nom From ee4e497cc692c90541c3794e20f95b94b5f66549 Mon Sep 17 00:00:00 2001 From: neatchee Date: Thu, 26 Jan 2023 10:22:15 -0800 Subject: [PATCH 48/91] Per PR suggestion, split name and domain, and look for emoji ID, for unreact, so remote emoji's can be unreacted --- app/services/unreact_service.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/services/unreact_service.rb b/app/services/unreact_service.rb index f4089f33b5..d449570c7a 100644 --- a/app/services/unreact_service.rb +++ b/app/services/unreact_service.rb @@ -3,8 +3,10 @@ class UnreactService < BaseService include Payloadable - def call(account, status, name) - reaction = StatusReaction.find_by(account: account, status: status, name: name) +def call(account, status, emoji) + name, domain = emoji.split('@') + custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) + reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) return if reaction.nil? reaction.destroy! From bbce42a7cb59c6b6247c62d4c7ea79b0769a0122 Mon Sep 17 00:00:00 2001 From: neatchee Date: Thu, 26 Jan 2023 11:32:03 -0800 Subject: [PATCH 49/91] Fix rebase issues --- .../flavours/glitch/components/status_action_bar.jsx | 2 +- app/javascript/flavours/glitch/features/status/index.jsx | 9 --------- .../mastodon/features/status/components/action_bar.jsx | 4 ---- config/locales-glitch/simple_form.de.yml | 1 - 4 files changed, 1 insertion(+), 15 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index 637984c336..0d82ecddc2 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -330,7 +330,7 @@ class StatusActionBar extends ImmutablePureComponent { { - signedIn + permissions ? : reactButton } diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index faf010faa2..ee83a3773e 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -42,11 +42,7 @@ import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { initBoostModal } from 'flavours/glitch/actions/boosts'; -<<<<<<< HEAD import { makeCustomEmojiMap, makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; -======= -import { makeGetStatus } from 'flavours/glitch/selectors'; ->>>>>>> f0197c80d (display external custom emoji reactions properly) import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ColumnHeader from '../../components/column_header'; import StatusContainer from 'flavours/glitch/containers/status_container'; @@ -708,12 +704,7 @@ class Status extends ImmutablePureComponent { domain={domain} showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} -<<<<<<< HEAD pictureInPicture={pictureInPicture} - emojiMap={this.props.emojiMap} -======= - usingPiP={usingPiP} ->>>>>>> f0197c80d (display external custom emoji reactions properly) /> >>>>>> 7cbe6dc3e (rebase with upstream) const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, diff --git a/config/locales-glitch/simple_form.de.yml b/config/locales-glitch/simple_form.de.yml index 293f851263..57446e7069 100644 --- a/config/locales-glitch/simple_form.de.yml +++ b/config/locales-glitch/simple_form.de.yml @@ -1,7 +1,6 @@ --- de: simple_form: -<<<<<<< HEAD glitch_only: glitch-soc hints: defaults: From 1987dd766af32fcfe2661031270cc5d45555781f Mon Sep 17 00:00:00 2001 From: Ivan Rodriguez <104603218+IRod22@users.noreply.github.com> Date: Tue, 7 Mar 2023 23:21:32 -0600 Subject: [PATCH 50/91] Keep emoji picker within screen bounds Adds the `flip` prop to ``. Fixes #40 --- .../features/compose/components/emoji_picker_dropdown.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index a9c0aa4f21..b634dc780a 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -390,7 +390,7 @@ class EmojiPickerDropdown extends React.PureComponent { />}
- + {({ props, placement })=> (
From 8e71bfde83ddf4d43167f6dfc917c7fd02822616 Mon Sep 17 00:00:00 2001 From: neatchee Date: Wed, 8 Mar 2023 13:27:25 -0800 Subject: [PATCH 51/91] Remove old .js locale files accidentally restored during rebase --- app/javascript/flavours/glitch/locales/de.js | 12 -- app/javascript/flavours/glitch/locales/en.js | 197 ------------------- app/javascript/flavours/glitch/locales/fr.js | 12 -- 3 files changed, 221 deletions(-) delete mode 100644 app/javascript/flavours/glitch/locales/de.js delete mode 100644 app/javascript/flavours/glitch/locales/en.js delete mode 100644 app/javascript/flavours/glitch/locales/fr.js diff --git a/app/javascript/flavours/glitch/locales/de.js b/app/javascript/flavours/glitch/locales/de.js deleted file mode 100644 index a4daa59495..0000000000 --- a/app/javascript/flavours/glitch/locales/de.js +++ /dev/null @@ -1,12 +0,0 @@ -import inherited from 'mastodon/locales/de.json'; - -const messages = { - 'notification.reaction': '{name} hat auf deinen Beitrag reagiert', - 'notifications.column_settings.reaction': 'Reaktionen:', - - 'tooltips.reactions': 'Reaktionen', - - 'status.react': 'Reagieren', -}; - -export default Object.assign({}, inherited, messages); diff --git a/app/javascript/flavours/glitch/locales/en.js b/app/javascript/flavours/glitch/locales/en.js deleted file mode 100644 index f6918fc4a5..0000000000 --- a/app/javascript/flavours/glitch/locales/en.js +++ /dev/null @@ -1,197 +0,0 @@ -import inherited from 'mastodon/locales/en.json'; - -const messages = { - 'getting_started.open_source_notice': 'Glitchsoc is free open source software forked from {Mastodon}. You can contribute or report issues on GitHub at {github}.', - 'layout.auto': 'Auto', - 'layout.current_is': 'Your current layout is:', - 'layout.desktop': 'Desktop', - 'layout.single': 'Mobile', - 'layout.hint.auto': 'Automatically chose layout based on “Enable advanced web interface” setting and screen size.', - 'layout.hint.desktop': 'Use multiple-column layout regardless of the “Enable advanced web interface” setting or screen size.', - 'layout.hint.single': 'Use single-column layout regardless of the “Enable advanced web interface” setting or screen size.', - 'navigation_bar.app_settings': 'App settings', - 'navigation_bar.misc': 'Misc', - 'navigation_bar.keyboard_shortcuts': 'Keyboard shortcuts', - 'navigation_bar.info': 'Extended information', - 'navigation_bar.featured_users': 'Featured users', - 'getting_started.onboarding': 'Show me around', - 'onboarding.next': 'Next', - 'onboarding.done': 'Done', - 'onboarding.skip': 'Skip', - 'onboarding.page_one.federation': '{domain} is an \'instance\' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.', - 'onboarding.page_one.welcome': 'Welcome to {domain}!', - 'onboarding.page_one.handle': 'You are on {domain}, so your full handle is {handle}', - 'onboarding.page_two.compose': 'Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.', - 'onboarding.page_three.search': 'Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.', - 'onboarding.page_three.profile': 'Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.', - 'onboarding.page_four.home': 'The home timeline shows posts from people you follow.', - 'onboarding.page_four.notifications': 'The notifications column shows when someone interacts with you.', - 'onboarding.page_five.public_timelines': 'The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.', - 'onboarding.page_six.admin': 'Your instance\'s admin is {admin}.', - 'onboarding.page_six.read_guidelines': 'Please read {domain}\'s {guidelines}!', - 'onboarding.page_six.guidelines': 'community guidelines', - 'onboarding.page_six.almost_done': 'Almost done...', - 'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.', - 'onboarding.page_six.apps_available': 'There are {apps} available for iOS, Android and other platforms.', - 'onboarding.page_six.various_app': 'mobile apps', - 'onboarding.page_six.appetoot': 'Bon Appetoot!', - 'settings.auto_collapse': 'Automatic collapsing', - 'settings.auto_collapse_all': 'Everything', - 'settings.auto_collapse_lengthy': 'Lengthy toots', - 'settings.auto_collapse_media': 'Toots with media', - 'settings.auto_collapse_notifications': 'Notifications', - 'settings.auto_collapse_reblogs': 'Boosts', - 'settings.auto_collapse_replies': 'Replies', - 'settings.show_action_bar': 'Show action buttons in collapsed toots', - 'settings.close': 'Close', - 'settings.collapsed_statuses': 'Collapsed toots', - 'settings.enable_collapsed': 'Enable collapsed toots', - 'settings.enable_collapsed_hint': 'Collapsed posts have parts of their contents hidden to take up less screen space. This is distinct from the Content Warning feature', - 'settings.general': 'General', - 'settings.compose_box_opts': 'Compose box', - 'settings.side_arm': 'Secondary toot button:', - 'settings.side_arm.none': 'None', - 'settings.side_arm_reply_mode': 'When replying to a toot, the secondary toot button should:', - 'settings.side_arm_reply_mode.keep': 'Keep its set privacy', - 'settings.side_arm_reply_mode.copy': 'Copy privacy setting of the toot being replied to', - 'settings.side_arm_reply_mode.restrict': 'Restrict privacy setting to that of the toot being replied to', - 'settings.always_show_spoilers_field': 'Always enable the Content Warning field', - 'settings.prepend_cw_re': 'Prepend “re: ” to content warnings when replying', - 'settings.preselect_on_reply': 'Pre-select usernames on reply', - 'settings.preselect_on_reply_hint': 'When replying to a conversation with multiple participants, pre-select usernames past the first', - 'settings.confirm_missing_media_description': 'Show confirmation dialog before sending toots lacking media descriptions', - 'settings.confirm_before_clearing_draft': 'Show confirmation dialog before overwriting the message being composed', - 'settings.show_content_type_choice': 'Show content-type choice when authoring toots', - 'settings.content_warnings': 'Content Warnings', - 'settings.content_warnings.regexp': 'Regular expression', - 'settings.content_warnings_shared_state': 'Show/hide content of all copies at once', - 'settings.content_warnings_shared_state_hint': 'Reproduce upstream Mastodon behavior by having the Content Warning button affect all copies of a post at once. This will prevent automatic collapsing of any copy of a toot with unfolded CW', - 'settings.content_warnings_media_outside': 'Display media attachments outside content warnings', - 'settings.content_warnings_media_outside_hint': 'Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments', - 'settings.content_warnings_unfold_opts': 'Auto-unfolding options', - 'settings.enable_content_warnings_auto_unfold': 'Automatically unfold content-warnings', - 'settings.deprecated_setting': 'This setting is now controlled from Mastodon\'s {settings_page_link}', - 'settings.shared_settings_link': 'user preferences', - 'settings.content_warnings_filter': 'Content warnings to not automatically unfold:', - 'settings.layout_opts': 'Layout options', - 'settings.rewrite_mentions_no': 'Do not rewrite mentions', - 'settings.rewrite_mentions_acct': 'Rewrite with username and domain (when the account is remote)', - 'settings.rewrite_mentions_username': 'Rewrite with username', - 'settings.show_reply_counter': 'Display an estimate of the reply count', - 'settings.hicolor_privacy_icons': 'High color privacy icons', - 'settings.hicolor_privacy_icons.hint': 'Display privacy icons in bright and easily distinguishable colors', - 'settings.confirm_boost_missing_media_description': 'Show confirmation dialog before boosting toots lacking media descriptions', - 'settings.tag_misleading_links': 'Tag misleading links', - 'settings.tag_misleading_links.hint': 'Add a visual indication with the link target host to every link not mentioning it explicitly', - 'settings.rewrite_mentions': 'Rewrite mentions in displayed statuses', - 'settings.notifications_opts': 'Notifications options', - 'settings.notifications.tab_badge': 'Unread notifications badge', - 'settings.notifications.tab_badge.hint': 'Display a badge for unread notifications in the column icons when the notifications column isn\'t open', - 'settings.notifications.favicon_badge': 'Unread notifications favicon badge', - 'settings.notifications.favicon_badge.hint': 'Add a badge for unread notifications to the favicon', - 'settings.status_icons': 'Toot icons', - 'settings.status_icons_language': 'Language indicator', - 'settings.status_icons_reply': 'Reply indicator', - 'settings.status_icons_local_only': 'Local-only indicator', - 'settings.status_icons_media': 'Media and poll indicators', - 'settings.status_icons_visibility': 'Toot privacy indicator', - 'settings.layout': 'Layout:', - 'settings.image_backgrounds': 'Image backgrounds', - 'settings.image_backgrounds_media': 'Preview collapsed toot media', - 'settings.image_backgrounds_media_hint': 'If the post has any media attachment, use the first one as a background', - 'settings.image_backgrounds_users': 'Give collapsed toots an image background', - 'settings.media': 'Media', - 'settings.media_letterbox': 'Letterbox media', - 'settings.media_letterbox_hint': 'Scale down and letterbox media to fill the image containers instead of stretching and cropping them', - 'settings.media_fullwidth': 'Full-width media previews', - 'settings.inline_preview_cards': 'Inline preview cards for external links', - 'settings.media_reveal_behind_cw': 'Reveal sensitive media behind a CW by default', - 'settings.pop_in_player': 'Enable pop-in player', - 'settings.pop_in_position': 'Pop-in player position:', - 'settings.pop_in_left': 'Left', - 'settings.pop_in_right': 'Right', - 'settings.preferences': 'User preferences', - 'settings.wide_view': 'Wide view (Desktop mode only)', - 'settings.wide_view_hint': 'Stretches columns to better fill the available space.', - 'settings.navbar_under': 'Navbar at the bottom (Mobile only)', - 'status.collapse': 'Collapse', - 'status.react': 'React', - 'status.uncollapse': 'Uncollapse', - 'status.in_reply_to': 'This toot is a reply', - 'status.has_preview_card': 'Features an attached preview card', - 'status.has_pictures': 'Features attached pictures', - 'status.is_poll': 'This toot is a poll', - 'status.has_video': 'Features attached videos', - 'status.has_audio': 'Features attached audio files', - 'status.local_only': 'Only visible from your instance', - - 'content_type.change': 'Content type', - 'compose.content-type.html': 'HTML', - 'compose.content-type.markdown': 'Markdown', - 'compose.content-type.plain': 'Plain text', - - 'compose_form.poll.single_choice': 'Allow one choice', - 'compose_form.poll.multiple_choices': 'Allow multiple choices', - 'compose_form.spoiler': 'Hide text behind warning', - - 'column.toot': 'Toots and replies', - 'column_header.profile': 'Profile', - 'column.heading': 'Misc', - 'column.subheading': 'Miscellaneous options', - 'column_subheading.navigation': 'Navigation', - 'column_subheading.lists': 'Lists', - - 'media_gallery.sensitive': 'Sensitive', - - 'favourite_modal.combo': 'You can press {combo} to skip this next time', - - 'home.column_settings.show_direct': 'Show DMs', - - 'notification.markForDeletion': 'Mark for deletion', - 'notification.reaction': '{name} reacted to your post', - 'notifications.clear': 'Clear all my notifications', - 'notifications.column_settings.reaction': 'Reactions:', - 'notifications.marked_clear_confirmation': 'Are you sure you want to permanently clear all selected notifications?', - 'notifications.marked_clear': 'Clear selected notifications', - - 'notification_purge.start': 'Enter notification cleaning mode', - 'notification_purge.btn_all': 'Select\nall', - 'notification_purge.btn_none': 'Select\nnone', - 'notification_purge.btn_invert': 'Invert\nselection', - 'notification_purge.btn_apply': 'Clear\nselected', - - 'compose.attach.upload': 'Upload a file', - 'compose.attach.doodle': 'Draw something', - 'compose.attach': 'Attach...', - - 'advanced_options.local-only.short': 'Local-only', - 'advanced_options.local-only.long': 'Do not post to other instances', - 'advanced_options.local-only.tooltip': 'This post is local-only', - 'advanced_options.icon_title': 'Advanced options', - 'advanced_options.threaded_mode.short': 'Threaded mode', - 'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting', - 'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled', - - 'endorsed_accounts_editor.endorsed_accounts': 'Featured accounts', - - 'account.add_account_note': 'Add note for @{name}', - 'account_note.cancel': 'Cancel', - 'account_note.save': 'Save', - 'account_note.edit': 'Edit', - 'account_note.glitch_placeholder': 'No comment provided', - 'account.joined': 'Joined {date}', - 'account.follows': 'Follows', - - 'home.column_settings.advanced': 'Advanced', - 'home.column_settings.filter_regex': 'Filter out by regular expressions', - 'direct.group_by_conversations': 'Group by conversation', - 'community.column_settings.allow_local_only': 'Show local-only toots', - - 'keyboard_shortcuts.bookmark': 'to bookmark', - 'keyboard_shortcuts.toggle_collapse': 'to collapse/uncollapse toots', - 'keyboard_shortcuts.secondary_toot': 'to send toot using secondary privacy setting', - - 'tooltips.reactions': 'Reactions', -}; - -export default Object.assign({}, inherited, messages); diff --git a/app/javascript/flavours/glitch/locales/fr.js b/app/javascript/flavours/glitch/locales/fr.js deleted file mode 100644 index 3ebe055e0f..0000000000 --- a/app/javascript/flavours/glitch/locales/fr.js +++ /dev/null @@ -1,12 +0,0 @@ -import inherited from 'mastodon/locales/fr.json'; - -const messages = { - 'notification.reaction': '{name} a réagi·e à votre message', - 'notifications.column_settings.reaction': 'Réactions:', - - 'tooltips.reactions': 'Réactions', - - 'status.react': 'Réagir', -}; - -export default Object.assign({}, inherited, messages); From 83e1c8e742373aaee6dbd95cc4d199158aeedace Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Mon, 3 Apr 2023 22:16:35 +0200 Subject: [PATCH 52/91] Migrate emoji reactions --- app/models/concerns/has_user_settings.rb | 14 ++++++ app/models/user_settings.rb | 1 + ...0215074425_move_emoji_reaction_settings.rb | 48 +++++++++++++++++++ db/schema.rb | 2 +- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20230215074425_move_emoji_reaction_settings.rb diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 0e9d4e1cd4..bc6af2d361 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -127,6 +127,10 @@ module HasUserSettings settings['hide_followers_count'] end + def setting_visible_reactions + integer_cast_setting('visible_reactions', 0) + end + def allows_report_emails? settings['notification_emails.report'] end @@ -170,4 +174,14 @@ module HasUserSettings def hide_all_media? settings['web.display_media'] == 'hide_all' end + + def integer_cast_setting(key, min = nil, max = nil) + i = ActiveModel::Type::Integer.new.cast(settings[key]) + # the cast above doesn't return a number if passed the string "e" + i = 0 unless i.is_a? Numeric + return min if !min.nil? && i < min + return max if !max.nil? && i > max + + i + end end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 0be8c5fbce..43afe32add 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -18,6 +18,7 @@ class UserSettings setting :default_privacy, default: nil setting :default_content_type, default: 'text/plain' setting :hide_followers_count, default: false + setting :visible_reactions, default: 6 namespace :web do setting :crop_images, default: true diff --git a/db/migrate/20230215074425_move_emoji_reaction_settings.rb b/db/migrate/20230215074425_move_emoji_reaction_settings.rb new file mode 100644 index 0000000000..9b9a65e046 --- /dev/null +++ b/db/migrate/20230215074425_move_emoji_reaction_settings.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class MoveEmojiReactionSettings < ActiveRecord::Migration[6.1] + class User < ApplicationRecord; end + + MAPPING = { + setting_visible_reactions: 'visible_reactions', + }.freeze + + class LegacySetting < ApplicationRecord + self.table_name = 'settings' + + def var + self[:var]&.to_sym + end + + def value + YAML.safe_load(self[:value], permitted_classes: [ActiveSupport::HashWithIndifferentAccess]) if self[:value].present? + end + end + + def up + User.find_each do |user| + previous_settings = LegacySetting.where(thing_type: 'User', thing_id: user.id).index_by(&:var) + + user_settings = Oj.load(user.settings || '{}') + user_settings.delete('theme') + + MAPPING.each do |legacy_key, new_key| + value = previous_settings[legacy_key]&.value + + next if value.blank? + + if value.is_a?(Hash) + value.each do |nested_key, nested_value| + user_settings[MAPPING[legacy_key][nested_key.to_sym]] = nested_value + end + else + user_settings[new_key] = value + end + end + + user.update_column('settings', Oj.dump(user_settings)) # rubocop:disable Rails/SkipsModelValidations + end + end + + def down; end +end diff --git a/db/schema.rb b/db/schema.rb index da6c611a87..003a05bb9d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_03_30_155710) do +ActiveRecord::Schema.define(version: 2023_02_15_074425) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 9133f5af9d2faefbad766a332307aadeeb453c8a Mon Sep 17 00:00:00 2001 From: neatchee Date: Sun, 7 May 2023 01:10:49 -0700 Subject: [PATCH 53/91] Fix placement of reactions bar for new threading UI --- app/javascript/flavours/glitch/styles/components/status.scss | 3 ++- app/javascript/styles/mastodon/components.scss | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 728c8b97af..a6e1cfc080 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -1055,7 +1055,8 @@ a.status-card.compact:hover { border-bottom: 0; .status__content, - .status__action-bar { + .status__action-bar, + .reactions-bar { margin-inline-start: 46px + 10px; width: calc(100% - (46px + 10px)); } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index ca16106c4a..ecef8f36da 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1151,7 +1151,7 @@ body > [data-popper-placement] { .status__content, .status__action-bar, - .reaction-bar, + .reactions-bar, .media-gallery, .video-player, .audio-player, From 7f1b0f43e976d426ab3c385db28e85c378bebd5a Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sun, 7 May 2023 22:01:04 +0200 Subject: [PATCH 54/91] Update emoji reaction patches --- .../glitch/components/status_action_bar.jsx | 4 +-- .../flavours/glitch/features/status/index.jsx | 7 ++--- app/services/unreact_service.rb | 28 +++++++++---------- config/locales-glitch/en.yml | 5 ++++ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index 0d82ecddc2..b3fe48271e 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -119,7 +119,7 @@ class StatusActionBar extends ImmutablePureComponent { handleEmojiPick = data => { this.props.onReactionAdd(this.props.status.get('id'), data.native.replace(/:/g, ''), data.imageUrl); - } + }; handleReblogClick = e => { const { signedIn } = this.context.identity; @@ -201,7 +201,7 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onAddFilter(this.props.status); }; - handleNoOp = () => {} // hack for reaction add button + handleNoOp = () => {}; // hack for reaction add button render () { const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props; diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index ee83a3773e..34c466ec50 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -42,7 +42,7 @@ import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { initBoostModal } from 'flavours/glitch/actions/boosts'; -import { makeCustomEmojiMap, makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; +import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ColumnHeader from '../../components/column_header'; import StatusContainer from 'flavours/glitch/containers/status_container'; @@ -57,7 +57,6 @@ import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/com import { Icon } from 'flavours/glitch/components/icon'; import { Helmet } from 'react-helmet'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; -import { makeCustomEmojiMap } from '../../selectors'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -305,11 +304,11 @@ class Status extends ImmutablePureComponent { if (signedIn) { dispatch(addReaction(statusId, name, url)); } - } + }; handleReactionRemove = (statusId, name) => { this.props.dispatch(removeReaction(statusId, name)); - } + }; handlePin = (status) => { if (status.get('pinned')) { diff --git a/app/services/unreact_service.rb b/app/services/unreact_service.rb index d449570c7a..37dc1d7c53 100644 --- a/app/services/unreact_service.rb +++ b/app/services/unreact_service.rb @@ -3,21 +3,21 @@ class UnreactService < BaseService include Payloadable -def call(account, status, emoji) - name, domain = emoji.split('@') - custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) - reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) - return if reaction.nil? + def call(account, status, emoji) + name, domain = emoji.split('@') + custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) + reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) + return if reaction.nil? - reaction.destroy! + reaction.destroy! - json = Oj.dump(serialize_payload(reaction, ActivityPub::UndoEmojiReactionSerializer)) - if status.account.local? - ActivityPub::RawDistributionWorker.perform_async(json, status.account.id) - else - ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url) + json = Oj.dump(serialize_payload(reaction, ActivityPub::UndoEmojiReactionSerializer)) + if status.account.local? + ActivityPub::RawDistributionWorker.perform_async(json, status.account.id) + else + ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url) + end + + reaction end - - reaction - end end diff --git a/config/locales-glitch/en.yml b/config/locales-glitch/en.yml index e88e98191e..7cba60663e 100644 --- a/config/locales-glitch/en.yml +++ b/config/locales-glitch/en.yml @@ -45,3 +45,8 @@ en: body: "%{name} reacted to your post:" subject: "%{name} reacted to your post" title: New reaction + notification_mailer: + reaction: + body: "%{name} reacted to your post:" + subject: "%{name} reacted to your post" + title: New reaction From f08f3c9eb8d4c1c59207f73e3b739a0af5c72c00 Mon Sep 17 00:00:00 2001 From: neatchee Date: Sun, 7 May 2023 15:33:50 -0700 Subject: [PATCH 55/91] Restore loc files for non-English languages; CrowdIn should handle this --- app/javascript/flavours/glitch/locales/de.json | 6 ------ app/javascript/flavours/glitch/locales/fr.json | 6 ------ app/javascript/mastodon/locales/de.json | 4 ---- app/javascript/mastodon/locales/fr.json | 8 ++++---- config/locales-glitch/de.yml | 5 ----- config/locales-glitch/fr.yml | 5 ----- 6 files changed, 4 insertions(+), 30 deletions(-) diff --git a/app/javascript/flavours/glitch/locales/de.json b/app/javascript/flavours/glitch/locales/de.json index 91d452c786..03e6e3b98a 100644 --- a/app/javascript/flavours/glitch/locales/de.json +++ b/app/javascript/flavours/glitch/locales/de.json @@ -61,7 +61,6 @@ "keyboard_shortcuts.bookmark": "zu Lesezeichen hinzufügen", "keyboard_shortcuts.secondary_toot": "Toot mit sekundärer Privatsphäreeinstellung absenden", "keyboard_shortcuts.toggle_collapse": "Toots ein-/ausklappen", - "tooltips.reactions": "Reaktionen", "layout.auto": "Automatisch", "layout.desktop": "Desktop", "layout.hint.auto": "Automatisch das Layout anhand der Einstellung \"Erweitertes Webinterface verwenden\" und Bildschirmgröße auswählen.", @@ -75,8 +74,6 @@ "navigation_bar.keyboard_shortcuts": "Tastaturkürzel", "navigation_bar.misc": "Sonstiges", "notification.markForDeletion": "Zum Entfernen auswählen", - "notification.reaction": "{name} hat auf deinen Beitrag reagiert", - "notifications.column_settings.reaction": "Reaktionen:", "notification_purge.btn_all": "Alle\nauswählen", "notification_purge.btn_apply": "Ausgewählte\nentfernen", "notification_purge.btn_invert": "Auswahl\numkehren", @@ -136,7 +133,6 @@ "settings.deprecated_setting": "Diese Einstellung wird nun von Mastodons {settings_page_link} gesteuert", "settings.enable_collapsed": "Eingeklappte Toots aktivieren", "settings.enable_collapsed_hint": "Eingeklappte Posts haben einen Teil ihres Inhalts verborgen, um weniger Platz am Bildschirm einzunehmen. Das passiert unabhängig von der Inhaltswarnfunktion", - "settings.enter_amount_prompt": "Gib eine Zahl ein", "settings.enable_content_warnings_auto_unfold": "Inhaltswarnungen automatisch ausklappen", "settings.general": "Allgemein", "settings.hicolor_privacy_icons": "Eingefärbte Privatsphäre-Symbole", @@ -150,7 +146,6 @@ "settings.layout_opts": "Layout-Optionen", "settings.media": "Medien", "settings.media_fullwidth": "Medienvorschau in voller Breite", - "settings.num_visible_reactions": "Anzahl sichtbarer Reaktionen", "settings.media_letterbox": "Mediengröße anpassen", "settings.media_letterbox_hint": "Medien runterskalieren und einpassen um die Bildbehälter zu füllen anstatt zu strecken und zuzuschneiden", "settings.media_reveal_behind_cw": "Empfindliche Medien hinter Inhaltswarnungen standardmäßig anzeigen", @@ -193,7 +188,6 @@ "settings.wide_view": "Breite Ansicht (nur für den Desktop-Modus)", "settings.wide_view_hint": "Verbreitert Spalten, um den verfügbaren Platz besser zu füllen.", "status.collapse": "Einklappen", - "status.react": "Reagieren", "status.has_audio": "Hat angehängte Audiodateien", "status.has_pictures": "Hat angehängte Bilder", "status.has_preview_card": "Hat eine Vorschaukarte", diff --git a/app/javascript/flavours/glitch/locales/fr.json b/app/javascript/flavours/glitch/locales/fr.json index 589012d6fc..b0ccce7692 100644 --- a/app/javascript/flavours/glitch/locales/fr.json +++ b/app/javascript/flavours/glitch/locales/fr.json @@ -61,7 +61,6 @@ "keyboard_shortcuts.bookmark": "ajouter aux marque-pages", "keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité", "keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts", - "tooltips.reactions": "Réactions", "layout.auto": "Auto", "layout.desktop": "Ordinateur", "layout.hint.auto": "Choisir automatiquement la mise en page selon l'option \"Activer l'interface Web avancée\" et la taille d'écran.", @@ -75,8 +74,6 @@ "navigation_bar.keyboard_shortcuts": "Raccourcis clavier", "navigation_bar.misc": "Autres", "notification.markForDeletion": "Ajouter aux éléments à supprimer", - "notification.reaction": "{name} a réagi·e à votre message", - "notifications.column_settings.reaction": "Réactions:", "notification_purge.btn_all": "Sélectionner\ntout", "notification_purge.btn_apply": "Effacer\nla sélection", "notification_purge.btn_invert": "Inverser\nla sélection", @@ -129,7 +126,6 @@ "settings.deprecated_setting": "Cette option est maintenant définie par les {settings_page_link} de Mastodon", "settings.enable_collapsed": "Activer le repliement des posts", "settings.enable_collapsed_hint": "Les posts repliés ont une partie de leur contenu caché pour libérer de l'espace sur l'écran. C'est une option différente de l'avertissement de contenu", - "settings.enter_amount_prompt": "Entrez un montant", "settings.enable_content_warnings_auto_unfold": "Déplier automatiquement les avertissements de contenu", "settings.general": "Général", "settings.hicolor_privacy_icons": "Indicateurs de confidentialité en couleurs", @@ -143,7 +139,6 @@ "settings.layout_opts": "Mise en page", "settings.media": "Média", "settings.media_fullwidth": "Utiliser toute la largeur pour les aperçus", - "settings.num_visible_reactions": "Nombre de réactions visibles", "settings.media_letterbox": "Afficher les médias en Letterbox", "settings.media_letterbox_hint": "Réduit le média et utilise une letterbox pour afficher l'image entière plutôt que de l'étirer et de la rogner", "settings.media_reveal_behind_cw": "Toujours afficher les médias sensibles avec avertissement", @@ -194,7 +189,6 @@ "status.is_poll": "Ce post est un sondage", "status.local_only": "Visible uniquement depuis votre instance", "status.sensitive_toggle": "Cliquer pour voir", - "status.react": "Réagir", "status.uncollapse": "Déplier", "web_app_crash.change_your_settings": "Changez vos {settings}", "web_app_crash.content": "Voici les différentes options qui s'offrent à vous :", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 010ec2574c..461470fe68 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -395,7 +395,6 @@ "notification.admin.report": "{name} meldete {target}", "notification.admin.sign_up": "{name} registrierte sich", "notification.favourite": "{name} hat deinen Beitrag favorisiert", - "notification.reaction": "{name} hat auf deinen Beitrag reagiert", "notification.follow": "{name} folgt dir jetzt", "notification.follow_request": "{name} möchte dir folgen", "notification.mention": "{name} erwähnte dich", @@ -410,7 +409,6 @@ "notifications.column_settings.admin.sign_up": "Neue Registrierungen:", "notifications.column_settings.alert": "Desktop-Benachrichtigungen", "notifications.column_settings.favourite": "Favorisierungen:", - "notifications.column_settings.reaction": "Reaktionen:", "notifications.column_settings.filter_bar.advanced": "Erweiterte Filterleiste aktivieren", "notifications.column_settings.filter_bar.category": "Filterleiste:", "notifications.column_settings.filter_bar.show_bar": "Filterleiste anzeigen", @@ -594,7 +592,6 @@ "status.edited_x_times": "{count, plural, one {{count} mal} other {{count} mal}} bearbeitet", "status.embed": "Beitrag per iFrame einbetten", "status.favourite": "Favorisieren", - "status.react": "Reagieren", "status.filter": "Beitrag filtern", "status.filtered": "Gefiltert", "status.hide": "Beitrag ausblenden", @@ -654,7 +651,6 @@ "timeline_hint.resources.statuses": "Ältere Beiträge", "trends.counter_by_accounts": "{count, plural, one {{counter} Profil} other {{counter} Profile}} {days, plural, one {seit gestern} other {in {days} Tagen}}", "trends.trending_now": "Aktuelle Trends", - "tooltips.reactions": "Reaktionen", "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", "units.short.billion": "{count} Mrd.", "units.short.million": "{count} Mio.", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index a5b27d6f28..75b7890d27 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -100,7 +100,7 @@ "closed_registrations_modal.preamble": "Mastodon est décentralisé : peu importe où vous créez votre compte, vous serez en mesure de suivre et d'interagir avec quiconque sur ce serveur. Vous pouvez même l'héberger !", "closed_registrations_modal.title": "Inscription sur Mastodon", "column.about": "À propos", - "column.blocks": "Utilisateurs bloqués", + "column.blocks": "Comptes bloqués", "column.bookmarks": "Signets", "column.community": "Fil public local", "column.direct": "Mention privée", @@ -140,7 +140,7 @@ "compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix", "compose_form.poll.switch_to_single": "Changer le sondage pour autoriser qu'un seul choix", "compose_form.publish": "Publier", - "compose_form.publish_form": "Publish", + "compose_form.publish_form": "Publier", "compose_form.publish_loud": "{publish} !", "compose_form.save_changes": "Enregistrer les modifications", "compose_form.sensitive.hide": "Marquer le média comme sensible", @@ -566,8 +566,8 @@ "search_results.statuses_fts_disabled": "La recherche de messages par leur contenu n'est pas activée sur ce serveur Mastodon.", "search_results.title": "Rechercher {q}", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", - "server_banner.about_active_users": "Personnes utilisant ce serveur au cours des 30 derniers jours (Utilisateur·rice·s Actifs·ives Mensuellement)", - "server_banner.active_users": "Utilisateurs actifs", + "server_banner.about_active_users": "Personnes utilisant ce serveur au cours des 30 derniers jours (Comptes actifs mensuellement)", + "server_banner.active_users": "comptes actifs", "server_banner.administered_by": "Administré par :", "server_banner.introduction": "{domain} fait partie du réseau social décentralisé propulsé par {mastodon}.", "server_banner.learn_more": "En savoir plus", diff --git a/config/locales-glitch/de.yml b/config/locales-glitch/de.yml index acef54ecd3..233bf91b38 100644 --- a/config/locales-glitch/de.yml +++ b/config/locales-glitch/de.yml @@ -40,8 +40,3 @@ de: use_this: Benutze das settings: flavours: Varianten - notification_mailer: - reaction: - body: "%{name} hat auf deinen Beitrag reagiert:" - subject: "%{name} hat auf deinen Beitrag reagiert" - title: Neue Reaktion diff --git a/config/locales-glitch/fr.yml b/config/locales-glitch/fr.yml index 770f5d4d9e..15c3f8ce52 100644 --- a/config/locales-glitch/fr.yml +++ b/config/locales-glitch/fr.yml @@ -40,8 +40,3 @@ fr: use_this: Utiliser ceci settings: flavours: Thèmes - notification_mailer: - reaction: - body: "%{name} a réagi·e à votre message:" - subject: "%{name} a réagi·e à votre message" - title: Nouvelle réaction From 38f39b422a486dc10aa7e07bd13dc09d9635e4d6 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sun, 7 May 2023 22:05:44 +0200 Subject: [PATCH 56/91] Add missing authorization to ReactService --- app/policies/status_policy.rb | 4 ++++ app/services/react_service.rb | 2 ++ 2 files changed, 6 insertions(+) diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 52cfd50506..2472b82f37 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -28,6 +28,10 @@ class StatusPolicy < ApplicationPolicy show? && !blocking_author? end + def react? + show? && !blocking_author? + end + def destroy? owned? end diff --git a/app/services/react_service.rb b/app/services/react_service.rb index 773dd3fd6c..79d1eaaf30 100644 --- a/app/services/react_service.rb +++ b/app/services/react_service.rb @@ -5,6 +5,8 @@ class ReactService < BaseService include Payloadable def call(account, status, emoji) + authorize_with account, status, :react? + name, domain = emoji.split('@') custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) From 92fea0e0282038192ca46e20bafaf5e3670aed26 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sun, 7 May 2023 23:27:19 +0200 Subject: [PATCH 57/91] Reactions: Return 404 when status should not be visible, asynchronous unreact --- .../api/v1/statuses/reactions_controller.rb | 22 +++++++++++++++---- app/models/concerns/account_associations.rb | 1 + app/workers/unreact_worker.rb | 11 ++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 app/workers/unreact_worker.rb diff --git a/app/controllers/api/v1/statuses/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb index 333054f2a0..e90e46c507 100644 --- a/app/controllers/api/v1/statuses/reactions_controller.rb +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -5,21 +5,35 @@ class Api::V1::Statuses::ReactionsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action :require_user! - before_action :set_status + before_action :set_status, only: [:create] def create ReactService.new.call(current_account, @status, params[:id]) - render_empty + render json: @status, serializer: REST::StatusSerializer end def destroy - UnreactService.new.call(current_account, @status, params[:id]) - render_empty + react = current_account.status_reactions.find_by(status_id: params[:status_id], name: params[:id]) + + if react + @status = react.status + UnreactWorker.perform_async(current_account.id, @status.id, params[: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, reactions_map: { @status.id => false }) + rescue Mastodon::NotPermittedError + not_found end private def set_status @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found end end diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 592812e960..85ab07a723 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -13,6 +13,7 @@ module AccountAssociations # Timelines has_many :statuses, inverse_of: :account, dependent: :destroy has_many :favourites, inverse_of: :account, dependent: :destroy + has_many :status_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/workers/unreact_worker.rb b/app/workers/unreact_worker.rb new file mode 100644 index 0000000000..15f1f4dd77 --- /dev/null +++ b/app/workers/unreact_worker.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class UnreactWorker + include Sidekiq::Worker + + def perform(account_id, status_id, emoji) + UnreactService.new.call(Account.find(account_id), Status.find(status_id), emoji) + rescue ActiveRecord::RecordNotFound + true + end +end From 20da97252d791e4359a6a36a86136b8934754c16 Mon Sep 17 00:00:00 2001 From: neatchee Date: Sun, 7 May 2023 16:24:43 -0700 Subject: [PATCH 58/91] Remove stale/missed references to makeCustomEmojiMap / EmojiMap --- app/javascript/mastodon/containers/status_container.jsx | 3 +-- app/javascript/mastodon/selectors/index.js | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 865d4fabac..c86dd6b7a6 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import Status from '../components/status'; -import { makeGetStatus, makeGetPictureInPicture, makeCustomEmojiMap } from '../selectors'; +import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; import { replyCompose, mentionCompose, @@ -71,7 +71,6 @@ const makeMapStateToProps = () => { status: getStatus(state, props), nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null, pictureInPicture: getPictureInPicture(state, props), - emojiMap: makeCustomEmojiMap(state), }); return mapStateToProps; diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index a3ecdd5191..58972bdf7f 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -135,11 +135,3 @@ export const getAccountHidden = createSelector([ ], (hidden, followingOrRequested, isSelf) => { return hidden && !(isSelf || followingOrRequested); }); - -export const makeCustomEmojiMap = createSelector( - [state => state.get('custom_emojis')], - items => items.reduce( - (map, emoji) => map.set(emoji.get('shortcode'), emoji), - ImmutableMap(), - ), -); From c4d82b4170f262a7abe508fcf8b432783fdd2849 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Tue, 9 May 2023 23:08:13 +0200 Subject: [PATCH 59/91] Move reaction endpoints from route.rb to api.rb --- config/routes/api.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/routes/api.rb b/config/routes/api.rb index 8cc135c136..750135b7c4 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -16,6 +16,11 @@ namespace :api, format: false do resource :favourite, only: :create post :unfavourite, to: 'favourites#destroy' + # foreign custom emojis are encoded as shortcode@domain.tld + # the constraint prevents rails from interpreting the ".tld" as a filename extension + post '/react/:id', to: 'reactions#create', constraints: { id: /[^\/]+/ } + post '/unreact/:id', to: 'reactions#destroy', constraints: { id: /[^\/]+/ } + resource :bookmark, only: :create post :unbookmark, to: 'bookmarks#destroy' @@ -27,6 +32,7 @@ namespace :api, format: false do resource :history, only: :show resource :source, only: :show + resources :reactions, only: [:update, :destroy] post :translate, to: 'translations#create' end From 0859f5b511159f466152b5b1ae807d87435ebcd2 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Tue, 9 May 2023 23:18:20 +0200 Subject: [PATCH 60/91] Fix max_reactions typedef --- app/javascript/flavours/glitch/initial_state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index 1744f6a395..fd995795f8 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -62,7 +62,6 @@ * @property {boolean} limited_federation_mode * @property {string} locale * @property {string | null} mascot - * @property {number} max_reactions * @property {string=} me * @property {string=} moved_to_account_id * @property {string=} owner @@ -97,6 +96,7 @@ * @property {object} local_settings * @property {number} max_toot_chars * @property {number} poll_limits + * @property {number} max_reactions */ const element = document.getElementById('initial-state'); From 672c1232117b7635054adab2ca02233e41fea7a7 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Tue, 9 May 2023 23:20:21 +0200 Subject: [PATCH 61/91] Add missing visible_reactions to vanilla initial_state typedef --- app/javascript/mastodon/initial_state.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index ac71e302a5..2dd755655d 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -81,6 +81,7 @@ * @property {boolean} use_blurhash * @property {boolean=} use_pending_items * @property {string} version + * @property {number} visible_reactions */ /** @@ -89,6 +90,7 @@ * @property {InitialStateLanguage[]} languages * @property {InitialStateMeta} meta * @property {number} max_toot_chars + * @property {number} max_reactions */ const element = document.getElementById('initial-state'); From ffd8aa6a2ae0e7ca3f4fa496c9baf2dd84f368b6 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Tue, 9 May 2023 23:37:34 +0200 Subject: [PATCH 62/91] Add back missing visibleReactions variable to both initial_state.js files --- app/javascript/flavours/glitch/initial_state.js | 1 + app/javascript/mastodon/initial_state.js | 1 + 2 files changed, 2 insertions(+) diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index fd995795f8..38b4d8ad11 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -152,6 +152,7 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); +export const visibleReactions = getMeta('visible_reactions'); export const languages = initialState?.languages; export const statusPageUrl = getMeta('status_page_url'); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 2dd755655d..ceaa74ab0b 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -136,6 +136,7 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); +export const visibleReactions = getMeta('visible_reactions'); export const languages = initialState?.languages; // @ts-expect-error export const statusPageUrl = getMeta('status_page_url'); From 263f10fd3e32c0d9713d33c366620824865e3249 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Tue, 9 May 2023 23:37:40 +0200 Subject: [PATCH 63/91] Removed unused imports in status_container.js --- app/javascript/flavours/glitch/containers/status_container.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 02a4d8350d..1ddfc6a62b 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -41,9 +41,6 @@ import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { defineMessages, injectIntl } from 'react-intl'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state'; import { showAlertForError } from '../actions/alerts'; -import AccountContainer from 'flavours/glitch/containers/account_container'; -import Spoilers from '../components/spoilers'; -import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, From bf7945f15b9549daf82a6e9289168ad3e62593cb Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Tue, 9 May 2023 23:41:48 +0200 Subject: [PATCH 64/91] Run rubocop -a --- app/lib/activitypub/activity/emoji_react.rb | 2 +- app/lib/activitypub/activity/like.rb | 2 +- app/models/status_reaction.rb | 1 + app/services/unreact_service.rb | 26 +++++++++---------- config/routes/api.rb | 4 +-- .../fabricators/status_reaction_fabricator.rb | 4 +-- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/lib/activitypub/activity/emoji_react.rb b/app/lib/activitypub/activity/emoji_react.rb index 2730270fc7..526f22803d 100644 --- a/app/lib/activitypub/activity/emoji_react.rb +++ b/app/lib/activitypub/activity/emoji_react.rb @@ -10,7 +10,7 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity @account.reacted?(original_status, name) custom_emoji = nil - if name =~ /^:.*:$/ + if /^:.*:$/.match?(name) process_emoji_tags(@json['tag']) name.delete! ':' diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 66d8e59c8f..8acc99b9db 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -22,7 +22,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity return false if name.nil? custom_emoji = nil - if name =~ /^:.*:$/ + if /^:.*:$/.match?(name) process_emoji_tags(@json['tag']) name.delete! ':' diff --git a/app/models/status_reaction.rb b/app/models/status_reaction.rb index 00be17e231..a54d03a309 100644 --- a/app/models/status_reaction.rb +++ b/app/models/status_reaction.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # == Schema Information # # Table name: status_reactions diff --git a/app/services/unreact_service.rb b/app/services/unreact_service.rb index 37dc1d7c53..7c1b32e94f 100644 --- a/app/services/unreact_service.rb +++ b/app/services/unreact_service.rb @@ -4,20 +4,20 @@ class UnreactService < BaseService include Payloadable def call(account, status, emoji) - name, domain = emoji.split('@') - custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) - reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) - return if reaction.nil? + name, domain = emoji.split('@') + custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) + reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) + return if reaction.nil? - reaction.destroy! + reaction.destroy! - json = Oj.dump(serialize_payload(reaction, ActivityPub::UndoEmojiReactionSerializer)) - if status.account.local? - ActivityPub::RawDistributionWorker.perform_async(json, status.account.id) - else - ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url) - end - - reaction + json = Oj.dump(serialize_payload(reaction, ActivityPub::UndoEmojiReactionSerializer)) + if status.account.local? + ActivityPub::RawDistributionWorker.perform_async(json, status.account.id) + else + ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url) end + + reaction + end end diff --git a/config/routes/api.rb b/config/routes/api.rb index 750135b7c4..71e164a55d 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -18,8 +18,8 @@ namespace :api, format: false do # foreign custom emojis are encoded as shortcode@domain.tld # the constraint prevents rails from interpreting the ".tld" as a filename extension - post '/react/:id', to: 'reactions#create', constraints: { id: /[^\/]+/ } - post '/unreact/:id', to: 'reactions#destroy', constraints: { id: /[^\/]+/ } + post '/react/:id', to: 'reactions#create', constraints: { id: %r{[^/]+} } + post '/unreact/:id', to: 'reactions#destroy', constraints: { id: %r{[^/]+} } resource :bookmark, only: :create post :unbookmark, to: 'bookmarks#destroy' diff --git a/spec/fabricators/status_reaction_fabricator.rb b/spec/fabricators/status_reaction_fabricator.rb index 3d4b93efe0..bd042ec07a 100644 --- a/spec/fabricators/status_reaction_fabricator.rb +++ b/spec/fabricators/status_reaction_fabricator.rb @@ -1,6 +1,6 @@ Fabricator(:status_reaction) do account nil status nil - name "MyString" + name 'MyString' custom_emoji nil -end \ No newline at end of file +end From 9da713f009981204e446f56c29a25322f0813f02 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:00:06 +0200 Subject: [PATCH 65/91] Fix n+1 query for move emoji reaction settings migration --- ...0215074425_move_emoji_reaction_settings.rb | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/db/migrate/20230215074425_move_emoji_reaction_settings.rb b/db/migrate/20230215074425_move_emoji_reaction_settings.rb index 9b9a65e046..420772b692 100644 --- a/db/migrate/20230215074425_move_emoji_reaction_settings.rb +++ b/db/migrate/20230215074425_move_emoji_reaction_settings.rb @@ -20,29 +20,30 @@ class MoveEmojiReactionSettings < ActiveRecord::Migration[6.1] end def up - User.find_each do |user| - previous_settings = LegacySetting.where(thing_type: 'User', thing_id: user.id).index_by(&:var) + User.find_in_batches do |users| + previous_settings_for_batch = LegacySetting.where(thing_type: 'User', thing_id: users.map(&:id)).group_by(&:thing_id) - user_settings = Oj.load(user.settings || '{}') - user_settings.delete('theme') + users.each do |user| + previous_settings = previous_settings_for_batch[user.id]&.index_by(&:var) || {} + user_settings = Oj.load(user.settings || '{}') + user_settings.delete('theme') - MAPPING.each do |legacy_key, new_key| - value = previous_settings[legacy_key]&.value + MAPPING.each do |legacy_key, new_key| + value = previous_settings[legacy_key]&.value - next if value.blank? + next if value.blank? - if value.is_a?(Hash) - value.each do |nested_key, nested_value| - user_settings[MAPPING[legacy_key][nested_key.to_sym]] = nested_value + if value.is_a?(Hash) + value.each do |nested_key, nested_value| + user_settings[MAPPING[legacy_key][nested_key.to_sym]] = nested_value + end + else + user_settings[new_key] = value end - else - user_settings[new_key] = value end - end - user.update_column('settings', Oj.dump(user_settings)) # rubocop:disable Rails/SkipsModelValidations + user.update_column('settings', Oj.dump(user_settings)) # rubocop:disable Rails/SkipsModelValidations + end end end - - def down; end end From d18ca9eef6f42a686549de2b59040e861f69c7d2 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:02:17 +0200 Subject: [PATCH 66/91] Remove duplicate notification_mailer definition --- config/locales-glitch/en.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config/locales-glitch/en.yml b/config/locales-glitch/en.yml index 7cba60663e..e88e98191e 100644 --- a/config/locales-glitch/en.yml +++ b/config/locales-glitch/en.yml @@ -45,8 +45,3 @@ en: body: "%{name} reacted to your post:" subject: "%{name} reacted to your post" title: New reaction - notification_mailer: - reaction: - body: "%{name} reacted to your post:" - subject: "%{name} reacted to your post" - title: New reaction From 4ba93c2c10c805f0a3afdd48f271734e9937aba4 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:05:39 +0200 Subject: [PATCH 67/91] Remove further leftover makeCustomEmojiMap references --- app/javascript/mastodon/features/status/index.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 0c29af8117..daa3cd6810 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -49,7 +49,7 @@ import { initMuteModal } from '../../actions/mutes'; import { initBlockModal } from '../../actions/blocks'; import { initBoostModal } from '../../actions/boosts'; import { initReport } from '../../actions/reports'; -import { makeCustomEmojiMap, makeGetStatus, makeGetPictureInPicture } from '../../selectors'; +import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import ScrollContainer from 'mastodon/containers/scroll_container'; import ColumnHeader from '../../components/column_header'; import StatusContainer from '../../containers/status_container'; @@ -63,7 +63,6 @@ import { textForScreenReader, defaultMediaVisibility } from '../../components/st import { Icon } from 'mastodon/components/icon'; import { Helmet } from 'react-helmet'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; -import { makeCustomEmojiMap } from '../../selectors'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -156,7 +155,6 @@ const makeMapStateToProps = () => { askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, domain: state.getIn(['meta', 'domain']), pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), - emojiMap: makeCustomEmojiMap(state), }; }; @@ -264,11 +262,11 @@ class Status extends ImmutablePureComponent { if (signedIn) { dispatch(addReaction(statusId, name, url)); } - } + }; handleReactionRemove = (statusId, name) => { this.props.dispatch(removeReaction(statusId, name)); - } + }; handlePin = (status) => { if (status.get('pinned')) { From 956ce7518566512bb87d1a6c1cde018f04d32853 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:11:15 +0200 Subject: [PATCH 68/91] eslint fix --- app/javascript/flavours/glitch/actions/interactions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index 8f515c990c..fecc4d03b0 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -401,7 +401,7 @@ export function unpinFail(status, error) { status, error, }; -}; +} export const addReaction = (statusId, name, url) => (dispatch, getState) => { const status = getState().get('statuses').get(statusId); From 7f21afa5b8a19ca018795e74ba516a0528466359 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:15:19 +0200 Subject: [PATCH 69/91] Fix visible reactions setting not applying --- app/views/settings/preferences/appearance/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 09ca7eb411..9751da546f 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -40,7 +40,7 @@ = ff.input :'web.crop_images', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_crop_images') .fields-group.fields-row__column.fields-row__column-6 - = f.input :setting_visible_reactions, wrapper: :with_label, input_html: { type: 'number', min: '0', data: { default: '6' } }, hint: false + = ff.input :'visible_reactions', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_visible_reactions'), input_html: { type: 'number', min: '0', data: { default: '6' } }, hint: false %h4= t 'appearance.discovery' From 00d74e293aeaba2fb25be5e89562729444750c61 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:18:32 +0200 Subject: [PATCH 70/91] Re-apply schema version --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 003a05bb9d..da6c611a87 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_02_15_074425) do +ActiveRecord::Schema.define(version: 2023_03_30_155710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 9be1967d052e92c7c502d0c578e119b62ca82fce Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:19:41 +0200 Subject: [PATCH 71/91] Introduce visible reactions default setting --- config/settings.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings.yml b/config/settings.yml index c9c37a6f71..39671f1f45 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -22,6 +22,7 @@ defaults: &defaults trends_as_landing_page: true trendable_by_default: false trending_status_cw: true + visible_reactions: 6 hide_followers_count: false reserved_usernames: - admin From 69a5c9483bdb828ffa0a448baed6a92a7f989e72 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:34:52 +0200 Subject: [PATCH 72/91] Remove old french emoji reaction-related strings --- config/locales-glitch/simple_form.fr.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/locales-glitch/simple_form.fr.yml b/config/locales-glitch/simple_form.fr.yml index 98489f9466..bc6302cf89 100644 --- a/config/locales-glitch/simple_form.fr.yml +++ b/config/locales-glitch/simple_form.fr.yml @@ -21,9 +21,7 @@ fr: setting_hide_followers_count: Cacher votre nombre d'abonné·e·s setting_skin: Thème setting_system_emoji_font: Utiliser la police par défaut du système pour les émojis (s'applique uniquement au mode Glitch) - setting_visible_reactions: Nombre de réactions emoji visibles notification_emails: trending_link: Un nouveau lien en tendances nécessite un examen trending_status: Un nouveau post en tendances nécessite un examen trending_tag: Un nouveau tag en tendances nécessite un examen - setting_visible_reactions: Nombre de réactions emoji visibles From ab5d17c9fa6e1c32f09e3759b3d2da8ddeb71040 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:43:56 +0200 Subject: [PATCH 73/91] Remove German translation for setting_visible_reactions --- config/locales-glitch/simple_form.de.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/config/locales-glitch/simple_form.de.yml b/config/locales-glitch/simple_form.de.yml index 57446e7069..0d92038c58 100644 --- a/config/locales-glitch/simple_form.de.yml +++ b/config/locales-glitch/simple_form.de.yml @@ -21,7 +21,6 @@ de: setting_hide_followers_count: Anzahl der Follower verbergen setting_skin: Skin setting_system_emoji_font: Systemschriftart für Emojis verwenden (nur für Glitch-Variante) - setting_visible_reactions: Anzahl der sichtbaren Emoji-Reaktionen notification_emails: trending_link: Neuer angesagter Link muss überprüft werden trending_status: Neuer angesagter Post muss überprüft werden From 241d61af52d398762eeae3b44a7d7da68aa72069 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 00:49:41 +0200 Subject: [PATCH 74/91] api.rb: Remove resources line that wasn't in routes.rb anymore Co-authored-by: Plastikmensch --- config/routes/api.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/routes/api.rb b/config/routes/api.rb index 71e164a55d..b54ab14168 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -32,7 +32,6 @@ namespace :api, format: false do resource :history, only: :show resource :source, only: :show - resources :reactions, only: [:update, :destroy] post :translate, to: 'translations#create' end From 245e212ba15a94abd971e7cfaa0f2dcbc398a2fd Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 20:53:18 +0200 Subject: [PATCH 75/91] status_reaction_fabricator: Use a unicode emoji instead of "MyString" Co-authored-by: Plastikmensch --- spec/fabricators/status_reaction_fabricator.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/fabricators/status_reaction_fabricator.rb b/spec/fabricators/status_reaction_fabricator.rb index bd042ec07a..6c5e67e8a4 100644 --- a/spec/fabricators/status_reaction_fabricator.rb +++ b/spec/fabricators/status_reaction_fabricator.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + Fabricator(:status_reaction) do - account nil - status nil - name 'MyString' - custom_emoji nil + account + status + name '👍' + custom_emoji end From a801d5035c1016719f94ab49db28f79b7a06ba0c Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Wed, 10 May 2023 20:59:58 +0200 Subject: [PATCH 76/91] Fix invalidating status reactions when they already exist Co-authored-by: Plastikmensch --- app/validators/status_reaction_validator.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/validators/status_reaction_validator.rb b/app/validators/status_reaction_validator.rb index 8c623c823d..0338bf531a 100644 --- a/app/validators/status_reaction_validator.rb +++ b/app/validators/status_reaction_validator.rb @@ -9,7 +9,7 @@ class StatusReactionValidator < ActiveModel::Validator return if reaction.name.blank? reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name) - reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if reaction.account.local? && limit_reached?(reaction) + reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if reaction.account.local? && new_reaction?(reaction) && limit_reached?(reaction) end private @@ -18,6 +18,10 @@ class StatusReactionValidator < ActiveModel::Validator SUPPORTED_EMOJIS.include?(name) end + def new_reaction?(reaction) + !reaction.status.status_reactions.exists?(status: reaction.status, account: reaction.account, name: reaction.name) + end + def limit_reached?(reaction) reaction.status.status_reactions.where(status: reaction.status, account: reaction.account).count >= LIMIT end From 88cb32e766217dc90c8fea8d876970ae61afafe9 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Thu, 11 May 2023 13:40:24 +0200 Subject: [PATCH 77/91] ReactionsController: Don't check for status reaction existence in destroy UnreactService checks for its existence in the background anyway, so remove redundant checks. --- .../api/v1/statuses/reactions_controller.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/v1/statuses/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb index e90e46c507..2d7e4f5984 100644 --- a/app/controllers/api/v1/statuses/reactions_controller.rb +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Statuses::ReactionsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action :require_user! - before_action :set_status, only: [:create] + before_action :set_status def create ReactService.new.call(current_account, @status, params[:id]) @@ -13,15 +13,7 @@ class Api::V1::Statuses::ReactionsController < Api::BaseController end def destroy - react = current_account.status_reactions.find_by(status_id: params[:status_id], name: params[:id]) - - if react - @status = react.status - UnreactWorker.perform_async(current_account.id, @status.id, params[:id]) - else - @status = Status.find(params[:status_id]) - authorize @status, :show? - end + UnreactWorker.perform_async(current_account.id, @status.id, params[:id]) render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reactions_map: { @status.id => false }) rescue Mastodon::NotPermittedError From 051bb17de8919038ffacfdd672833075fc65e588 Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Sun, 14 May 2023 23:49:18 +0200 Subject: [PATCH 78/91] Add `custom_emoji` to `reacted?` Signed-off-by: Plastikmensch --- app/lib/activitypub/activity/emoji_react.rb | 5 +++-- app/lib/activitypub/activity/like.rb | 2 +- app/lib/activitypub/activity/undo.rb | 14 +++++++++++++- app/models/concerns/account_interactions.rb | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/lib/activitypub/activity/emoji_react.rb b/app/lib/activitypub/activity/emoji_react.rb index 526f22803d..292ea6fd3a 100644 --- a/app/lib/activitypub/activity/emoji_react.rb +++ b/app/lib/activitypub/activity/emoji_react.rb @@ -6,8 +6,7 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity name = @json['content'] return if original_status.nil? || !original_status.account.local? || - delete_arrived_first?(@json['id']) || - @account.reacted?(original_status, name) + delete_arrived_first?(@json['id']) custom_emoji = nil if /^:.*:$/.match?(name) @@ -18,6 +17,8 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity return if custom_emoji.nil? end + return if @account.reacted?(original_status, name, custom_emoji) + reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji) LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction') diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 8acc99b9db..37215c1799 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -29,7 +29,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity custom_emoji = CustomEmoji.find_by(shortcode: name, domain: @account.domain) return false if custom_emoji.nil? # invalid custom emoji, treat it as a regular like end - return true if @account.reacted?(original_status, name) + return true if @account.reacted?(original_status, name, custom_emoji) reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji) LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction') diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index 54cda1947f..ba8d03703c 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -117,13 +117,25 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity def undo_emoji_react name = @object['content'] + tags = @object['tag'] return if name.nil? status = status_from_uri(target_uri) + name.delete! ':' return if status.nil? || !status.account.local? - if @account.reacted?(status, name.delete(':')) + custom_emoji = nil + emoji_tag = as_array(tags).find { |tag| tag['type'] == 'Emoji' } + + if emoji_tag + custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(emoji_tag) + return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank? + + custom_emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain) + end + + if @account.reacted?(status, name, custom_emoji) reaction = status.status_reactions.where(account: @account, name: name).first reaction&.destroy else diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 89edd50ed2..e3d2449733 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -243,8 +243,8 @@ module AccountInteractions status.proper.favourites.where(account: self).exists? end - def reacted?(status, name) - status.proper.status_reactions.where(account: self, name: name).exists? + def reacted?(status, name, custom_emoji = nil) + status.proper.status_reactions.where(account: self, name: name, custom_emoji: custom_emoji).exists? end def bookmarked?(status) From 239170830ecbed37eeac7d01522411158eb349a5 Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Sun, 14 May 2023 23:53:57 +0200 Subject: [PATCH 79/91] Rescue uncaught RecordInvalid errors These occur when an account tries to react with disabled custom emojis. In both `EmojiReact` and `Like? activities, the activity is discarded. Signed-off-by: Plastikmensch --- app/lib/activitypub/activity/emoji_react.rb | 2 ++ app/lib/activitypub/activity/like.rb | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/lib/activitypub/activity/emoji_react.rb b/app/lib/activitypub/activity/emoji_react.rb index 292ea6fd3a..9f28064cce 100644 --- a/app/lib/activitypub/activity/emoji_react.rb +++ b/app/lib/activitypub/activity/emoji_react.rb @@ -22,5 +22,7 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji) LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction') + rescue ActiveRecord::RecordInvalid + nil end end diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 37215c1799..1ded77f4ae 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -34,5 +34,8 @@ class ActivityPub::Activity::Like < ActivityPub::Activity reaction = original_status.status_reactions.create!(account: @account, name: name, custom_emoji: custom_emoji) LocalNotificationWorker.perform_async(original_status.account_id, reaction.id, 'StatusReaction', 'reaction') true + # account tried to react with disabled custom emoji. Returning true to discard activity. + rescue ActiveRecord::RecordInvalid + true end end From 8cecb468b0f35a5d80022c15f594a7132fb954e6 Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Sun, 14 May 2023 23:56:10 +0200 Subject: [PATCH 80/91] Only process single custom emoji Processing all custom emojis is neither wise nor necessary as both `Like` and `EmojiReact` only expect a single custom emoji Signed-off-by: Plastikmensch --- app/lib/activitypub/activity.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 00d724a282..0fc6d3ef71 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -179,13 +179,14 @@ class ActivityPub::Activity nil end - # Ensure all emojis declared in the activity's tags are + # Ensure emoji declared in the activity's tags are # present in the database and downloaded to the local cache. # Required by EmojiReact and Like for emoji reactions. def process_emoji_tags(tags) - as_array(tags).each do |tag| - process_single_emoji tag if tag['type'] == 'Emoji' - end + emoji_tag = as_array(tags).find { |tag| tag['type'] == 'Emoji' } + return if emoji_tag.nil? + + process_single_emoji emoji_tag end def process_single_emoji(tag) From f9730eba770144c2a2adc586abcb12112cccfeab Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Sun, 14 May 2023 23:59:56 +0200 Subject: [PATCH 81/91] Handle `Undo` from Misskey Right now Misskey users were able to react, but couldn't remove their reactions. delegates `Undo` for a `Like` to `undo_emoji_react` when there is no favourite found. (Misskey `Like` activities can still create a fav when the emoji tag is invalid, I don't see the point though) Signed-off-by: Plastikmensch --- app/lib/activitypub/activity/undo.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index ba8d03703c..fd836b9728 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -110,13 +110,15 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity if @account.favourited?(status) favourite = status.favourites.where(account: @account).first favourite&.destroy + elsif @object['_misskey_reaction'].present? + undo_emoji_react else delete_later!(object_uri) end end def undo_emoji_react - name = @object['content'] + name = @object['content'] || @object['_misskey_reaction'] tags = @object['tag'] return if name.nil? From b326dcab78b9fd0ab62f2356957f7c714b4e8c6f Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Mon, 15 May 2023 00:03:12 +0200 Subject: [PATCH 82/91] Don't allow reactions with disabled custom emojis Also doesn't set custom_emoji to a local variant of name when not given. Signed-off-by: Plastikmensch --- app/models/status_reaction.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/status_reaction.rb b/app/models/status_reaction.rb index a54d03a309..bc6eda2a1c 100644 --- a/app/models/status_reaction.rb +++ b/app/models/status_reaction.rb @@ -26,7 +26,8 @@ class StatusReaction < ApplicationRecord private + # Sets custom_emoji to nil when disabled def set_custom_emoji - self.custom_emoji = CustomEmoji.find_by(shortcode: name, domain: account.domain) if name.blank? + self.custom_emoji = CustomEmoji.find_by(disabled: false, shortcode: name, domain: custom_emoji.domain) if name.present? && custom_emoji.present? end end From ea10f2e1e0319a62aa36da2d0a5210ba30928aca Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Mon, 15 May 2023 00:08:18 +0200 Subject: [PATCH 83/91] Don't set `me` to true for remote reactions When an account and a remote account reacted with a custom emoji with the same shortcode, the `me` attribute was also true for the remote reaction, despite being a different emoji. This query should probably be optimised, but it works. Signed-off-by: Plastikmensch --- app/models/status.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/status.rb b/app/models/status.rb index a8d600ef1a..8a70fa7322 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -300,7 +300,7 @@ class Status < ApplicationRecord if account.nil? scope.select('name, custom_emoji_id, count(*) as count, false as me') else - scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from status_reactions r where r.account_id = #{account.id} and r.status_id = status_reactions.status_id and r.name = status_reactions.name) as me") + scope.select("name, custom_emoji_id, count(*) as count, exists(select 1 from status_reactions r where r.account_id = #{account.id} and r.status_id = status_reactions.status_id and r.name = status_reactions.name and (r.custom_emoji_id = status_reactions.custom_emoji_id or r.custom_emoji_id is null and status_reactions.custom_emoji_id is null)) as me") end end From 4e15a89b392761b889c849e03bf77e52e39889da Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Wed, 17 May 2023 14:26:40 +0200 Subject: [PATCH 84/91] Only allow reacting with remote emojis when status is local Handling remote reactions with foreign emojis would require an extensive rewrite of vanilla code, so instead prevent reactions with remote emojis when the status is not local. Signed-off-by: Plastikmensch --- app/services/react_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/react_service.rb b/app/services/react_service.rb index 79d1eaaf30..de2fd1de9c 100644 --- a/app/services/react_service.rb +++ b/app/services/react_service.rb @@ -8,6 +8,8 @@ class ReactService < BaseService authorize_with account, status, :react? name, domain = emoji.split('@') + return unless domain.nil? || status.local? + custom_emoji = CustomEmoji.find_by(shortcode: name, domain: domain) reaction = StatusReaction.find_by(account: account, status: status, name: name, custom_emoji: custom_emoji) return reaction unless reaction.nil? From 3a91f535faf654ac2abf2c0232a7c1bd4216a8d7 Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Wed, 17 May 2023 14:53:11 +0200 Subject: [PATCH 85/91] Refactor emoji reactions Instead of processing tag and then look for the custom emoji, let the processing return an emoji. Add `name` to `process_emoji_tags` to check if it matches the shortcode. Removed `process_single_emoji` and added its code to `process_emoji_tags` Removed arg from `maybe_process_misskey_reaction`. Ideally, `original_status` should be a global object, but I wanted to modify vanilla code as little as possible. Signed-off-by: Plastikmensch --- app/lib/activitypub/activity.rb | 20 +++++++++----------- app/lib/activitypub/activity/emoji_react.rb | 6 ++---- app/lib/activitypub/activity/like.rb | 11 +++++------ app/lib/activitypub/activity/undo.rb | 13 ++++--------- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 0fc6d3ef71..572ad907e2 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -182,21 +182,17 @@ class ActivityPub::Activity # Ensure emoji declared in the activity's tags are # present in the database and downloaded to the local cache. # Required by EmojiReact and Like for emoji reactions. - def process_emoji_tags(tags) - emoji_tag = as_array(tags).find { |tag| tag['type'] == 'Emoji' } - return if emoji_tag.nil? + def process_emoji_tags(name, tags) + tag = as_array(tags).find { |item| item['type'] == 'Emoji' } + return if tag.nil? - process_single_emoji emoji_tag - end - - def process_single_emoji(tag) custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(tag) - return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank? + return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank? || !name.eql?(custom_emoji_parser.shortcode) emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain) - return unless emoji.nil? || - custom_emoji_parser.image_remote_url != emoji.image_remote_url || - (custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at) + return emoji unless emoji.nil? || + custom_emoji_parser.image_remote_url != emoji.image_remote_url || + (custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at) begin emoji ||= CustomEmoji.new(domain: @account.domain, @@ -206,6 +202,8 @@ class ActivityPub::Activity emoji.save rescue Seahorse::Client::NetworkingError => e Rails.logger.warn "Error fetching emoji: #{e}" + return end + emoji end end diff --git a/app/lib/activitypub/activity/emoji_react.rb b/app/lib/activitypub/activity/emoji_react.rb index 9f28064cce..f5b5a8076b 100644 --- a/app/lib/activitypub/activity/emoji_react.rb +++ b/app/lib/activitypub/activity/emoji_react.rb @@ -8,12 +8,10 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity !original_status.account.local? || delete_arrived_first?(@json['id']) - custom_emoji = nil if /^:.*:$/.match?(name) - process_emoji_tags(@json['tag']) - name.delete! ':' - custom_emoji = CustomEmoji.find_by(shortcode: name, domain: @account.domain) + custom_emoji = process_emoji_tags(@json['tag']) + return if custom_emoji.nil? end diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 1ded77f4ae..7ea571d03e 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -5,7 +5,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity original_status = status_from_uri(object_uri) return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) - return if maybe_process_misskey_reaction(original_status) + return if maybe_process_misskey_reaction return if @account.favourited?(original_status) @@ -17,16 +17,15 @@ class ActivityPub::Activity::Like < ActivityPub::Activity # Misskey delivers reactions as likes with the emoji in _misskey_reaction # see https://misskey-hub.net/ns.html#misskey-reaction for details - def maybe_process_misskey_reaction(original_status) + def maybe_process_misskey_reaction + original_status = status_from_uri(object_uri) name = @json['_misskey_reaction'] return false if name.nil? - custom_emoji = nil if /^:.*:$/.match?(name) - process_emoji_tags(@json['tag']) - name.delete! ':' - custom_emoji = CustomEmoji.find_by(shortcode: name, domain: @account.domain) + custom_emoji = process_emoji_tags(@json['tag']) + return false if custom_emoji.nil? # invalid custom emoji, treat it as a regular like end return true if @account.reacted?(original_status, name, custom_emoji) diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index fd836b9728..5efcfdc99f 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -119,22 +119,17 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity def undo_emoji_react name = @object['content'] || @object['_misskey_reaction'] - tags = @object['tag'] return if name.nil? status = status_from_uri(target_uri) - name.delete! ':' return if status.nil? || !status.account.local? - custom_emoji = nil - emoji_tag = as_array(tags).find { |tag| tag['type'] == 'Emoji' } + if /^:.*:$/.match?(name) + name.delete! ':' + custom_emoji = process_emoji_tags(name, @object['tag']) - if emoji_tag - custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(emoji_tag) - return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank? - - custom_emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain) + return if custom_emoji.nil? end if @account.reacted?(status, name, custom_emoji) From 2a13d27be4d9597eed79ec8c3e3c64fcbba0679a Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Wed, 17 May 2023 17:51:56 +0200 Subject: [PATCH 86/91] Fix being able to bypass MAX_REACTIONS When reacting with different custom emojis with the same shortcode, it would count as an already present reaction and processed, bypassing the limit. Signed-off-by: Plastikmensch --- app/validators/status_reaction_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/validators/status_reaction_validator.rb b/app/validators/status_reaction_validator.rb index 0338bf531a..046476de9d 100644 --- a/app/validators/status_reaction_validator.rb +++ b/app/validators/status_reaction_validator.rb @@ -19,7 +19,7 @@ class StatusReactionValidator < ActiveModel::Validator end def new_reaction?(reaction) - !reaction.status.status_reactions.exists?(status: reaction.status, account: reaction.account, name: reaction.name) + !reaction.status.status_reactions.exists?(status: reaction.status, account: reaction.account, name: reaction.name, custom_emoji: reaction.custom_emoji) end def limit_reached?(reaction) From 0e1ef3efd01cfd3b4118eef06fb41b08e8e11d6e Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sat, 27 May 2023 12:56:39 +0200 Subject: [PATCH 87/91] Fix some RubyCop offenses --- db/migrate/20221124114030_create_status_reactions.rb | 2 ++ spec/models/status_reaction_spec.rb | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/db/migrate/20221124114030_create_status_reactions.rb b/db/migrate/20221124114030_create_status_reactions.rb index 5f010c4a0b..7e6e87e7bb 100644 --- a/db/migrate/20221124114030_create_status_reactions.rb +++ b/db/migrate/20221124114030_create_status_reactions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateStatusReactions < ActiveRecord::Migration[6.1] def change create_table :status_reactions do |t| diff --git a/spec/models/status_reaction_spec.rb b/spec/models/status_reaction_spec.rb index 18860318cc..ccfa9ee8d8 100644 --- a/spec/models/status_reaction_spec.rb +++ b/spec/models/status_reaction_spec.rb @@ -1,5 +1,3 @@ -require 'rails_helper' +# frozen_string_literal: true -RSpec.describe StatusReaction, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end +require 'rails_helper' From 55199ec150ce3d7ed45d55ec84ad500429a548d7 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sat, 27 May 2023 13:05:31 +0200 Subject: [PATCH 88/91] Use named import for AnimatedNumber --- app/javascript/flavours/glitch/components/status_reactions.js | 2 +- app/javascript/mastodon/components/status_reactions.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_reactions.js b/app/javascript/flavours/glitch/components/status_reactions.js index ff025e8d28..1ee8a0a028 100644 --- a/app/javascript/flavours/glitch/components/status_reactions.js +++ b/app/javascript/flavours/glitch/components/status_reactions.js @@ -7,7 +7,7 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion'; import classNames from 'classnames'; import React from 'react'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; -import AnimatedNumber from './animated_number'; +import { AnimatedNumber } from './animated_number'; import { assetHost } from '../utils/config'; export default class StatusReactions extends ImmutablePureComponent { diff --git a/app/javascript/mastodon/components/status_reactions.js b/app/javascript/mastodon/components/status_reactions.js index ff025e8d28..1ee8a0a028 100644 --- a/app/javascript/mastodon/components/status_reactions.js +++ b/app/javascript/mastodon/components/status_reactions.js @@ -7,7 +7,7 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion'; import classNames from 'classnames'; import React from 'react'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; -import AnimatedNumber from './animated_number'; +import { AnimatedNumber } from './animated_number'; import { assetHost } from '../utils/config'; export default class StatusReactions extends ImmutablePureComponent { From 3dc590a327ab0765196b2946ecfe9fb92b829606 Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Sat, 27 May 2023 13:09:48 +0200 Subject: [PATCH 89/91] Add missing `name` param. Follow-up to 3a91f535faf654ac2abf2c0232a7c1bd4216a8d7 Missed these while porting changes. Signed-off-by: Plastikmensch --- app/lib/activitypub/activity/emoji_react.rb | 2 +- app/lib/activitypub/activity/like.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/activity/emoji_react.rb b/app/lib/activitypub/activity/emoji_react.rb index f5b5a8076b..c9d88bc51c 100644 --- a/app/lib/activitypub/activity/emoji_react.rb +++ b/app/lib/activitypub/activity/emoji_react.rb @@ -10,7 +10,7 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity if /^:.*:$/.match?(name) name.delete! ':' - custom_emoji = process_emoji_tags(@json['tag']) + custom_emoji = process_emoji_tags(name, @json['tag']) return if custom_emoji.nil? end diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 7ea571d03e..86d70e0d70 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -24,7 +24,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity if /^:.*:$/.match?(name) name.delete! ':' - custom_emoji = process_emoji_tags(@json['tag']) + custom_emoji = process_emoji_tags(name, @json['tag']) return false if custom_emoji.nil? # invalid custom emoji, treat it as a regular like end From fd5e5a759ec516725c29148c1e06df4bd98a964e Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sat, 27 May 2023 13:55:17 +0200 Subject: [PATCH 90/91] Move status_reactions.js to status_reactions.jsx --- .../components/{status_reactions.js => status_reactions.jsx} | 0 .../components/{status_reactions.js => status_reactions.jsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/javascript/flavours/glitch/components/{status_reactions.js => status_reactions.jsx} (100%) rename app/javascript/mastodon/components/{status_reactions.js => status_reactions.jsx} (100%) diff --git a/app/javascript/flavours/glitch/components/status_reactions.js b/app/javascript/flavours/glitch/components/status_reactions.jsx similarity index 100% rename from app/javascript/flavours/glitch/components/status_reactions.js rename to app/javascript/flavours/glitch/components/status_reactions.jsx diff --git a/app/javascript/mastodon/components/status_reactions.js b/app/javascript/mastodon/components/status_reactions.jsx similarity index 100% rename from app/javascript/mastodon/components/status_reactions.js rename to app/javascript/mastodon/components/status_reactions.jsx From 3b3bfecba6131e79e1ffe8cfb1f6c45472f899a1 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Sat, 27 May 2023 14:05:48 +0200 Subject: [PATCH 91/91] Fix translations --- .../glitch/locales/defaultMessages.json | 90 +++++++++++++++++++ .../flavours/glitch/locales/en.json | 8 +- config/locales-glitch/en.yml | 4 +- 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/app/javascript/flavours/glitch/locales/defaultMessages.json b/app/javascript/flavours/glitch/locales/defaultMessages.json index bf21fdef2d..8feefa4a49 100644 --- a/app/javascript/flavours/glitch/locales/defaultMessages.json +++ b/app/javascript/flavours/glitch/locales/defaultMessages.json @@ -78,6 +78,15 @@ ], "path": "app/javascript/flavours/glitch/components/notification_purge_buttons.json" }, + { + "descriptors": [ + { + "defaultMessage": "React", + "id": "status.react" + } + ], + "path": "app/javascript/flavours/glitch/components/status_action_bar.json" + }, { "descriptors": [ { @@ -119,6 +128,15 @@ ], "path": "app/javascript/flavours/glitch/components/status_icons.json" }, + { + "descriptors": [ + { + "defaultMessage": "{name} reacted to your status", + "id": "notification.reaction" + } + ], + "path": "app/javascript/flavours/glitch/components/status_prepend.json" + }, { "descriptors": [ { @@ -903,6 +921,24 @@ ], "path": "app/javascript/flavours/glitch/features/local_settings/page/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "Reactions:", + "id": "notifications.column_settings.reaction" + } + ], + "path": "app/javascript/flavours/glitch/features/notifications/components/column_settings.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Reactions", + "id": "notifications.filter.reactions" + } + ], + "path": "app/javascript/flavours/glitch/features/notifications/components/filter_bar.json" + }, { "descriptors": [ { @@ -956,6 +992,15 @@ ], "path": "app/javascript/flavours/glitch/features/reblogs/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "React", + "id": "status.react" + } + ], + "path": "app/javascript/flavours/glitch/features/status/components/action_bar.json" + }, { "descriptors": [ { @@ -1110,5 +1155,50 @@ } ], "path": "app/javascript/flavours/glitch/features/ui/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "React", + "id": "status.react" + } + ], + "path": "app/javascript/mastodon/components/status_action_bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Reactions:", + "id": "notifications.column_settings.reaction" + } + ], + "path": "app/javascript/mastodon/features/notifications/components/column_settings.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Reactions", + "id": "notifications.filter.reactions" + } + ], + "path": "app/javascript/mastodon/features/notifications/components/filter_bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "{name} reacted to your status", + "id": "notification.reaction" + } + ], + "path": "app/javascript/mastodon/features/notifications/components/notification.json" + }, + { + "descriptors": [ + { + "defaultMessage": "React", + "id": "status.react" + } + ], + "path": "app/javascript/mastodon/features/status/components/action_bar.json" } ] \ No newline at end of file diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 6d16f23efe..293073de57 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -61,7 +61,6 @@ "keyboard_shortcuts.bookmark": "to bookmark", "keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting", "keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots", - "tooltips.reactions": "Reactions", "layout.auto": "Auto", "layout.desktop": "Desktop", "layout.hint.auto": "Automatically chose layout based on “Enable advanced web interface” setting and screen size.", @@ -76,12 +75,13 @@ "navigation_bar.misc": "Misc", "notification.markForDeletion": "Mark for deletion", "notification.reaction": "{name} reacted to your post", - "notifications.column_settings.reaction": "Reactions:", "notification_purge.btn_all": "Select\nall", "notification_purge.btn_apply": "Clear\nselected", "notification_purge.btn_invert": "Invert\nselection", "notification_purge.btn_none": "Select\nnone", "notification_purge.start": "Enter notification cleaning mode", + "notifications.column_settings.reaction": "Reactions:", + "notifications.filter.reactions": "Reactions", "notifications.marked_clear": "Clear selected notifications", "notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?", "onboarding.done": "Done", @@ -136,7 +136,6 @@ "settings.deprecated_setting": "This setting is now controlled from Mastodon's {settings_page_link}", "settings.enable_collapsed": "Enable collapsed toots", "settings.enable_collapsed_hint": "Collapsed posts have parts of their contents hidden to take up less screen space. This is distinct from the Content Warning feature", - "settings.enter_amount_prompt": "Enter an amount", "settings.enable_content_warnings_auto_unfold": "Automatically unfold content-warnings", "settings.general": "General", "settings.hicolor_privacy_icons": "High color privacy icons", @@ -150,7 +149,6 @@ "settings.layout_opts": "Layout options", "settings.media": "Media", "settings.media_fullwidth": "Full-width media previews", - "settings.num_visible_reactions": "Number of visible reactions", "settings.media_letterbox": "Letterbox media", "settings.media_letterbox_hint": "Scale down and letterbox media to fill the image containers instead of stretching and cropping them", "settings.media_reveal_behind_cw": "Reveal sensitive media behind a CW by default", @@ -193,7 +191,6 @@ "settings.wide_view": "Wide view (Desktop mode only)", "settings.wide_view_hint": "Stretches columns to better fill the available space.", "status.collapse": "Collapse", - "status.react": "React", "status.has_audio": "Features attached audio files", "status.has_pictures": "Features attached pictures", "status.has_preview_card": "Features an attached preview card", @@ -201,6 +198,7 @@ "status.in_reply_to": "This toot is a reply", "status.is_poll": "This toot is a poll", "status.local_only": "Only visible from your instance", + "status.react": "React", "status.sensitive_toggle": "Click to view", "status.uncollapse": "Uncollapse", "web_app_crash.change_your_settings": "Change your {settings}", diff --git a/config/locales-glitch/en.yml b/config/locales-glitch/en.yml index e88e98191e..43b8ca6c72 100644 --- a/config/locales-glitch/en.yml +++ b/config/locales-glitch/en.yml @@ -38,10 +38,10 @@ en: title: User verification generic: use_this: Use this - settings: - flavours: Flavours notification_mailer: reaction: body: "%{name} reacted to your post:" subject: "%{name} reacted to your post" title: New reaction + settings: + flavours: Flavours