From 2b0112aab183adf02f7d38b7581998698fe161bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Dec 2022 00:57:51 +0900 Subject: [PATCH 001/154] Bump sidekiq-unique-jobs from 7.1.27 to 7.1.29 (#22078) Bumps [sidekiq-unique-jobs](https://github.com/mhenrixon/sidekiq-unique-jobs) from 7.1.27 to 7.1.29. - [Release notes](https://github.com/mhenrixon/sidekiq-unique-jobs/releases) - [Changelog](https://github.com/mhenrixon/sidekiq-unique-jobs/blob/main/CHANGELOG.md) - [Commits](https://github.com/mhenrixon/sidekiq-unique-jobs/compare/v7.1.27...v7.1.29) --- updated-dependencies: - dependency-name: sidekiq-unique-jobs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ebc0d0e049..46a6ad9ab5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,9 +126,9 @@ GEM msgpack (~> 1.2) brakeman (5.4.0) browser (4.2.0) - brpoplpush-redis_script (0.1.2) + brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) - redis (>= 1.0, <= 5.0) + redis (>= 1.0, < 6) builder (3.2.4) bullet (7.0.4) activesupport (>= 3.0.0) @@ -627,10 +627,11 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 4, < 7) tilt (>= 1.4.0) - sidekiq-unique-jobs (7.1.27) + sidekiq-unique-jobs (7.1.29) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) concurrent-ruby (~> 1.0, >= 1.0.5) - sidekiq (>= 5.0, < 8.0) + redis (< 5.0) + sidekiq (>= 5.0, < 7.0) thor (>= 0.20, < 3.0) simple-navigation (4.4.0) activesupport (>= 2.3.2) From ad568924c064c41e056dfec651217dbe7bb637fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Dec 2022 00:58:15 +0900 Subject: [PATCH 002/154] Bump axios from 1.2.0 to 1.2.1 (#22076) Bumps [axios](https://github.com/axios/axios) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.2.0...v1.2.1) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index eecdf83dbe..1845facdea 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "array-includes": "^3.1.6", "arrow-key-navigation": "^1.2.0", "autoprefixer": "^9.8.8", - "axios": "^1.2.0", + "axios": "^1.2.1", "babel-loader": "^8.3.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-preval": "^5.1.0", diff --git a/yarn.lock b/yarn.lock index 25230d8f91..17e6a5c269 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2503,10 +2503,10 @@ axe-core@^4.4.3: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f" integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w== -axios@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.0.tgz#1cb65bd75162c70e9f8d118a905126c4a201d383" - integrity sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw== +axios@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a" + integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" From c3d4a644cfc92fd251566b50889f58078dee1e54 Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 24 Nov 2022 11:50:32 +0000 Subject: [PATCH 003/154] 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 ++++++++++++++++++ config/routes.rb | 1 + .../20221124114030_create_status_reactions.rb | 14 +++++++++ .../fabricators/status_reaction_fabricator.rb | 6 ++++ spec/models/status_reaction_spec.rb | 5 ++++ 9 files changed, 133 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 6cfe19d238..64f95f3d05 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 @@ -263,6 +264,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 659c45b835..43c1e86aff 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 @@ -146,6 +147,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/config/routes.rb b/config/routes.rb index 8639f0ef53..da4ddfa200 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -453,6 +453,7 @@ Rails.application.routes.draw do resource :history, only: :show resource :source, only: :show + resources :reactions, only: [:update, :destroy] post :translate, to: 'translations#create' 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 a5c6f4f4b08c70940fe7fc5a6a6d9b32189730c2 Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 24 Nov 2022 17:30:52 +0000 Subject: [PATCH 004/154] add frontend for emoji reactions this is still pretty bare bones but hey, it works. --- .../flavours/glitch/actions/interactions.js | 85 +++++++++ .../flavours/glitch/components/status.js | 12 ++ .../glitch/components/status_reactions_bar.js | 177 ++++++++++++++++++ .../glitch/containers/status_container.js | 16 +- .../flavours/glitch/reducers/statuses.js | 44 +++++ 5 files changed, 333 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 225ee7eb2a..ab275621c2 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)); @@ -392,3 +402,78 @@ export function unpinFail(status, error) { 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.js b/app/javascript/flavours/glitch/components/status.js index 4041b48194..31b234ef30 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -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'; @@ -75,6 +76,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, @@ -102,6 +105,7 @@ class Status extends ImmutablePureComponent { scrollKey: PropTypes.string, deployPictureInPicture: PropTypes.func, settings: ImmutablePropTypes.map.isRequired, + emojiMap: ImmutablePropTypes.map.isRequired, pictureInPicture: PropTypes.shape({ inUse: PropTypes.bool, available: PropTypes.bool, @@ -800,6 +804,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 947573fc7d..40a5744379 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import Status from 'flavours/glitch/components/status'; -import { List as ImmutableList } from 'immutable'; import { makeGetStatus } from 'flavours/glitch/selectors'; import { replyCompose, @@ -16,6 +15,8 @@ import { unbookmark, pin, unpin, + statusAddReaction, + statusRemoveReaction, } from 'flavours/glitch/actions/interactions'; import { muteStatus, @@ -44,6 +45,10 @@ 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' }, @@ -83,6 +88,7 @@ const makeMapStateToProps = () => { account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, + emojiMap: customEmojiMap(state), pictureInPicture: { inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id, @@ -166,6 +172,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 f0c4c804b7..605238bc29 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 91fcd87069d644090d521bf12ffd3174c2f71ff0 Mon Sep 17 00:00:00 2001 From: fef Date: Fri, 25 Nov 2022 23:02:40 +0000 Subject: [PATCH 005/154] show reactions in detailed status view --- .../glitch/containers/status_container.js | 7 ++--- .../status/components/detailed_status.js | 12 +++++++ .../flavours/glitch/features/status/index.js | 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 40a5744379..381276d9dc 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -45,10 +45,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' }, @@ -88,7 +85,7 @@ const makeMapStateToProps = () => { account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, - emojiMap: customEmojiMap(state), + emojiMap: buildCustomEmojiMap(state), pictureInPicture: { inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id, diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 7d2c2aace3..b2f38ff1ff 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -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'; export default @injectIntl class DetailedStatus extends ImmutablePureComponent { @@ -43,6 +44,9 @@ class DetailedStatus extends ImmutablePureComponent { showMedia: PropTypes.bool, usingPiP: 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.js b/app/javascript/flavours/glitch/features/status/index.js index e190652b06..730af52f03 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -20,6 +20,8 @@ import { unreblog, pin, unpin, + statusAddReaction, + statusRemoveReaction, } from 'flavours/glitch/actions/interactions'; import { replyCompose, @@ -56,6 +58,7 @@ import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status'; import Icon from 'flavours/glitch/components/icon'; import { Helmet } from 'react-helmet'; +import buildCustomEmojiMap from '../../utils/emoji_map'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -146,6 +149,7 @@ const makeMapStateToProps = () => { askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0, domain: state.getIn(['meta', 'domain']), usingPiP: state.get('picture_in_picture').statusId === props.params.statusId, + emojiMap: buildCustomEmojiMap(state), }; }; @@ -291,6 +295,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)); @@ -676,6 +704,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} @@ -683,6 +713,7 @@ class Status extends ImmutablePureComponent { showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} usingPiP={usingPiP} + emojiMap={this.props.emojiMap} /> state.get('custom_emojis')], + items => items.reduce( + (map, emoji) => map.set(emoji.get('shortcode'), emoji), + ImmutableMap(), + ), +); +export default buildCustomEmojiMap; From 5b30421f3b6171041e432d854c34a6ab7ae012ad Mon Sep 17 00:00:00 2001 From: fef Date: Mon, 28 Nov 2022 22:23:13 +0000 Subject: [PATCH 006/154] 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 f4c67cccd7..a6b91f62da 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 15c49f2fec..b8dd7e4d01 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -235,6 +235,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 64f95f3d05..5a48db2932 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 cacabea9380e6ac551a94641a2a8ad0c9912ec96 Mon Sep 17 00:00:00 2001 From: fef Date: Mon, 28 Nov 2022 22:25:12 +0000 Subject: [PATCH 007/154] 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 092e42a567e26557836f8739bc6e60cb94071ff5 Mon Sep 17 00:00:00 2001 From: fef Date: Mon, 28 Nov 2022 23:16:56 +0000 Subject: [PATCH 008/154] 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 da4c7fe4c8..eb0df86d39 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 4577711201e28bb6c5a92f07b09a874ced3bbb4c Mon Sep 17 00:00:00 2001 From: fef Date: Tue, 29 Nov 2022 00:39:40 +0000 Subject: [PATCH 009/154] 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 eb0df86d39..9eb02287e1 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 bbf25c8a85..4391b0c75d 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -148,4 +148,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 d23daaf85c..0ac196b7a0 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 079b0d15c546ca959f45b2a0885f5b622a6f634b Mon Sep 17 00:00:00 2001 From: fef Date: Tue, 29 Nov 2022 04:31:22 +0100 Subject: [PATCH 010/154] cherry-pick emoji reaction changes --- .../flavours/glitch/actions/notifications.js | 1 + .../flavours/glitch/components/status.js | 1 + .../glitch/components/status_prepend.js | 11 ++++++ .../glitch/components/status_reactions_bar.js | 1 - .../components/column_settings.js | 11 ++++++ .../notifications/components/filter_bar.js | 8 ++++ .../notifications/components/notification.js | 22 +++++++++++ app/javascript/flavours/glitch/locales/de.js | 5 ++- app/javascript/flavours/glitch/locales/en.js | 4 ++ app/javascript/flavours/glitch/locales/fr.js | 5 ++- .../flavours/glitch/reducers/settings.js | 3 ++ app/models/notification.rb | 38 ++++++++++++------- .../rest/notification_serializer.rb | 2 +- app/services/status_reaction_service.rb | 1 + config/locales/de.yml | 4 ++ config/locales/en.yml | 4 ++ config/locales/en_GB.yml | 4 ++ config/locales/fr.yml | 4 ++ 18 files changed, 111 insertions(+), 18 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 158a5b7e43..fa556269c8 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.js b/app/javascript/flavours/glitch/components/status.js index 31b234ef30..05fecdb968 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -726,6 +726,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.js b/app/javascript/flavours/glitch/components/status_prepend.js index f825330621..6c96f2ee2f 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.js +++ b/app/javascript/flavours/glitch/components/status_prepend.js @@ -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.js b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js index c1de0f90ea..6027a55d85 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js +++ b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js @@ -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' }, @@ -74,6 +75,13 @@ class FilterBar extends React.PureComponent { > +
); - } else if (placeholder) { + } else if (placeholder || number) { return (
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 2a1fedb93b..54c9860cfe 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -9,6 +9,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../initial_state'; import classNames from 'classnames'; import { PERMISSION_MANAGE_USERS } 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' }, @@ -66,6 +69,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, @@ -127,6 +131,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; @@ -230,6 +244,8 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onFilter(); } + nop = () => {} + render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn } = this.context.identity; @@ -349,11 +365,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.js b/app/javascript/mastodon/containers/status_container.js index 294105f259..556910f089 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -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, @@ -66,6 +68,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props), pictureInPicture: getPictureInPicture(state, props), + emojiMap: makeCustomEmojiMap(state), }); return mapStateToProps; @@ -129,6 +132,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.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index 8cca8af2a8..efdbf9c908 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -319,6 +319,7 @@ class EmojiPickerDropdown extends React.PureComponent { onSkinTone: PropTypes.func.isRequired, skinTone: PropTypes.number.isRequired, button: PropTypes.node, + disabled: PropTypes.bool, }; state = { @@ -356,7 +357,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.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index a38f8d3c2d..3e91ebc369 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -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.js b/app/javascript/mastodon/features/notifications/components/filter_bar.js index 368eb0b7e6..2b5f48f0b4 100644 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.js +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.js @@ -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' }, @@ -74,6 +75,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 c2fd15b17c..d68e059af4 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -45,7 +45,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' }, @@ -85,7 +84,6 @@ const makeMapStateToProps = () => { account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, - emojiMap: makeCustomEmojiMap(state), pictureInPicture: { inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id, diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 619c57685a..1ed89ea059 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -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.js b/app/javascript/flavours/glitch/features/status/index.js index 910be59982..fd27692479 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -43,7 +43,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 } from 'flavours/glitch/selectors'; +import { makeGetStatus } from 'flavours/glitch/selectors'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import ColumnHeader from '../../components/column_header'; @@ -148,7 +148,6 @@ const makeMapStateToProps = () => { askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0, domain: state.getIn(['meta', 'domain']), usingPiP: state.get('picture_in_picture').statusId === props.params.statusId, - emojiMap: makeCustomEmojiMap(state), }; }; @@ -707,7 +706,6 @@ class Status extends ImmutablePureComponent { showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} usingPiP={usingPiP} - emojiMap={this.props.emojiMap} /> { 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 7e16a2286db83067f6d7ff14b950ad9fd206ce3b Mon Sep 17 00:00:00 2001 From: fef Date: Sat, 3 Dec 2022 14:23:55 +0000 Subject: [PATCH 037/154] run i18n-tasks normalize --- config/locales/de.yml | 8 ++++---- config/locales/en.yml | 8 ++++---- config/locales/en_GB.yml | 8 ++++---- config/locales/fr.yml | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 1899b131d1..e8b7261f4b 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1285,10 +1285,6 @@ de: body: 'Dein Beitrag wurde von %{name} favorisiert:' subject: "%{name} hat deinen Beitrag favorisiert" 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" @@ -1305,6 +1301,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} hat deinen Beitrag geteilt" diff --git a/config/locales/en.yml b/config/locales/en.yml index 5792f5de98..e5484e0769 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1285,10 +1285,6 @@ en: body: 'Your post was favourited by %{name}:' subject: "%{name} favourited your post" title: New favourite - reaction: - body: '%{name} reacted to your post:' - subject: '%{name} reacted to your post' - title: New reaction follow: body: "%{name} is now following you!" subject: "%{name} is now following you" @@ -1305,6 +1301,10 @@ 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/en_GB.yml b/config/locales/en_GB.yml index 80084d9a6f..506d462409 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -707,10 +707,6 @@ en_GB: body: 'Your status was favourited by %{name}:' subject: "%{name} favourited your status" title: New favourite - reaction: - body: '%{name} reacted on your post with %{reaction}:' - subject: '%{name} reacted on your post' - title: New reaction follow: body: "%{name} is now following you!" subject: "%{name} is now following you" @@ -725,6 +721,10 @@ en_GB: body: 'You were mentioned by %{name} in:' subject: You were mentioned by %{name} title: New mention + reaction: + body: "%{name} reacted on your post with %{reaction}:" + subject: "%{name} reacted on your post" + title: New reaction reblog: body: 'Your status was boosted by %{name}:' subject: "%{name} boosted your status" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9f3cf843a1..e1deaab221 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1285,10 +1285,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" @@ -1305,6 +1301,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 e6c9206f5c5a82a367fc612ca658aafab187c231 Mon Sep 17 00:00:00 2001 From: fef Date: Sat, 3 Dec 2022 16:20:29 +0000 Subject: [PATCH 038/154] fix image for new custom emoji reactions --- .../flavours/glitch/actions/interactions.js | 7 ++++--- .../flavours/glitch/components/status_action_bar.js | 2 +- .../flavours/glitch/containers/status_container.js | 4 ++-- .../glitch/features/status/components/action_bar.js | 2 +- .../flavours/glitch/features/status/index.js | 4 ++-- app/javascript/flavours/glitch/reducers/statuses.js | 12 +++++++++--- app/javascript/mastodon/actions/interactions.js | 7 ++++--- .../mastodon/containers/status_container.js | 4 ++-- app/javascript/mastodon/features/status/index.js | 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 0df6db4277..2d483de818 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.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 5129d3a68a..e6e58f8ada 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -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 d68e059af4..3edcf9c7a8 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -167,8 +167,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.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 1ed89ea059..6f86e2aa2c 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -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.js b/app/javascript/flavours/glitch/features/status/index.js index fd27692479..37b703efaa 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -293,12 +293,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 22fdeb2841..446c991cac 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 dd1395cbd3..54592ec535 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.js b/app/javascript/mastodon/containers/status_container.js index 556910f089..70adc04934 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -132,8 +132,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.js b/app/javascript/mastodon/features/status/index.js index e262cd94fb..d22009d0ed 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -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 2228906674..cb9759119a 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 bb93649f38fd15e88e370273fb83ead1110b1b00 Mon Sep 17 00:00:00 2001 From: fef Date: Sat, 3 Dec 2022 16:55:37 +0000 Subject: [PATCH 039/154] 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 3edcf9c7a8..93a92c1377 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -168,7 +168,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 55ba8f9c928fdf5f060faf69a4209f959b131f1e Mon Sep 17 00:00:00 2001 From: fef Date: Sun, 4 Dec 2022 08:47:24 +0000 Subject: [PATCH 040/154] also disable reaction buttons in vanilla flavour --- .../mastodon/components/status_action_bar.js | 2 +- .../mastodon/components/status_reactions.js | 11 +++++++---- .../mastodon/features/status/components/action_bar.js | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 0f8d548144..c880a69594 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -364,7 +364,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.js b/app/javascript/mastodon/features/status/components/action_bar.js index 095bccfa8a..9ab228bdec 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -259,7 +259,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 041/154] 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 66ade5c1fd1bbc42d2d994fce9fe32f0f89bc919 Mon Sep 17 00:00:00 2001 From: fef Date: Sun, 4 Dec 2022 12:33:47 +0000 Subject: [PATCH 042/154] properly disable reactions when not logged in --- .../flavours/glitch/components/status.js | 2 + .../glitch/components/status_action_bar.js | 6 ++- .../glitch/components/status_reactions.js | 14 ++++--- .../glitch/containers/status_container.js | 6 +-- .../features/status/components/action_bar.js | 8 +++- .../status/components/detailed_status.js | 2 + .../flavours/glitch/features/status/index.js | 6 --- app/javascript/mastodon/components/status.js | 3 +- .../mastodon/components/status_action_bar.js | 8 +--- .../mastodon/components/status_reactions.js | 38 +++++++++---------- .../features/status/components/action_bar.js | 8 +++- .../status/components/detailed_status.js | 4 +- .../mastodon/features/status/index.js | 6 --- 13 files changed, 56 insertions(+), 55 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 08cfbdada0..99ed644aa7 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -62,6 +62,7 @@ class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -810,6 +811,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.js b/app/javascript/flavours/glitch/components/status_action_bar.js index e6e58f8ada..53a8a76864 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -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 93a92c1377..3edcf9c7a8 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -168,11 +168,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.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 6f86e2aa2c..39d32178ae 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -236,7 +236,13 @@ class ActionBar extends React.PureComponent {
-
+
+ { + signedIn + ? + : reactButton + } +
{shareButton}
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index a3d6150b7b..87eb463d3b 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -27,6 +27,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.js b/app/javascript/flavours/glitch/features/status/index.js index 37b703efaa..0879a7ab65 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -299,12 +299,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.js b/app/javascript/mastodon/components/status.js index 284c5dbd78..367131efe5 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -65,6 +65,7 @@ class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -547,7 +548,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.js b/app/javascript/mastodon/components/status_action_bar.js index c880a69594..40738aa4ed 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -131,13 +131,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.js b/app/javascript/mastodon/features/status/components/action_bar.js index 9ab228bdec..91a8a7793e 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -299,7 +299,13 @@ class ActionBar extends React.PureComponent {
-
+
+ { + canReact + ? + : reactButton + } +
{shareButton} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index f34b66110f..acb9fff572 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -31,6 +31,7 @@ class DetailedStatus extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -51,7 +52,6 @@ class DetailedStatus extends ImmutablePureComponent { onToggleMediaVisibility: PropTypes.func, onReactionAdd: PropTypes.func.isRequired, onReactionRemove: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, }; state = { @@ -284,7 +284,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.js b/app/javascript/mastodon/features/status/index.js index d22009d0ed..a3a0f0af77 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -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 1cb9c9dccaf5f736c63f0a6eb932828518d8d144 Mon Sep 17 00:00:00 2001 From: fef Date: Wed, 7 Dec 2022 12:19:36 +0000 Subject: [PATCH 043/154] 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 ---- config/routes.rb | 6 ++++-- 4 files changed, 16 insertions(+), 17 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 diff --git a/config/routes.rb b/config/routes.rb index 2eeff8fcbb..87634a7f54 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -442,8 +442,10 @@ Rails.application.routes.draw do resource :favourite, only: :create post :unfavourite, to: 'favourites#destroy' - post '/react/:id', to: 'reactions#create' - post '/unreact/:id', to: 'reactions#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' From 6e5fc00fff651c6d0fb92d7f4dbbd2b1fb879be1 Mon Sep 17 00:00:00 2001 From: fef Date: Wed, 7 Dec 2022 12:47:03 +0000 Subject: [PATCH 044/154] 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 74c0ec42f6cb4911e709c3042a64deaf518e2592 Mon Sep 17 00:00:00 2001 From: fef Date: Wed, 7 Dec 2022 21:52:53 +0100 Subject: [PATCH 045/154] fix schema after rebase --- db/schema.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/db/schema.rb b/db/schema.rb index 7462bf2c7d..bb685a9dbd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -897,6 +897,19 @@ ActiveRecord::Schema.define(version: 2022_12_06_114142) 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 @@ -1211,6 +1224,9 @@ ActiveRecord::Schema.define(version: 2022_12_06_114142) 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 1d43e6b9b05a9c5b27c50e2e0d3d6552806608fa Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 8 Dec 2022 09:48:55 +0000 Subject: [PATCH 046/154] fix status action bar after upstream changes --- app/javascript/mastodon/components/status_action_bar.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 40738aa4ed..56a98744f6 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -374,7 +374,11 @@ class StatusActionBar extends ImmutablePureComponent { - + { + signedIn + ? + : reactButton + } {shareButton} From 714e68db38b4fe115cf96fd7b9805909c4319b67 Mon Sep 17 00:00:00 2001 From: prplecake Date: Sun, 11 Dec 2022 00:27:44 -0600 Subject: [PATCH 047/154] Add noindex setting to Admin settings Discovery page (#22205) * Add noindex setting to Admin settings Discovery page * Replace default_noindex i18n --- app/views/admin/settings/discovery/show.html.haml | 3 +++ config/locales/en.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index b429cdd7b5..f60d1c7662 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -26,6 +26,9 @@ .fields-group = f.input :timeline_preview, as: :boolean, wrapper: :with_label + .fields-group + = f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html') + %h4= t('admin.settings.discovery.follow_recommendations') .fields-group diff --git a/config/locales/en.yml b/config/locales/en.yml index 1cc53dca4b..a045db1ab1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -704,6 +704,9 @@ en: content_retention: preamble: Control how user-generated content is stored in Mastodon. title: Content retention + default_noindex: + desc_html: Affects all users who have not changed this setting themselves + title: Opt users out of search engine indexing by default discovery: follow_recommendations: Follow recommendations preamble: Surfacing interesting content is instrumental in onboarding new users who may not know anyone Mastodon. Control how various discovery features work on your server. From 736b4283b0936134e83092f8b16d686f6478a51f Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Sun, 11 Dec 2022 01:37:00 -0500 Subject: [PATCH 048/154] Update Node 16.18.1 for latest security release (#22019) * Update Node 16.18.1 for latest security release * Increase Yarn network timeout for build error --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 69153c0300..1a97965ac6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.4 # This needs to be bullseye-slim because the Ruby image is built on bullseye-slim -ARG NODE_VERSION="16.17.1-bullseye-slim" +ARG NODE_VERSION="16.18.1-bullseye-slim" FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.4-slim as ruby FROM node:${NODE_VERSION} as build @@ -36,7 +36,7 @@ RUN apt update && \ bundle config set --local without 'development test' && \ bundle config set silence_root_warning true && \ bundle install -j"$(nproc)" && \ - yarn install --pure-lockfile + yarn install --pure-lockfile --network-timeout 600000 FROM node:${NODE_VERSION} From c957eb758c1b4beb5d000e922e8931492b9912dd Mon Sep 17 00:00:00 2001 From: fef Date: Sun, 11 Dec 2022 13:26:23 +0000 Subject: [PATCH 049/154] 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 2d483de818..a7eb7a78dd 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 54592ec535..7433870250 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 f70bdba9264bd7c572cee3c45421733919b7d03c Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 13 Dec 2022 19:41:53 +0100 Subject: [PATCH 050/154] Change default reply language to be default language when replying to a translated reply (#22272) Fixes #22250 --- app/javascript/mastodon/reducers/compose.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 1dafb07fd2..9496b56f87 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -330,8 +330,10 @@ export default function compose(state = initialState, action) { map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); - if (action.status.get('language')) { + if (action.status.get('language') && !action.status.has('translation')) { map.set('language', action.status.get('language')); + } else { + map.set('language', state.get('default_language')); } if (action.status.get('spoiler_text').length > 0) { From 52a50c5e43a78f21a1054869352db31b4fa3aba0 Mon Sep 17 00:00:00 2001 From: cadars Date: Tue, 13 Dec 2022 19:43:03 +0100 Subject: [PATCH 051/154] Make handle more easily selectable on profile page (#21479) * Make handle more easily selectable on profile page * Wrap handle in a span * Add `user-select: all` to span * remove whitespace --- app/javascript/mastodon/features/account/components/header.js | 4 +++- app/javascript/styles/mastodon/components.scss | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 5eb89c0ea0..f117412be8 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -342,7 +342,9 @@ class Header extends ImmutablePureComponent {

{badge} - @{acct} {lockedIcon} + + @{acct} {lockedIcon} +

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b56d43000d..1271fc7f3a 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7092,6 +7092,10 @@ noscript { font-weight: 400; overflow: hidden; text-overflow: ellipsis; + + span { + user-select: all; + } } } } From 55b210b3e599fa51186a646572056cda7aa3f23c Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 13 Dec 2022 20:02:32 +0100 Subject: [PATCH 052/154] Fix crash and incorrect behavior in tootctl domains crawl (#19004) --- lib/mastodon/domains_cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mastodon/domains_cli.rb b/lib/mastodon/domains_cli.rb index a7c78c4a7a..77364ffbbe 100644 --- a/lib/mastodon/domains_cli.rb +++ b/lib/mastodon/domains_cli.rb @@ -97,7 +97,7 @@ module Mastodon failed = Concurrent::AtomicFixnum.new(0) start_at = Time.now.to_f seed = start ? [start] : Instance.pluck(:domain) - blocked_domains = Regexp.new('\\.?' + DomainBlock.where(severity: 1).pluck(:domain).join('|') + '$') + blocked_domains = /\.?(#{DomainBlock.where(severity: 1).pluck(:domain).map { |domain| Regexp.escape(domain) }.join('|')})$/ progress = create_progress_bar pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0) From 42e16ea52dcafef7737368b05537670cc49d3f91 Mon Sep 17 00:00:00 2001 From: Rin <36845451+AtelierSnek@users.noreply.github.com> Date: Wed, 14 Dec 2022 06:03:09 +1100 Subject: [PATCH 053/154] fix missing style in warning and strike cards (#22177) --- app/javascript/styles/mastodon/admin.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 7a50a89bb6..2ed0d613ed 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1665,6 +1665,15 @@ a.sparkline { box-sizing: border-box; min-height: 100%; + a { + text: &highlight-text-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + p { margin-bottom: 20px; unicode-bidi: plaintext; From 09191dee66461fcec7ab8ed906c89410065529b2 Mon Sep 17 00:00:00 2001 From: zunda Date: Tue, 13 Dec 2022 19:03:16 +0000 Subject: [PATCH 054/154] Add single splat to callback method definitions to avoid ArgumentError (#22246) It looks like a [bug](https://bugs.ruby-lang.org/issues/18633) around autosplat is [fixed](https://bugs.ruby-lang.org/projects/ruby-master/repository/git/revisions/fbaadd1cfe7fbfd1b904f193f99d7c845a6ed804) on ruby-3.2.0-rc1 and breaks a test (but not on ruby <= 3.1.3): ``` $ bundle exec rspec ./spec/controllers/api/v1/emails/confirmations_controller_spec.rb:41 : 1) Api::V1::Emails::ConfirmationsController#create with an oauth token from an app that created the account when the account is already confirmed but user changed e-mail and has not confirmed it returns http success Failure/Error: def email_changed(user, **) @resource = user @instance = Rails.configuration.x.local_domain return unless @resource.active_for_authentication? I18n.with_locale(locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject') end end ArgumentError: wrong number of arguments (given 2, expected 1) # ./app/mailers/user_mailer.rb:51:in `email_changed' # ./app/models/user.rb:444:in `render_and_send_devise_message' # ./app/models/user.rb:430:in `block in send_pending_devise_notifications' # ./app/models/user.rb:429:in `each' # ./app/models/user.rb:429:in `send_pending_devise_notifications' # ./spec/controllers/api/v1/emails/confirmations_controller_spec.rb:38:in `block (7 levels) in ' ``` --- app/mailers/user_mailer.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 09e2b7c534..2889d13b53 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -11,7 +11,7 @@ class UserMailer < Devise::Mailer helper RoutingHelper - def confirmation_instructions(user, token, **) + def confirmation_instructions(user, token, *, **) @resource = user @token = token @instance = Rails.configuration.x.local_domain @@ -25,7 +25,7 @@ class UserMailer < Devise::Mailer end end - def reset_password_instructions(user, token, **) + def reset_password_instructions(user, token, *, **) @resource = user @token = token @instance = Rails.configuration.x.local_domain @@ -37,7 +37,7 @@ class UserMailer < Devise::Mailer end end - def password_change(user, **) + def password_change(user, *, **) @resource = user @instance = Rails.configuration.x.local_domain @@ -48,7 +48,7 @@ class UserMailer < Devise::Mailer end end - def email_changed(user, **) + def email_changed(user, *, **) @resource = user @instance = Rails.configuration.x.local_domain @@ -59,7 +59,7 @@ class UserMailer < Devise::Mailer end end - def two_factor_enabled(user, **) + def two_factor_enabled(user, *, **) @resource = user @instance = Rails.configuration.x.local_domain @@ -70,7 +70,7 @@ class UserMailer < Devise::Mailer end end - def two_factor_disabled(user, **) + def two_factor_disabled(user, *, **) @resource = user @instance = Rails.configuration.x.local_domain @@ -81,7 +81,7 @@ class UserMailer < Devise::Mailer end end - def two_factor_recovery_codes_changed(user, **) + def two_factor_recovery_codes_changed(user, *, **) @resource = user @instance = Rails.configuration.x.local_domain @@ -92,7 +92,7 @@ class UserMailer < Devise::Mailer end end - def webauthn_enabled(user, **) + def webauthn_enabled(user, *, **) @resource = user @instance = Rails.configuration.x.local_domain @@ -103,7 +103,7 @@ class UserMailer < Devise::Mailer end end - def webauthn_disabled(user, **) + def webauthn_disabled(user, *, **) @resource = user @instance = Rails.configuration.x.local_domain From 364f5f1f459b4bb6a0fe6cc261624ec73f214645 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 05:29:33 +0900 Subject: [PATCH 055/154] Bump prettier from 2.8.0 to 2.8.1 (#22255) Bumps [prettier](https://github.com/prettier/prettier) from 2.8.0 to 2.8.1. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/2.8.0...2.8.1) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1845facdea..943e30f11d 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", "postcss-scss": "^4.0.6", - "prettier": "^2.8.0", + "prettier": "^2.8.1", "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", "react-test-renderer": "^16.14.0", diff --git a/yarn.lock b/yarn.lock index 17e6a5c269..1e1894a7c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8624,10 +8624,10 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" - integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== +prettier@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.1.tgz#4e1fd11c34e2421bc1da9aea9bd8127cd0a35efc" + integrity sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" From 7e2d5e8aa78d6887cddff639f817c94739052fbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 05:30:26 +0900 Subject: [PATCH 056/154] Bump sass from 1.56.1 to 1.56.2 (#22257) Bumps [sass](https://github.com/sass/dart-sass) from 1.56.1 to 1.56.2. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.56.1...1.56.2) --- updated-dependencies: - dependency-name: sass dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 943e30f11d..5f84319a3c 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "requestidlecallback": "^0.3.0", "reselect": "^4.1.7", "rimraf": "^3.0.2", - "sass": "^1.56.1", + "sass": "^1.56.2", "sass-loader": "^10.2.0", "stacktrace-js": "^2.0.2", "stringz": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 1e1894a7c3..85332c45e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9593,10 +9593,10 @@ sass-loader@^10.2.0: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" - integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== +sass@^1.56.2: + version "1.56.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.2.tgz#9433b345ab3872996c82a53a58c014fd244fd095" + integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" From 062197a193d60aaf6df0eb67f989f5afe246df1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 05:31:21 +0900 Subject: [PATCH 057/154] Bump public_suffix from 5.0.0 to 5.0.1 (#22259) Bumps [public_suffix](https://github.com/weppos/publicsuffix-ruby) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/weppos/publicsuffix-ruby/releases) - [Changelog](https://github.com/weppos/publicsuffix-ruby/blob/main/CHANGELOG.md) - [Commits](https://github.com/weppos/publicsuffix-ruby/compare/v5.0.0...v5.0.1) --- updated-dependencies: - dependency-name: public_suffix dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 46a6ad9ab5..30df1f1e63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -481,7 +481,7 @@ GEM pry (>= 0.13, < 0.15) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (5.0.0) + public_suffix (5.0.1) puma (5.6.5) nio4r (~> 2.0) pundit (2.2.0) From 44739096eca1080fe4af6b29552c2d417a63da01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 05:34:07 +0900 Subject: [PATCH 058/154] Bump loofah from 2.19.0 to 2.19.1 (#22278) Bumps [loofah](https://github.com/flavorjones/loofah) from 2.19.0 to 2.19.1. - [Release notes](https://github.com/flavorjones/loofah/releases) - [Changelog](https://github.com/flavorjones/loofah/blob/main/CHANGELOG.md) - [Commits](https://github.com/flavorjones/loofah/compare/v2.19.0...v2.19.1) --- updated-dependencies: - dependency-name: loofah dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 30df1f1e63..cb8a014552 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -382,7 +382,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.19.0) + loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) From 1133e05e3da306460fa802c2bb78c25cc73ed9be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 05:41:34 +0900 Subject: [PATCH 059/154] Bump rails-html-sanitizer from 1.4.3 to 1.4.4 (#22279) Bumps [rails-html-sanitizer](https://github.com/rails/rails-html-sanitizer) from 1.4.3 to 1.4.4. - [Release notes](https://github.com/rails/rails-html-sanitizer/releases) - [Changelog](https://github.com/rails/rails-html-sanitizer/blob/master/CHANGELOG.md) - [Commits](https://github.com/rails/rails-html-sanitizer/compare/v1.4.3...v1.4.4) --- updated-dependencies: - dependency-name: rails-html-sanitizer dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cb8a014552..578a88436b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -525,8 +525,8 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) + rails-html-sanitizer (1.4.4) + loofah (~> 2.19, >= 2.19.1) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) From a9bd5f65bbb6beea0048aed07c437ab65a764b41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Dec 2022 05:41:52 +0900 Subject: [PATCH 060/154] Bump postcss from 8.4.19 to 8.4.20 (#22256) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.19 to 8.4.20. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.19...8.4.20) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5f84319a3c..dd93102ed6 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "object.values": "^1.1.6", "path-complete-extname": "^1.0.0", "pg": "^8.5.0", - "postcss": "^8.4.19", + "postcss": "^8.4.20", "postcss-loader": "^3.0.0", "postcss-object-fit-images": "^1.1.2", "promise.prototype.finally": "^3.1.4", diff --git a/yarn.lock b/yarn.lock index 85332c45e0..abbd51eb9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8583,10 +8583,10 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27, postcss@^7.0.32: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.2.15, postcss@^8.4.17, postcss@^8.4.19: - version "8.4.19" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc" - integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA== +postcss@^8.2.15, postcss@^8.4.17, postcss@^8.4.20: + version "8.4.20" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.20.tgz#64c52f509644cecad8567e949f4081d98349dc56" + integrity sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g== dependencies: nanoid "^3.3.4" picocolors "^1.0.0" From bc91069e08e0dadcb523b6e372c0c0830376706c Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 13 Dec 2022 19:41:53 +0100 Subject: [PATCH 061/154] [Glitch] Change default reply language to be default language when replying to a translated reply Port f70bdba9264bd7c572cee3c45421733919b7d03c to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/reducers/compose.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 9b50ec23a9..ddc3fa41e3 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -421,8 +421,10 @@ export default function compose(state = initialState, action) { map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); - if (action.status.get('language')) { + if (action.status.get('language') && !action.status.has('translation')) { map.set('language', action.status.get('language')); + } else { + map.set('language', state.get('default_language')); } if (action.status.get('spoiler_text').length > 0) { From 32b8b3355b8a4c2d8993cd52445512c06ae07d5c Mon Sep 17 00:00:00 2001 From: cadars Date: Tue, 13 Dec 2022 19:43:03 +0100 Subject: [PATCH 062/154] [Glitch] Make handle more easily selectable on profile page Port 52a50c5e43a78f21a1054869352db31b4fa3aba0 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/account/components/header.js | 4 +++- .../flavours/glitch/styles/components/accounts.scss | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 9a5f2fd62d..d261c6ea54 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -345,7 +345,9 @@ class Header extends ImmutablePureComponent {

{badge} - @{acct} {lockedIcon} + + @{acct} {lockedIcon} +

diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index ac2d642a8d..ed1d3d660f 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -593,6 +593,10 @@ font-weight: 400; overflow: hidden; text-overflow: ellipsis; + + span { + user-select: all; + } } } } From 21ee6a777d785b07af769caf629b007588f1bb7e Mon Sep 17 00:00:00 2001 From: Rin <36845451+AtelierSnek@users.noreply.github.com> Date: Wed, 14 Dec 2022 06:03:09 +1100 Subject: [PATCH 063/154] [Glitch] fix missing style in warning and strike cards Port 42e16ea52dcafef7737368b05537670cc49d3f91 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/admin.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index c2426944be..ad9a36bb54 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1681,6 +1681,15 @@ a.sparkline { box-sizing: border-box; min-height: 100%; + a { + text: &highlight-text-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + p { margin-bottom: 20px; unicode-bidi: plaintext; From ccc01559de06ca46319abe4f09856e267812c7ba Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Dec 2022 10:11:04 +0100 Subject: [PATCH 064/154] Fix invalid CSS for links in warning and strike cards --- app/javascript/flavours/glitch/styles/admin.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index ad9a36bb54..7d5b28a12c 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1682,7 +1682,7 @@ a.sparkline { min-height: 100%; a { - text: &highlight-text-color; + color: $highlight-text-color; text-decoration: none; &:hover { From 78ef635980ff391ea3ba4c37de836947a97958a0 Mon Sep 17 00:00:00 2001 From: Evan <35814742+evanphilip@users.noreply.github.com> Date: Wed, 14 Dec 2022 19:50:07 +0100 Subject: [PATCH 065/154] Add command to remove avatar and header images of inactive remote accounts from the local database (#22149) * Add tootctl subcommand media remove-profile-media * Trigger workflows * Correcting external linting * External linting error * External linting fix * Merging with remove command * Linting * Correct long option names Co-authored-by: Claire * Correct long option names Co-authored-by: Claire * Correct long option names Co-authored-by: Claire * Remove saving a list of purged accounts Co-authored-by: Claire --- lib/mastodon/media_cli.rb | 78 ++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb index bba4a1bd72..24cc989646 100644 --- a/lib/mastodon/media_cli.rb +++ b/lib/mastodon/media_cli.rb @@ -14,35 +14,78 @@ module Mastodon end option :days, type: :numeric, default: 7, aliases: [:d] + option :prune_profiles, type: :boolean, default: false + option :remove_headers, type: :boolean, default: false + option :include_follows, type: :boolean, default: false option :concurrency, type: :numeric, default: 5, aliases: [:c] - option :verbose, type: :boolean, default: false, aliases: [:v] option :dry_run, type: :boolean, default: false - desc 'remove', 'Remove remote media files' + desc 'remove', 'Remove remote media files, headers or avatars' long_desc <<-DESC - Removes locally cached copies of media attachments from other servers. - + Removes locally cached copies of media attachments (and optionally profile + headers and avatars) from other servers. By default, only media attachements + are removed. The --days option specifies how old media attachments have to be before - they are removed. It defaults to 7 days. + they are removed. In case of avatars and headers, it specifies how old + the last webfinger request and update to the user has to be before they + are pruned. It defaults to 7 days. + If --prune-profiles is specified, only avatars and headers are removed. + If --remove-headers is specified, only headers are removed. + If --include-follows is specified along with --prune-profiles or + --remove-headers, all non-local profiles will be pruned irrespective of + follow status. By default, only accounts that are not followed by or + following anyone locally are pruned. DESC + # rubocop:disable Metrics/PerceivedComplexity def remove - time_ago = options[:days].days.ago - dry_run = options[:dry_run] ? '(DRY RUN)' : '' + if options[:prune_profiles] && options[:remove_headers] + say('--prune-profiles and --remove-headers should not be specified simultaneously', :red, true) + exit(1) + end + if options[:include_follows] && !(options[:prune_profiles] || options[:remove_headers]) + say('--include-follows can only be used with --prune-profiles or --remove-headers', :red, true) + exit(1) + end + time_ago = options[:days].days.ago + dry_run = options[:dry_run] ? ' (DRY RUN)' : '' - processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where('created_at < ?', time_ago)) do |media_attachment| - next if media_attachment.file.blank? + if options[:prune_profiles] || options[:remove_headers] + processed, aggregate = parallelize_with_progress(Account.remote.where({ last_webfingered_at: ..time_ago, updated_at: ..time_ago })) do |account| + next if !options[:include_follows] && Follow.where(account: account).or(Follow.where(target_account: account)).exists? + next if account.avatar.blank? && account.header.blank? + next if options[:remove_headers] && account.header.blank? - size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0) + size = (account.header_file_size || 0) + size += (account.avatar_file_size || 0) if options[:prune_profiles] - unless options[:dry_run] - media_attachment.file.destroy - media_attachment.thumbnail.destroy - media_attachment.save + unless options[:dry_run] + account.header.destroy + account.avatar.destroy if options[:prune_profiles] + account.save! + end + + size end - size + say("Visited #{processed} accounts and removed profile media totaling #{number_to_human_size(aggregate)}#{dry_run}", :green, true) end - say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)}) #{dry_run}", :green, true) + unless options[:prune_profiles] || options[:remove_headers] + processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where(created_at: ..time_ago)) do |media_attachment| + next if media_attachment.file.blank? + + size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0) + + unless options[:dry_run] + media_attachment.file.destroy + media_attachment.thumbnail.destroy + media_attachment.save + end + + size + end + + say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true) + end end option :start_after @@ -183,6 +226,7 @@ module Mastodon say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run}", :green, true) end + # rubocop:enable Metrics/PerceivedComplexity option :account, type: :string option :domain, type: :string @@ -269,7 +313,7 @@ module Mastodon def lookup(url) path = Addressable::URI.parse(url).path - path_segments = path.split('/')[2..-1] + path_segments = path.split('/')[2..] path_segments.delete('cache') unless [7, 10].include?(path_segments.size) From 1f762f4271685e30373b15a2e204f8edff59d349 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 14:07:34 +0100 Subject: [PATCH 066/154] Fix wasteful request to /api/v1/custom_emojis when not logged in (#22326) --- app/javascript/mastodon/containers/mastodon.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 724719f74f..002b71e93d 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -23,7 +23,9 @@ export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); -store.dispatch(fetchCustomEmojis()); +if (initialState.meta.me) { + store.dispatch(fetchCustomEmojis()); +} const createIdentityContext = state => ({ signedIn: !!state.meta.me, From 5917b46c0530777b684cbd661d0f454264e4f046 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 14:15:50 +0100 Subject: [PATCH 067/154] Allow admins to refresh remotely-suspended accounts (#22327) * Change suspension text to mention that a remotely suspended account is not locally-suspended * Add ability to refresh profile of remotely suspended accounts --- app/views/admin/accounts/show.html.haml | 6 +++++- config/locales/en.yml | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index dc3b359566..db5c255c91 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -195,9 +195,13 @@ - if @account.suspended? %hr.spacer/ - %p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible') + - if @account.suspension_origin_remote? + %p.muted-hint= @deletion_request.present? ? t('admin.accounts.remote_suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.remote_suspension_irreversible') + - else + %p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible') = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account) + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) && @account.suspension_origin_remote? - if @deletion_request.present? = link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account) diff --git a/config/locales/en.yml b/config/locales/en.yml index a045db1ab1..0a0effbc1d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -116,6 +116,8 @@ en: redownloaded_msg: Successfully refreshed %{username}'s profile from origin reject: Reject rejected_msg: Successfully rejected %{username}'s sign-up application + remote_suspension_irreversible: The data of this account has been irreversibly deleted. + remote_suspension_reversible_hint_html: The account has been suspended on their server, and the data will be fully removed on %{date}. Until then, the remote server can restore this account without any ill effects. If you wish to remove all of the account's data immediately, you can do so below. remove_avatar: Remove avatar remove_header: Remove header removed_avatar_msg: Successfully removed %{username}'s avatar image From f239d31f23b8bd55fb26f67906b815e4abe65d92 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 14:52:50 +0100 Subject: [PATCH 068/154] Add --email and --dry-run options to `tootctl accounts delete` (#22328) --- lib/mastodon/accounts_cli.rb | 41 ++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 77cbef84ec..0dd8521313 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -200,21 +200,44 @@ module Mastodon end end - desc 'delete USERNAME', 'Delete a user' + option :email + option :dry_run, type: :boolean + desc 'delete [USERNAME]', 'Delete a user' long_desc <<-LONG_DESC Remove a user account with a given USERNAME. - LONG_DESC - def delete(username) - account = Account.find_local(username) - if account.nil? - say('No user with such username', :red) + With the --email option, the user is selected based on email + rather than username. + LONG_DESC + def delete(username = nil) + if username.present? && options[:email].present? + say('Use username or --email, not both', :red) + exit(1) + elsif username.blank? && options[:email].blank? + say('No username provided', :red) exit(1) end - say("Deleting user with #{account.statuses_count} statuses, this might take a while...") - DeleteAccountService.new.call(account, reserve_email: false) - say('OK', :green) + dry_run = options[:dry_run] ? ' (DRY RUN)' : '' + account = nil + + if username.present? + account = Account.find_local(username) + if account.nil? + say('No user with such username', :red) + exit(1) + end + else + account = Account.left_joins(:user).find_by(user: { email: options[:email] }) + if account.nil? + say('No user with such email', :red) + exit(1) + end + end + + say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run}") + DeleteAccountService.new.call(account, reserve_email: false) unless options[:dry_run] + say("OK#{dry_run}", :green) end option :force, type: :boolean, aliases: [:f], description: 'Override public key check' From 6cdbc345f4ed824b9491ac6b53406c44e23f7ba5 Mon Sep 17 00:00:00 2001 From: Meisam <39205857+MFTabriz@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:43:05 +0100 Subject: [PATCH 069/154] Validate nodeinfo response by schema (#21395) * add json-schema to :test in Gemfile * Create node_info_2.0_schema.json * test match_response_schema * Create match_response_schema.rb * Update nodeinfo_controller_spec.rb * Rename spec/support/node_info_2.0_schema.json to spec/support/schema/node_info_2.0_schema.json * Update match_response_schema.rb * cleanup * additionally validate the json schema itself disable throwing errors test the schema matcher * rename nodeinfo schema to nodeinfo_2.0 * use Rails.root.join to construct the path * prettify json * sync Gemfile.lock --- Gemfile | 5 +- Gemfile.lock | 3 + .../well_known/nodeinfo_controller_spec.rb | 2 + .../matchers/json/match_json_schema.rb | 6 + spec/support/schema/nodeinfo_2.0.json | 170 ++++++++++++++++++ 5 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 spec/support/matchers/json/match_json_schema.rb create mode 100644 spec/support/schema/nodeinfo_2.0.json diff --git a/Gemfile b/Gemfile index a399f51023..9fe2a11209 100644 --- a/Gemfile +++ b/Gemfile @@ -117,13 +117,14 @@ group :test do gem 'capybara', '~> 3.38' gem 'climate_control', '~> 0.2' gem 'faker', '~> 3.0' + gem 'json-schema', '~> 3.0' gem 'microformats', '~> 4.4' + gem 'rack-test', '~> 2.0' gem 'rails-controller-testing', '~> 1.0' + gem 'rspec_junit_formatter', '~> 0.6' gem 'rspec-sidekiq', '~> 3.1' gem 'simplecov', '~> 0.21', require: false gem 'webmock', '~> 3.18' - gem 'rspec_junit_formatter', '~> 0.6' - gem 'rack-test', '~> 2.0' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 578a88436b..ac3f560ea1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -344,6 +344,8 @@ GEM json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) + json-schema (3.0.0) + addressable (>= 2.8) jsonapi-renderer (0.2.2) jwt (2.4.1) kaminari (1.2.2) @@ -791,6 +793,7 @@ DEPENDENCIES idn-ruby json-ld json-ld-preloaded (~> 3.2) + json-schema (~> 3.0) kaminari (~> 1.2) kt-paperclip (~> 7.1) letter_opener (~> 1.8) diff --git a/spec/controllers/well_known/nodeinfo_controller_spec.rb b/spec/controllers/well_known/nodeinfo_controller_spec.rb index 694bb0fb9f..36e85f20d1 100644 --- a/spec/controllers/well_known/nodeinfo_controller_spec.rb +++ b/spec/controllers/well_known/nodeinfo_controller_spec.rb @@ -27,6 +27,8 @@ describe WellKnown::NodeInfoController, type: :controller do json = body_as_json + expect({ "foo" => 0 }).not_to match_json_schema("nodeinfo_2.0") + expect(json).to match_json_schema("nodeinfo_2.0") expect(json[:version]).to eq '2.0' expect(json[:usage]).to be_a Hash expect(json[:software]).to be_a Hash diff --git a/spec/support/matchers/json/match_json_schema.rb b/spec/support/matchers/json/match_json_schema.rb new file mode 100644 index 0000000000..5d9c9a618e --- /dev/null +++ b/spec/support/matchers/json/match_json_schema.rb @@ -0,0 +1,6 @@ +RSpec::Matchers.define :match_json_schema do |schema| + match do |input_json| + schema_path = Rails.root.join('spec', 'support', 'schema', "#{schema}.json").to_s + JSON::Validator.validate(schema_path, input_json, validate_schema: true) + end +end diff --git a/spec/support/schema/nodeinfo_2.0.json b/spec/support/schema/nodeinfo_2.0.json new file mode 100644 index 0000000000..085ce542bd --- /dev/null +++ b/spec/support/schema/nodeinfo_2.0.json @@ -0,0 +1,170 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://nodeinfo.diaspora.software/ns/schema/2.0#", + "description": "NodeInfo schema version 2.0.", + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "software", + "protocols", + "services", + "openRegistrations", + "usage", + "metadata" + ], + "properties": { + "version": { + "description": "The schema version, must be 2.0.", + "enum": ["2.0"] + }, + "software": { + "description": "Metadata about server software in use.", + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "description": "The canonical name of this server software.", + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "version": { + "description": "The version of this server software.", + "type": "string" + } + } + }, + "protocols": { + "description": "The protocols supported on this server.", + "type": "array", + "minItems": 1, + "items": { + "enum": [ + "activitypub", + "buddycloud", + "dfrn", + "diaspora", + "libertree", + "ostatus", + "pumpio", + "tent", + "xmpp", + "zot" + ] + } + }, + "services": { + "description": "The third party sites this server can connect to via their application API.", + "type": "object", + "additionalProperties": false, + "required": ["inbound", "outbound"], + "properties": { + "inbound": { + "description": "The third party sites this server can retrieve messages from for combined display with regular traffic.", + "type": "array", + "minItems": 0, + "items": { + "enum": [ + "atom1.0", + "gnusocial", + "imap", + "pnut", + "pop3", + "pumpio", + "rss2.0", + "twitter" + ] + } + }, + "outbound": { + "description": "The third party sites this server can publish messages to on the behalf of a user.", + "type": "array", + "minItems": 0, + "items": { + "enum": [ + "atom1.0", + "blogger", + "buddycloud", + "diaspora", + "dreamwidth", + "drupal", + "facebook", + "friendica", + "gnusocial", + "google", + "insanejournal", + "libertree", + "linkedin", + "livejournal", + "mediagoblin", + "myspace", + "pinterest", + "pnut", + "posterous", + "pumpio", + "redmatrix", + "rss2.0", + "smtp", + "tent", + "tumblr", + "twitter", + "wordpress", + "xmpp" + ] + } + } + } + }, + "openRegistrations": { + "description": "Whether this server allows open self-registration.", + "type": "boolean" + }, + "usage": { + "description": "Usage statistics for this server.", + "type": "object", + "additionalProperties": false, + "required": ["users"], + "properties": { + "users": { + "description": "statistics about the users of this server.", + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "description": "The total amount of on this server registered users.", + "type": "integer", + "minimum": 0 + }, + "activeHalfyear": { + "description": "The amount of users that signed in at least once in the last 180 days.", + "type": "integer", + "minimum": 0 + }, + "activeMonth": { + "description": "The amount of users that signed in at least once in the last 30 days.", + "type": "integer", + "minimum": 0 + } + } + }, + "localPosts": { + "description": "The amount of posts that were made by users that are registered on this server.", + "type": "integer", + "minimum": 0 + }, + "localComments": { + "description": "The amount of comments that were made by users that are registered on this server.", + "type": "integer", + "minimum": 0 + } + } + }, + "metadata": { + "description": "Free form key value pairs for software specific values. Clients should not rely on any specific key present.", + "type": "object", + "minProperties": 0, + "additionalProperties": true + } + } +} From fe9eab51d140ee0e0343eb07982f0a7ce825398c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 15:43:16 +0100 Subject: [PATCH 070/154] =?UTF-8?q?Change=20dropdown=20menu=20to=20contain?= =?UTF-8?q?=20=E2=80=9CCopy=20link=20to=20post=E2=80=9D=20even=20for=20non?= =?UTF-8?q?-public=20posts=20(#21316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #21244 --- .../mastodon/components/status_action_bar.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 2a1fedb93b..40c86afdf0 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -246,12 +246,13 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); - if (publicStatus) { - if (isRemote) { - menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); - } + if (publicStatus && isRemote) { + menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); + } - menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); + menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); + + if (publicStatus) { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } From 5fb1c3e934a1a782972ac2732ce7f0208c341ac2 Mon Sep 17 00:00:00 2001 From: Francis Murillo Date: Thu, 15 Dec 2022 14:47:06 +0000 Subject: [PATCH 071/154] Revoke all authorized applications on password reset (#21325) * Clear sessions on password change * Rename User::clear_sessions to revoke_access for a clearer meaning * Add reset paassword controller test * Use User.find instead of User.find_for_authentication for reset password test * Use redirect and render for better test meaning in reset password Co-authored-by: Effy Elden --- app/controllers/auth/passwords_controller.rb | 2 + app/models/user.rb | 16 +++-- .../auth/passwords_controller_spec.rb | 61 +++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 2996c0431b..a8ad669297 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -10,6 +10,8 @@ class Auth::PasswordsController < Devise::PasswordsController super do |resource| if resource.errors.empty? resource.session_activations.destroy_all + + resource.revoke_access! end end end diff --git a/app/models/user.rb b/app/models/user.rb index 5530a9070d..ca98a0afab 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -377,6 +377,15 @@ class User < ApplicationRecord super end + def revoke_access! + Doorkeeper::AccessGrant.by_resource_owner(self).update_all(revoked_at: Time.now.utc) + + Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| + batch.update_all(revoked_at: Time.now.utc) + Web::PushSubscription.where(access_token_id: batch).delete_all + end + end + def reset_password! # First, change password to something random and deactivate all sessions transaction do @@ -385,12 +394,7 @@ class User < ApplicationRecord end # Then, remove all authorized applications and connected push subscriptions - Doorkeeper::AccessGrant.by_resource_owner(self).in_batches.update_all(revoked_at: Time.now.utc) - - Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| - batch.update_all(revoked_at: Time.now.utc) - Web::PushSubscription.where(access_token_id: batch).delete_all - end + revoke_access! # Finally, send a reset password prompt to the user send_reset_password_instructions diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb index dcfdebb173..1c6874f08c 100644 --- a/spec/controllers/auth/passwords_controller_spec.rb +++ b/spec/controllers/auth/passwords_controller_spec.rb @@ -35,4 +35,65 @@ describe Auth::PasswordsController, type: :controller do end end end + + describe 'POST #update' do + let(:user) { Fabricate(:user) } + + before do + @password = 'reset0password' + request.env['devise.mapping'] = Devise.mappings[:user] + end + + context 'with valid reset_password_token' do + let!(:session_activation) { Fabricate(:session_activation, user: user) } + let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) } + + before do + @token = user.send_reset_password_instructions + + post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: @token } } + end + + it 'redirect to sign in' do + expect(response).to redirect_to '/auth/sign_in' + end + + it 'changes password' do + this_user = User.find(user.id) + + expect(this_user).to_not be_nil + expect(this_user.valid_password?(@password)).to be true + end + + it 'deactivates all sessions' do + expect(user.session_activations.count).to eq 0 + end + + it 'revokes all access tokens' do + expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0 + end + + it 'removes push subscriptions' do + expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0 + end + end + + context 'with invalid reset_password_token' do + before do + post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: 'some_invalid_value' } } + end + + it 'renders reset password' do + expect(response).to render_template(:new) + end + + it 'retains password' do + this_user = User.find(user.id) + + expect(this_user).to_not be_nil + expect(this_user.external_or_valid_password?(user.password)).to be true + end + end + end end From bae6ef315ef6cd5cef8213ce84edbcba3eb3cec8 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 15:47:23 +0100 Subject: [PATCH 072/154] Fix missing Javascript in domain block import confirmation page (#21471) Follow-up to #20597 --- app/views/admin/export_domain_blocks/import.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/admin/export_domain_blocks/import.html.haml b/app/views/admin/export_domain_blocks/import.html.haml index 01add232d1..804e61199e 100644 --- a/app/views/admin/export_domain_blocks/import.html.haml +++ b/app/views/admin/export_domain_blocks/import.html.haml @@ -1,6 +1,9 @@ - content_for :page_title do = t('admin.export_domain_blocks.import.title') +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + %p= t('admin.export_domain_blocks.import.description_html') - if defined?(@global_private_comment) && @global_private_comment.present? From e5d15a5b563863a07e7f91fc6f3bc0475bd6200a Mon Sep 17 00:00:00 2001 From: Justin Hutchings Date: Thu, 15 Dec 2022 06:51:13 -0800 Subject: [PATCH 073/154] Add CodeQL workflow (#21894) --- .github/workflows/codeql.yml | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..88ac2fb085 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,63 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '22 6 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From e8cc63105fe9e5166182ccea28008d880ca43fd9 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Thu, 15 Dec 2022 23:52:06 +0900 Subject: [PATCH 074/154] Don't delivery a reply to domains which are blocked by author (#22117) Co-authored-by: Claire --- app/lib/status_reach_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index ccf1e9e3a7..36fb0e80fb 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -70,7 +70,7 @@ class StatusReachFinder def followers_inboxes if @status.in_reply_to_local_account? && distributable? - @status.account.followers.or(@status.thread.account.followers).inboxes + @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).inboxes elsif @status.direct_visibility? || @status.limited_visibility? [] else From 7b68e6409bff40e60dd5c04c4e562177c2b14bc5 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 15:52:18 +0100 Subject: [PATCH 075/154] Fix invalid CSS for links in warning and strike cards (#22302) --- app/javascript/styles/mastodon/admin.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 2ed0d613ed..ba64fde09b 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1666,7 +1666,7 @@ a.sparkline { min-height: 100%; a { - text: &highlight-text-color; + color: $highlight-text-color; text-decoration: none; &:hover { From 441cac758f759ba16744f80e1d981e84f415bd29 Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Fri, 16 Dec 2022 01:56:05 +1100 Subject: [PATCH 076/154] Allow adding relays while secure mode & limited federation mode are enabled (#22324) --- app/controllers/admin/relays_controller.rb | 6 +++--- config/locales/en.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb index 6fbb6e0630..c1297c8b99 100644 --- a/app/controllers/admin/relays_controller.rb +++ b/app/controllers/admin/relays_controller.rb @@ -3,7 +3,7 @@ module Admin class RelaysController < BaseController before_action :set_relay, except: [:index, :new, :create] - before_action :require_signatures_enabled!, only: [:new, :create, :enable] + before_action :warn_signatures_not_enabled!, only: [:new, :create, :enable] def index authorize :relay, :update? @@ -56,8 +56,8 @@ module Admin params.require(:relay).permit(:inbox_url) end - def require_signatures_enabled! - redirect_to admin_relays_path, alert: I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode? + def warn_signatures_not_enabled! + flash.now[:error] = I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode? end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 0a0effbc1d..2fcfd4ee16 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -557,7 +557,7 @@ en: pending: Waiting for relay's approval save_and_enable: Save and enable setup: Setup a relay connection - signatures_not_enabled: Relays will not work correctly while secure mode or limited federation mode is enabled + signatures_not_enabled: Relays may not work correctly while secure mode or limited federation mode is enabled status: Status title: Relays report_notes: From 52540771b0e7f69d2d1e0c21b558976000e807e5 Mon Sep 17 00:00:00 2001 From: s0 Date: Fri, 16 Dec 2022 01:56:48 +1100 Subject: [PATCH 077/154] Fix crash in elasticsearch_check.rb (#21006) Nil unwrap causes the admin dashboard to crash/500 when the Chewy client info version number value is nil. This occurs when running another ES-compatible backend such as MeiliSearch. Obviously it would be good for chewy to recognise upstream but at least avoiding the crash would be fine. --- app/lib/admin/system_check/elasticsearch_check.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/admin/system_check/elasticsearch_check.rb b/app/lib/admin/system_check/elasticsearch_check.rb index 8aee182674..a63988224b 100644 --- a/app/lib/admin/system_check/elasticsearch_check.rb +++ b/app/lib/admin/system_check/elasticsearch_check.rb @@ -34,6 +34,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck end def compatible_version? + return false if running_version.nil? Gem::Version.new(running_version) >= Gem::Version.new(required_version) end end From c3388f4ab151a2603fabd67dadea435f851eaf12 Mon Sep 17 00:00:00 2001 From: Riedler Date: Thu, 15 Dec 2022 15:57:02 +0100 Subject: [PATCH 078/154] Fix profile avatar being slightly offset into left border (fixes #20822) (#20994) * hotfix for #20822 I don't know why it was shifted in the first place or why the width is specified twice, but this fixes the problem, so it looks fine to me. * realigned pfp with content below * fixed formatting my bad * added comment to explain the negative margin before I forget - comments are *important* ! Co-authored-by: Riedler --- app/javascript/styles/mastodon/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1271fc7f3a..f5d442a858 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7021,7 +7021,6 @@ noscript { display: block; flex: 0 0 auto; width: 94px; - margin-left: -2px; .account__avatar { background: darken($ui-base-color, 8%); @@ -7038,6 +7037,7 @@ noscript { padding-top: 10px; gap: 8px; overflow: hidden; + margin-left: -2px; // aligns the pfp with content below &__buttons { display: flex; From a0813806d6be42c2b1d466315b3bbecb4950f334 Mon Sep 17 00:00:00 2001 From: Moritz Heiber Date: Thu, 15 Dec 2022 15:57:17 +0100 Subject: [PATCH 079/154] Add hadolint as Dockerfile linter (#20993) * Added hadolint as Dockerfile linter in pipeline and resolved remaining hadolint issues in Dockerfile * Use more specific version of hadolint Action * Bumpt hadolint Action version to latest version to avoid deprecation notice * Being _really_ specific now --- .github/workflows/build-image.yml | 1 + Dockerfile | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 6c12bd0730..c161cbf3dd 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -18,6 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: hadolint/hadolint-action@v3.0.0 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - uses: docker/login-action@v2 diff --git a/Dockerfile b/Dockerfile index 1a97965ac6..ce7f4d7186 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,8 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] WORKDIR /opt/mastodon COPY Gemfile* package.json yarn.lock /opt/mastodon/ -RUN apt update && \ +# hadolint ignore=DL3008 +RUN apt-get update && \ apt-get install -y --no-install-recommends build-essential \ ca-certificates \ git \ @@ -50,10 +51,12 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" +# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# hadolint ignore=DL3008,DL3009 RUN apt-get update && \ echo "Etc/UTC" > /etc/localtime && \ groupadd -g "${GID}" mastodon && \ - useradd -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \ + useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \ apt-get -y --no-install-recommends install whois \ wget \ procps \ From 596923da4a4a0e384da5abeb0d987c127301630a Mon Sep 17 00:00:00 2001 From: luzpaz Date: Thu, 15 Dec 2022 09:57:26 -0500 Subject: [PATCH 080/154] Fix typos in source documentation (#21046) Fixed 2 source comment/documentation typos --- app/workers/scheduler/suspended_user_cleanup_scheduler.rb | 2 +- config/initializers/devise.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/workers/scheduler/suspended_user_cleanup_scheduler.rb b/app/workers/scheduler/suspended_user_cleanup_scheduler.rb index 50768f83cc..87e22161bc 100644 --- a/app/workers/scheduler/suspended_user_cleanup_scheduler.rb +++ b/app/workers/scheduler/suspended_user_cleanup_scheduler.rb @@ -9,7 +9,7 @@ class Scheduler::SuspendedUserCleanupScheduler MAX_PULL_SIZE = 50 # Since account deletion is very expensive, we want to avoid - # overloading the server by queing too much at once. + # overloading the server by queuing too much at once. # This job runs approximately once per 2 minutes, so with a # value of `MAX_DELETIONS_PER_JOB` of 10, a server can # handle the deletion of 7200 accounts per day, provided it diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c55bea7a75..d7b252c3f2 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -159,7 +159,7 @@ Devise.setup do |config| # config.request_keys = [] # Configure which authentication keys should be case-insensitive. - # These keys will be downcased upon creating or modifying a user and when used + # These keys will be lowercased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. config.case_insensitive_keys = [:email] From 63b379c2d95a8ea7127f2621603037cc9013870d Mon Sep 17 00:00:00 2001 From: nametoolong Date: Thu, 15 Dec 2022 23:18:20 +0800 Subject: [PATCH 081/154] Fix N+1 queries from in NotificationsController (#21202) Co-authored-by: Nonexistent --- app/controllers/api/v1/notifications_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 6d464997ea..7b1cfe2643 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -31,7 +31,7 @@ class Api::V1::NotificationsController < Api::BaseController private def load_notifications - notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id( + notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_paginated_by_id( limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params_slice(:max_id, :since_id, :min_id) ) From 04c611daa1b7ff27a8fe0af882ff9339aeddac6d Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Thu, 15 Dec 2022 15:18:39 +0000 Subject: [PATCH 082/154] Fix being unable to react with the keycap number sign emoji (#22231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #⃣ This bug is caused by the emoji consisting of: U+23 # U+FE0F U+20E3 ⃣ Because it starts with a #, it's interpreted as an anchor link, which is not passed to the API. Therefore, the API sees no emoji to react with and answers correctly with a 404. --- app/javascript/mastodon/actions/announcements.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js index 1bdea909f7..586dcfd337 100644 --- a/app/javascript/mastodon/actions/announcements.js +++ b/app/javascript/mastodon/actions/announcements.js @@ -102,7 +102,7 @@ export const addReaction = (announcementId, name) => (dispatch, getState) => { dispatch(addReactionRequest(announcementId, name, alreadyAdded)); } - api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => { + api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => { dispatch(addReactionSuccess(announcementId, name, alreadyAdded)); }).catch(err => { if (!alreadyAdded) { @@ -136,7 +136,7 @@ export const addReactionFail = (announcementId, name, error) => ({ export const removeReaction = (announcementId, name) => (dispatch, getState) => { dispatch(removeReactionRequest(announcementId, name)); - api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => { + api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => { dispatch(removeReactionSuccess(announcementId, name)); }).catch(err => { dispatch(removeReactionFail(announcementId, name, err)); From 4114a7088a7161a8aebf27d20433c0f47a4f178c Mon Sep 17 00:00:00 2001 From: Matt Hodges Date: Thu, 15 Dec 2022 09:18:59 -0600 Subject: [PATCH 083/154] Embed js height fix (#22141) * only begin iframe reheight once document state is complete * format * lint fixes * Update public/embed.js to use readystatechange event listener Co-authored-by: Claire * Call loaded() if ready, otherwise add listenter * lint fix Co-authored-by: Claire --- public/embed.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/public/embed.js b/public/embed.js index 5607c24d5f..defba403e4 100644 --- a/public/embed.js +++ b/public/embed.js @@ -1,24 +1,28 @@ // @ts-check -(function() { +(function () { 'use strict'; /** * @param {() => void} loaded */ - var ready = function(loaded) { - if (['interactive', 'complete'].indexOf(document.readyState) !== -1) { + var ready = function (loaded) { + if (document.readyState === 'complete') { loaded(); } else { - document.addEventListener('DOMContentLoaded', loaded); + document.addEventListener('readystatechange', function () { + if (document.readyState === 'complete') { + loaded(); + } + }); } }; - ready(function() { + ready(function () { /** @type {Map} */ var iframes = new Map(); - window.addEventListener('message', function(e) { + window.addEventListener('message', function (e) { var data = e.data || {}; if (typeof data !== 'object' || data.type !== 'setHeight' || !iframes.has(data.id)) { @@ -34,7 +38,7 @@ iframe.height = data.height; }); - [].forEach.call(document.querySelectorAll('iframe.mastodon-embed'), function(iframe) { + [].forEach.call(document.querySelectorAll('iframe.mastodon-embed'), function (iframe) { // select unique id for each iframe var id = 0, failCount = 0, idBuffer = new Uint32Array(1); while (id === 0 || iframes.has(id)) { @@ -49,10 +53,10 @@ iframes.set(id, iframe); - iframe.scrolling = 'no'; + iframe.scrolling = 'no'; iframe.style.overflow = 'hidden'; - iframe.onload = function() { + iframe.onload = function () { iframe.contentWindow.postMessage({ type: 'setHeight', id: id, From 903e5a3f459d28b093438dc4827b2fb976aef406 Mon Sep 17 00:00:00 2001 From: Alex Stine Date: Thu, 15 Dec 2022 09:20:21 -0600 Subject: [PATCH 084/154] Fix hidden label causing accessibility issue for search inputs (#21275) * Try unhiding search label. * Use aria-label. Remove label as empty labels are useless. * Remove addition of package-lock.json. --- .../features/compose/components/search.js | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index ebb23d92ff..03e6dcf2c8 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -123,20 +123,18 @@ class Search extends React.PureComponent { return (
- +
From 3a59ffde8d1600729bf51ff42f1d646062edfc8d Mon Sep 17 00:00:00 2001 From: Pleclown Date: Thu, 15 Dec 2022 16:20:34 +0100 Subject: [PATCH 085/154] Adding 12 hours option for polls (#21131) * Adding 12 hours option for polls Adding 12 hours option for polls * Adding 12 hours option for polls Missing > on a line --- app/javascript/mastodon/features/compose/components/poll_form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js index ede29b8a08..c58db64670 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.js +++ b/app/javascript/mastodon/features/compose/components/poll_form.js @@ -165,6 +165,7 @@ class PollForm extends ImmutablePureComponent { + From 58200132d07bc0b641c95af614b9ac02ce48c9fc Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 16 Dec 2022 00:20:46 +0900 Subject: [PATCH 086/154] `FormattedMessage` must be used directly (#20982) * `FormattedMessage` must be used directly * rollback --- .../mastodon/features/explore/index.js | 32 +++++++++---------- .../mastodon/locales/defaultMessages.json | 16 ++++++++++ app/javascript/mastodon/locales/en.json | 4 +++ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js index 286170c9ff..1ae249f458 100644 --- a/app/javascript/mastodon/features/explore/index.js +++ b/app/javascript/mastodon/features/explore/index.js @@ -24,16 +24,6 @@ const mapStateToProps = state => ({ isSearching: state.getIn(['search', 'submitted']) || !showTrends, }); -// Fix strange bug on Safari where (rendered by FormattedMessage) disappears -// after clicking around Explore top bar (issue #20885). -// Removing width=100% from also fixes it, as well as replacing with
-// We're choosing to wrap span with div to keep the changes local only to this tool bar. -const WrapFormattedMessage = ({ children, ...props }) =>
{children}
; -WrapFormattedMessage.propTypes = { - children: PropTypes.any, -}; - - export default @connect(mapStateToProps) @injectIntl class Explore extends React.PureComponent { @@ -78,12 +68,22 @@ class Explore extends React.PureComponent { {isSearching ? ( ) : ( - + <>
- - - - {signedIn && } + + + + + + + + + + {signedIn && ( + + + + )}
@@ -97,7 +97,7 @@ class Explore extends React.PureComponent { {intl.formatMessage(messages.title)} -
+ )}
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 445ab38947..230154eb10 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -2014,6 +2014,22 @@ { "defaultMessage": "Search results", "id": "explore.search_results" + }, + { + "defaultMessage": "Posts", + "id": "explore.trending_statuses" + }, + { + "defaultMessage": "Hashtags", + "id": "explore.trending_tags" + }, + { + "defaultMessage": "News", + "id": "explore.trending_links" + }, + { + "defaultMessage": "For you", + "id": "explore.suggested_follows" } ], "path": "app/javascript/mastodon/features/explore/index.json" diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index dc165e74c6..05b9353bf6 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -235,7 +235,11 @@ "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.report_issue": "Report issue", "explore.search_results": "Search results", + "explore.suggested_follows": "For you", "explore.title": "Explore", + "explore.trending_links": "News", + "explore.trending_statuses": "Posts", + "explore.trending_tags": "Hashtags", "filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.", "filter_modal.added.context_mismatch_title": "Context mismatch!", "filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.", From baecdf28820b267aebe0c9fa92bdc85d929746e7 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Fri, 16 Dec 2022 00:20:55 +0900 Subject: [PATCH 087/154] Fix typo in application_helper_spec.rb (#20981) enviroment -> environment --- spec/helpers/application_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 20ee32aa0f..1dbd985bf4 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -113,7 +113,7 @@ describe ApplicationHelper do Setting.site_title = site_title end - it 'returns site title on production enviroment' do + it 'returns site title on production environment' do Setting.site_title = 'site title' expect(Rails.env).to receive(:production?).and_return(true) expect(helper.title).to eq 'site title' From f0cebaee00f80270bef7a7ce8e03597dd118ad23 Mon Sep 17 00:00:00 2001 From: trwnh Date: Thu, 15 Dec 2022 09:21:13 -0600 Subject: [PATCH 088/154] Add localization for new admin scopes (#20979) * Add localization for new admin scopes * run bundle exec i18n-tasks normalize --- config/locales/doorkeeper.en.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 5567724ae4..2df0056c21 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -149,9 +149,19 @@ en: scopes: admin:read: read all data on the server admin:read:accounts: read sensitive information of all accounts + admin:read:canonical_email_blocks: read sensitive information of all canonical email blocks + admin:read:domain_allows: read sensitive information of all domain allows + admin:read:domain_blocks: read sensitive information of all domain blocks + admin:read:email_domain_blocks: read sensitive information of all email domain blocks + admin:read:ip_blocks: read sensitive information of all IP blocks admin:read:reports: read sensitive information of all reports and reported accounts admin:write: modify all data on the server admin:write:accounts: perform moderation actions on accounts + admin:write:canonical_email_blocks: perform moderation actions on canonical email blocks + admin:write:domain_allows: perform moderation actions on domain allows + admin:write:domain_blocks: perform moderation actions on domain blocks + admin:write:email_domain_blocks: perform moderation actions on email domain blocks + admin:write:ip_blocks: perform moderation actions on IP blocks admin:write:reports: perform moderation actions on reports crypto: use end-to-end encryption follow: modify account relationships From c50e9d078aa3c353afc140669f1cedcd354ee53e Mon Sep 17 00:00:00 2001 From: Francis Murillo Date: Thu, 15 Dec 2022 15:35:25 +0000 Subject: [PATCH 089/154] Render current day formats in the client timezone (#21878) * Fix remaining plain %time to %time.formatted * Add %time.relative-formatted to client format dates on the current day * Add missing comma dangle to formats * Use client side message format instead of the server * Add fallback message to relatve_format.today * Remove unused translation key and fix js lint issue Co-authored-by: Effy Elden --- app/javascript/mastodon/locales/en.json | 1 + app/javascript/packs/public.js | 38 +++++++++++++++++++ .../admin/report_notes/_report_note.html.haml | 7 +--- app/views/admin/reports/show.html.haml | 7 +--- app/views/disputes/strikes/show.html.haml | 7 +--- .../settings/featured_tags/index.html.haml | 2 +- config/locales/en.yml | 1 - 7 files changed, 46 insertions(+), 17 deletions(-) diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 05b9353bf6..997b0d9e53 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -461,6 +461,7 @@ "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_format.today": "Today at {time}", "relative_time.days": "{number}d", "relative_time.full.days": "{number, plural, one {# day} other {# days}} ago", "relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago", diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 786fc8ede2..a5e2014f7e 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -63,6 +63,18 @@ function main() { minute: 'numeric', }); + const dateFormat = new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'short', + day: 'numeric', + timeFormat: false, + }); + + const timeFormat = new Intl.DateTimeFormat(locale, { + timeStyle: 'short', + hour12: false, + }); + [].forEach.call(document.querySelectorAll('.emojify'), (content) => { content.innerHTML = emojify(content.innerHTML); }); @@ -75,6 +87,32 @@ function main() { content.textContent = formattedDate; }); + const isToday = date => { + const today = new Date(); + + return date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear(); + }; + const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale); + + [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => { + const datetime = new Date(content.getAttribute('datetime')); + + let formattedContent; + + if (isToday(datetime)) { + const formattedTime = timeFormat.format(datetime); + + formattedContent = todayFormat.format({ time: formattedTime }); + } else { + formattedContent = dateFormat.format(datetime); + } + + content.title = formattedContent; + content.textContent = formattedContent; + }); + [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { const datetime = new Date(content.getAttribute('datetime')); const now = new Date(); diff --git a/app/views/admin/report_notes/_report_note.html.haml b/app/views/admin/report_notes/_report_note.html.haml index 3bccd3b4bc..14df2f6090 100644 --- a/app/views/admin/report_notes/_report_note.html.haml +++ b/app/views/admin/report_notes/_report_note.html.haml @@ -4,11 +4,8 @@ .report-notes__item__header %span.username = link_to report_note.account.username, admin_account_path(report_note.account_id) - %time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) } - - if report_note.created_at.today? - = t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time)) - - else - = l report_note.created_at.to_date + %time.relative-formatted{ datetime: report_note.created_at } + = t('admin.report_notes.created_at') .report-notes__item__content = simple_format(h(report_note.content)) diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index cf960565f7..50ec64b065 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -144,11 +144,8 @@ = link_to @report.account.username, admin_account_path(@report.account_id) - else = link_to @report.account.domain, admin_instance_path(@report.account.domain) - %time{ datetime: @report.created_at.iso8601, title: l(@report.created_at) } - - if @report.created_at.today? - = t('admin.report_notes.today_at', time: l(@report.created_at, format: :time)) - - else - = l @report.created_at.to_date + %time.relative-formatted{ datetime: @report.created_at.iso8601 } + = t('admin.report_notes.created_at') .report-notes__item__content = simple_format(h(@report.comment)) diff --git a/app/views/disputes/strikes/show.html.haml b/app/views/disputes/strikes/show.html.haml index 4a3005f72a..cab0a17eb1 100644 --- a/app/views/disputes/strikes/show.html.haml +++ b/app/views/disputes/strikes/show.html.haml @@ -110,11 +110,8 @@ .report-notes__item__header %span.username = link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account) - %time{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) } - - if @appeal.created_at.today? - = t('admin.report_notes.today_at', time: l(@appeal.created_at, format: :time)) - - else - = l @appeal.created_at.to_date + %time.relative-formatted{ datetime: @appeal.created_at.iso8601 } + = t('admin.report_notes.created_at') .report-notes__item__content = simple_format(h(@appeal.text)) diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml index 595094fc76..078abd7882 100644 --- a/app/views/settings/featured_tags/index.html.haml +++ b/app/views/settings/featured_tags/index.html.haml @@ -26,6 +26,6 @@ - if featured_tag.last_status_at.nil? = t('accounts.nothing_here') - else - %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } .trends__item__current= friendly_number_to_human featured_tag.statuses_count diff --git a/config/locales/en.yml b/config/locales/en.yml index 2fcfd4ee16..9f71e5ed70 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -563,7 +563,6 @@ en: report_notes: created_msg: Report note successfully created! destroyed_msg: Report note successfully deleted! - today_at: Today at %{time} reports: account: notes: From 08c0e43b6fca879d7435f63b1b2c718a53c6cacc Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 15 Dec 2022 08:37:07 -0700 Subject: [PATCH 090/154] Increase the width of the unread notification border. (#21692) The smaller border is difficult to see for some users, especially when the browser window was thinner, and so the unread border is at the very left edge of the window. --- app/javascript/styles/mastodon/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f5d442a858..d03ab03c23 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7666,7 +7666,7 @@ noscript { left: 0; width: 100%; height: 100%; - border-left: 2px solid $highlight-text-color; + border-left: 4px solid $highlight-text-color; pointer-events: none; } } From 72a8af80886f270a27bd686f1fa86ef478748963 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Thu, 15 Dec 2022 10:37:17 -0500 Subject: [PATCH 091/154] Fix typo in handler function call name (#21829) --- app/javascript/mastodon/features/ui/components/columns_area.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index f4824f0457..e7def800e8 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -97,7 +97,7 @@ export default class ColumnsArea extends ImmutablePureComponent { if (this.mediaQuery.removeEventListener) { this.mediaQuery.removeEventListener('change', this.handleLayoutChange); } else { - this.mediaQuery.removeListener(this.handleLayouteChange); + this.mediaQuery.removeListener(this.handleLayoutChange); } } } From 22e36271c5c10fbf462fb385e12b50a015d0fd99 Mon Sep 17 00:00:00 2001 From: Colin Mitchell Date: Thu, 15 Dec 2022 10:38:37 -0500 Subject: [PATCH 092/154] Add environment variable to configure sidekiq concurrency (#19589) Co-authored-by: Effy Elden --- config/sidekiq.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 05c5b28c86..b8739aab33 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,5 +1,5 @@ --- -:concurrency: 5 +:concurrency: <%= ENV.fetch('SIDEKIQ_CONCURRENCY', 5) %> :queues: - [default, 8] - [push, 6] From 3d3429243fa0bacb20aac3db6c377441c0510f22 Mon Sep 17 00:00:00 2001 From: Dan Peterson Date: Thu, 15 Dec 2022 11:38:51 -0400 Subject: [PATCH 093/154] Fix default S3_HOSTNAME used in mastodon:setup (#19932) s3-us-east-1.amazonaws.com does not exist. Co-authored-by: Effy Elden --- lib/tasks/mastodon.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index c1e5bd2b45..3c891a07f0 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -194,7 +194,7 @@ namespace :mastodon do env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q| q.required true - q.default 's3-us-east-1.amazonaws.com' + q.default 's3.us-east-1.amazonaws.com' q.modify :strip end From 1f5740e65cc50ef3cc1feb7c0e5609df73d4173a Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 15 Dec 2022 05:39:41 -1000 Subject: [PATCH 094/154] Use Rails tag API to build RSS feed for spoilers and polls (#20163) * Use Rails tag API to build RSS feed for spoilers and polls While the previous method did not contain a bug or a potential issue, the tag API can be very resilient against future problems and reduces the amount of manual management of the escape status of the content. I've added tests to ensure that the formatting is broken and still escapes control characters correctly. * this seems cleaner and passes * Incorporate feedback by moving the br to its own line and using the tag helper over the string constant for the br tag itself * whoops, tag helper doesn't use a self-closing tag --- app/helpers/formatting_helper.rb | 25 +++++++++++++++++-------- spec/helpers/formatting_helper_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 spec/helpers/formatting_helper_spec.rb diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index a9d2f96512..c709314897 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -23,19 +23,28 @@ module FormattingHelper before_html = begin if status.spoiler_text? - "

#{I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)} #{h(status.spoiler_text)}


" - else - '' + tag.p do + tag.strong do + I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) + end + + status.spoiler_text + end + tag.hr end - end.html_safe # rubocop:disable Rails/OutputSafety + end after_html = begin if status.preloadable_poll - "

#{status.preloadable_poll.options.map { |o| " #{h(o)}" }.join('
')}

" - else - '' + tag.p do + safe_join( + status.preloadable_poll.options.map do |o| + tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true) + end, + tag.br + ) + end end - end.html_safe # rubocop:disable Rails/OutputSafety + end prerender_custom_emojis( safe_join([before_html, html, after_html]), diff --git a/spec/helpers/formatting_helper_spec.rb b/spec/helpers/formatting_helper_spec.rb new file mode 100644 index 0000000000..af604a87b5 --- /dev/null +++ b/spec/helpers/formatting_helper_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FormattingHelper, type: :helper do + include Devise::Test::ControllerHelpers + + describe '#rss_status_content_format' do + let(:status) { Fabricate(:status, text: 'Hello world<>', spoiler_text: 'This is a spoiler<>', poll: Fabricate(:poll, options: %w(Yes<> No))) } + let(:html) { helper.rss_status_content_format(status) } + + it 'renders the spoiler text' do + expect(html).to include('

This is a spoiler<>


') + end + + it 'renders the status text' do + expect(html).to include('

Hello world<>

') + end + + it 'renders the poll' do + expect(html).to include('Yes<>
') + end + end +end From 19f78ea8fadc4626f7b2db5dbe37accbde6a968c Mon Sep 17 00:00:00 2001 From: Kaspar V Date: Thu, 15 Dec 2022 16:39:59 +0100 Subject: [PATCH 095/154] linting: RuboCop update, config fixes (#20574) * fix(rubocop): update gems and add performance and rspec fix(rubocop): update gems and add performance and rspec - update present rubocop gems - add rubocop-rspec and rubocop-performance gems - move rubocop gems to gem group :development, :test in order to make linting in a github action that runs with RAILS_ENV=test possible * feat(rubocop): disable some annoyance RSpec cops To mee these prooved to be more annoying than helpful. If not agreed, they can be enabled any time. * fix(rubocop): do not ignore spec/**/* Because rubocop-rspec should lint the specs as well, and they deserve to be readable in general. It is relevant code, after all. * fix(rubocop): change ignore db/**/* to db/schema.rb because rails cops do some lints for migrations. E.g. reversable migrations linting and more. * fix(rubocop): tune rules configs Bunch of commits squashed: fix(rubocop): enable Layout/LineLength cop Because this project has code with line lenghts > 500 chars. This is not good practice at all, so I strongly suggest to change the practice in the future. But allow heredoc, URI and comments to still be long lines and make the default Max: 120 explicit, by repeating it in the config. To me this max length seems reasonable. Perhaps a bit more could be ok for some. But > 500 chars in one line Seems to be way too long IMHO. fix(rubocop): Metrics/CyclomaticComplexity Max to 12 The default is 7, perhaps quite strict. But 25 is too loose, the rule becomes pointless like that. fix(rubocop): AllCops ruby version, cacheing and more info - fix the target ruby version from 2.5 to 3.0 - have the cop error messages to be more informative and helpful - enable cacheing in /tmp fix(rubocop): Metrics/AbcSize to 34 from 115 Rubocops default is 17. If the rule is at 115 is becomes pointless. fix(rubocop): Metrics/BlockLength improvements - instead of ignoring tasks completely, ignore only the long blocks that are specific to tasks (task, namespace) - ignore also concern specific block methods (included, class_methods) fix(rubocop): Metrics/ClassLength count heredoc array as one line fix(rubocop): Metrics/MethodLength Max to 25 - the default is 10, but 65 is too loose, so perhaps 25? fix(rubocop): Metrics/ModuleLength array and heredoc count as one fix(rubocop): Metrics/PerceivedComplexity to 16 from 25 Rubocops default is 8, so how about only doubling that, instead of > than tripple it? fix(rubocop): enable Style/RedundantAssignment Because I think that this rule would never really hurt, but improve code quality and readability. fix(rubocop): enable Style/RescueStandardError I think everyone that ever had to debug what this can bring will hopefully agree that this rule totally makes sense. In the super rare exeptions where this is totally needed, it can be excluded by disabling comment in that place. fix(rubocop): Metrics/ParameterLists add explicit defaults and some excludes --- .rubocop.yml | 200 ++++++++++++++++++++++++++++++++++++++++++++++----- Gemfile | 6 +- Gemfile.lock | 30 +++++--- 3 files changed, 209 insertions(+), 27 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index aec11b0306..67284fe34b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,12 +1,18 @@ require: - rubocop-rails + - rubocop-rspec + - rubocop-performance AllCops: TargetRubyVersion: 2.7 - NewCops: disable + DisplayCopNames: true + DisplayStyleGuide: true + ExtraDetails: true + UseCache: true + CacheRootDirectory: tmp + NewCops: enable Exclude: - - 'spec/**/*' - - 'db/**/*' + - db/schema.rb - 'app/views/**/*' - 'config/**/*' - 'bin/*' @@ -67,15 +73,57 @@ Lint/UselessAccessModifier: - class_methods Metrics/AbcSize: - Max: 115 + Max: 34 # RuboCop default 17 Exclude: - - 'lib/mastodon/*_cli.rb' + - 'lib/**/*cli*.rb' + - db/*migrate/**/* + - lib/paperclip/color_extractor.rb + - app/workers/scheduler/follow_recommendations_scheduler.rb + - app/services/activitypub/fetch*_service.rb + - lib/paperclip/**/* + CountRepeatedAttributes: false + AllowedMethods: + - update_media_attachments! + - account_link_to + - attempt_oembed + - build_crutches + - calculate_scores + - cc + - dump_actor! + - filter_from_home? + - hydrate + - import_bookmarks! + - import_relationships! + - initialize + - link_to_mention + - log_target + - matches_time_window? + - parse_metadata + - perform_statuses_search! + - privatize_media_attachments! + - process_update + - publish_media_attachments! + - remotable_attachment + - render_initial_state + - render_with_cache + - searchable_by + - self.cached_filters_for + - set_fetchable_attributes! + - signed_request_actor + - statuses_to_delete + - update_poll! Metrics/BlockLength: Max: 55 Exclude: - - 'lib/tasks/**/*' - 'lib/mastodon/*_cli.rb' + CountComments: false + CountAsOne: [array, heredoc] + AllowedMethods: + - task + - namespace + - class_methods + - included Metrics/BlockNesting: Max: 3 @@ -85,34 +133,144 @@ Metrics/BlockNesting: Metrics/ClassLength: CountComments: false Max: 500 + CountAsOne: [array, heredoc] Exclude: - 'lib/mastodon/*_cli.rb' Metrics/CyclomaticComplexity: - Max: 25 + Max: 12 Exclude: - - 'lib/mastodon/*_cli.rb' + - lib/mastodon/*cli*.rb + - db/*migrate/**/* + AllowedMethods: + - attempt_oembed + - blocked? + - build_crutches + - calculate_scores + - cc + - discover_endpoint! + - filter_from_home? + - hydrate + - klass + - link_to_mention + - log_target + - matches_time_window? + - patch_for_forwarding! + - preprocess_attributes! + - process_update + - remotable_attachment + - scan_text! + - self.cached_filters_for + - set_fetchable_attributes! + - setup_redis_env_url + - update_media_attachments! Layout/LineLength: + Max: 140 # RuboCop default 120 + AllowHeredoc: true AllowURI: true - Enabled: false + IgnoreCopDirectives: true + AllowedPatterns: + # Allow comments to be long lines + - !ruby/regexp / \# .*$/ + - !ruby/regexp /^\# .*$/ + Exclude: + - lib/**/*cli*.rb + - db/*migrate/**/* + - db/seeds/**/* Metrics/MethodLength: CountComments: false - Max: 65 + CountAsOne: [array, heredoc] + Max: 25 # RuboCop default 10 Exclude: - 'lib/mastodon/*_cli.rb' + AllowedMethods: + - account_link_to + - attempt_oembed + - body_with_limit + - build_crutches + - cached_filters_for + - calculate_scores + - check_webfinger! + - clean_feeds! + - collection_items + - collection_presenter + - copy_account_notes! + - deduplicate_accounts! + - deduplicate_conversations! + - deduplicate_local_accounts! + - deduplicate_statuses! + - deduplicate_tags! + - deduplicate_users! + - discover_endpoint! + - extract_extra_uris_with_indices + - extract_hashtags_with_indices + - extract_mentions_or_lists_with_indices + - filter_from_home? + - from_elasticsearch + - handle_explicit_update! + - handle_mark_as_sensitive! + - hsl_to_rgb + - import_bookmarks! + - import_domain_blocks! + - import_relationships! + - ldap_options + - matches_time_window? + - outbox_presenter + - pam_get_user + - parallelize_with_progress + - parse_and_transform + - patch_for_forwarding! + - populate_home + - post_process_style + - preload_cache_collection_target_statuses + - privatize_media_attachments! + - provides_callback_for + - publish_media_attachments! + - relevant_account_timestamp + - remotable_attachment + - rgb_to_hsl + - rss_status_content_format + - set_fetchable_attributes! + - setup_redis_env_url + - signed_request_actor + - to_preview_card_attributes + - upgrade_storage_filesystem + - upgrade_storage_s3 + - user_settings_params + - hydrate + - cc + - self_destruct Metrics/ModuleLength: CountComments: false Max: 200 + CountAsOne: [array, heredoc] Metrics/ParameterLists: - Max: 5 - CountKeywordArgs: true + Max: 5 # RuboCop default 5 + CountKeywordArgs: true # RuboCop default true + MaxOptionalParameters: 3 # RuboCop default 3 + Exclude: + - app/models/concerns/account_interactions.rb + - app/services/activitypub/fetch_remote_account_service.rb + - app/services/activitypub/fetch_remote_actor_service.rb Metrics/PerceivedComplexity: - Max: 25 + Max: 16 # RuboCop default 8 + AllowedMethods: + - attempt_oembed + - build_crutches + - calculate_scores + - deduplicate_users! + - discover_endpoint! + - filter_from_home? + - hydrate + - patch_for_forwarding! + - process_update + - remove_orphans + - update_media_attachments! Naming/MemoizedInstanceVariableName: Enabled: false @@ -267,9 +425,6 @@ Style/PercentLiteralDelimiters: Style/PerlBackrefs: AutoCorrect: false -Style/RedundantAssignment: - Enabled: false - Style/RedundantFetchBlock: Enabled: true @@ -292,7 +447,7 @@ Style/RegexpLiteral: Enabled: false Style/RescueStandardError: - Enabled: false + Enabled: true Style/SignalException: Enabled: false @@ -311,3 +466,14 @@ Style/TrailingCommaInHashLiteral: Style/UnpackFirst: Enabled: false + +RSpec/ScatteredSetup: + Enabled: false +RSpec/ImplicitExpect: + Enabled: false +RSpec/NamedSubject: + Enabled: false +RSpec/DescribeClass: + Enabled: false +RSpec/LetSetup: + Enabled: false diff --git a/Gemfile b/Gemfile index 9fe2a11209..92620d991c 100644 --- a/Gemfile +++ b/Gemfile @@ -107,6 +107,10 @@ group :development, :test do gem 'pry-byebug', '~> 3.10' gem 'pry-rails', '~> 0.3' gem 'rspec-rails', '~> 5.1' + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + gem 'rubocop', require: false end group :production, :test do @@ -136,8 +140,6 @@ group :development do gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' gem 'memory_profiler' - gem 'rubocop', '~> 1.30', require: false - gem 'rubocop-rails', '~> 2.15', require: false gem 'brakeman', '~> 5.4', require: false gem 'bundler-audit', '~> 0.9', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ac3f560ea1..25ffebdbe1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -587,21 +587,27 @@ GEM rspec-support (3.11.1) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.30.1) + rubocop (1.39.0) + json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.0.0) + parser (>= 3.1.2.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.18.0, < 2.0) + rubocop-ast (>= 1.23.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.18.0) + rubocop-ast (1.23.0) parser (>= 3.1.1.0) - rubocop-rails (2.15.0) + rubocop-performance (1.15.1) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.17.2) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 1.7.0, < 2.0) + rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.15.0) + rubocop (~> 1.33) ruby-progressbar (1.11.0) ruby-saml (1.13.0) nokogiri (>= 1.10.5) @@ -843,8 +849,10 @@ DEPENDENCIES rspec-rails (~> 5.1) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.6) - rubocop (~> 1.30) - rubocop-rails (~> 2.15) + rubocop + rubocop-performance + rubocop-rails + rubocop-rspec ruby-progressbar (~> 1.11) sanitize (~> 6.0) scenic (~> 1.6) @@ -869,3 +877,9 @@ DEPENDENCIES webpacker (~> 5.4) webpush! xorcist (~> 1.1) + +RUBY VERSION + ruby 3.0.4p208 + +BUNDLED WITH + 2.2.33 From 623d3d2e32ac8ec2819f2cd99e6565d06c9b0023 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 16:40:32 +0100 Subject: [PATCH 096/154] Change CSP directives on API to be tight and concise (#20960) --- app/controllers/api/base_controller.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index defef0656f..41f3ce2ee3 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -16,6 +16,26 @@ class Api::BaseController < ApplicationController protect_from_forgery with: :null_session + content_security_policy do |p| + # Set every directive that does not have a fallback + p.default_src :none + p.frame_ancestors :none + p.form_action :none + + # Disable every directive with a fallback to cut on response size + p.base_uri false + p.font_src false + p.img_src false + p.style_src false + p.media_src false + p.frame_src false + p.manifest_src false + p.connect_src false + p.script_src false + p.child_src false + p.worker_src false + end + rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| render json: { error: e.to_s }, status: 422 end From 38596e49d4ea00b9a538f32fd443afc6aa29824a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 16:40:45 +0100 Subject: [PATCH 097/154] Fix the top action bar appearing in multi-column layout (#20943) --- app/javascript/styles/mastodon/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index d03ab03c23..0cde9f55c0 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2528,7 +2528,7 @@ $ui-header-height: 55px; } } - .ui__header { + .layout-single-column .ui__header { display: flex; background: $ui-base-color; border-bottom: 1px solid lighten($ui-base-color, 8%); From 725f21662f7ba287b36adc1d973c2aa57296c781 Mon Sep 17 00:00:00 2001 From: Fries <40834252+ayefries@users.noreply.github.com> Date: Thu, 15 Dec 2022 16:40:57 +0100 Subject: [PATCH 098/154] Add Montenegrin (cnr) (#21013) --- app/helpers/languages_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index fff073cedb..5e70a8f5cf 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -190,6 +190,7 @@ module LanguagesHelper ISO_639_3 = { ast: ['Asturian', 'Asturianu'].freeze, ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, + cnr: ['Montenegrin', 'crnogorski'].freeze, jbo: ['Lojban', 'la .lojban.'].freeze, kab: ['Kabyle', 'Taqbaylit'].freeze, kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].freeze, From bbc49f15e030df4e75af06ece8c5302b80b69342 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 16:44:29 +0100 Subject: [PATCH 099/154] Add explanation text to log-in page (#20946) --- app/views/auth/sessions/new.html.haml | 2 ++ config/locales/en.yml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 304e3ab849..e98c1ff3de 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -6,6 +6,8 @@ - unless omniauth_only? = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| + %h1.title= t('auth.sign_in.title', domain: site_hostname) + %p.lead= t('auth.sign_in.preamble_html', domain: site_hostname) .fields-group - if use_seamless_external_login? = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label': t('simple_form.labels.defaults.username_or_email') }, hint: false diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f71e5ed70..075ce2136f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -975,6 +975,9 @@ en: email_below_hint_html: If the below e-mail address is incorrect, you can change it here and receive a new confirmation e-mail. email_settings_hint_html: The confirmation e-mail was sent to %{email}. If that e-mail address is not correct, you can change it in account settings. title: Setup + sign_in: + preamble_html: Sign in with your %{domain} credentials. If your account is hosted on a different server, you will not be able to log in here. + title: Sign in to %{domain} sign_up: preamble: With an account on this Mastodon server, you'll be able to follow any other person on the network, regardless of where their account is hosted. title: Let's get you set up on %{domain}. From 673c54f114be78e0588f2bb883f2962dbc7574a7 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 17:04:38 +0100 Subject: [PATCH 100/154] Fix inability to use local LibreTranslate without setting ALLOWED_PRIVATE_ADDRESSES (#21926) Fixes #20029 --- app/lib/request.rb | 3 ++- app/lib/translation_service/libre_translate.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index 96d934a8f2..b2819c8edc 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -30,7 +30,8 @@ class Request @verb = verb @url = Addressable::URI.parse(url).normalize @http_client = options.delete(:http_client) - @options = options.merge(socket_class: use_proxy? ? ProxySocket : Socket) + @allow_local = options.delete(:allow_local) + @options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket) @options = @options.merge(proxy_url) if use_proxy? @headers = {} diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb index 43576e3062..4ebe21e454 100644 --- a/app/lib/translation_service/libre_translate.rb +++ b/app/lib/translation_service/libre_translate.rb @@ -27,7 +27,7 @@ class TranslationService::LibreTranslate < TranslationService def request(text, source_language, target_language) body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) - req = Request.new(:post, "#{@base_url}/translate", body: body) + req = Request.new(:post, "#{@base_url}/translate", body: body, allow_local: true) req.add_headers('Content-Type': 'application/json') req end From 059d64a59ea27c40ca6b5b9bf34487169d2a741f Mon Sep 17 00:00:00 2001 From: Meisam <39205857+MFTabriz@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:04:52 +0100 Subject: [PATCH 101/154] set activation for tag follow button (#21629) Co-authored-by: meisam --- app/javascript/mastodon/features/hashtag_timeline/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index b635c35291..733f54ff3a 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -194,7 +194,7 @@ class HashtagTimeline extends React.PureComponent { const following = tag.get('following'); followButton = ( - ); From 1e95fa3df5ad65051bf598da91d589f69b652959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=ADas=20P=C3=A1ll=20Gissurarson?= Date: Thu, 15 Dec 2022 17:05:40 +0100 Subject: [PATCH 102/154] Fix punycoded local domains not being prettified in initial state (#21440) --- app/serializers/initial_state_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 8d3f4f87df..70f40088dc 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -16,7 +16,7 @@ class InitialStateSerializer < ActiveModel::Serializer streaming_api_base_url: Rails.configuration.x.streaming_api_base_url, access_token: object.token, locale: I18n.locale, - domain: instance_presenter.domain, + domain: Addressable::IDNA.to_unicode(instance_presenter.domain), title: instance_presenter.title, admin: object.admin&.id&.to_s, search_enabled: Chewy.enabled?, From 7972e5981c312eb11e5f7f5676e7d565881ccdaa Mon Sep 17 00:00:00 2001 From: Yurii Izorkin Date: Thu, 15 Dec 2022 19:07:36 +0300 Subject: [PATCH 103/154] Add brotli compression (#19025) --- config/webpack/production.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/webpack/production.js b/config/webpack/production.js index 79dcebc7c4..143a23b99e 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -34,6 +34,12 @@ module.exports = merge(sharedConfig, { cache: true, test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/, }), + new CompressionPlugin({ + filename: '[path][base].br[query]', + algorithm: 'brotliCompress', + cache: true, + test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/, + }), new BundleAnalyzerPlugin({ // generates report.html analyzerMode: 'static', openAnalyzer: false, From 8f8c0fe88c19b38602d3f9de7742211b1b690af0 Mon Sep 17 00:00:00 2001 From: Luxiaba <5391976+luxiaba@users.noreply.github.com> Date: Fri, 16 Dec 2022 00:10:34 +0800 Subject: [PATCH 104/154] Remove inline-css in logo (#20814) --- app/javascript/images/logo-symbol-icon.svg | 2 +- app/javascript/images/logo-symbol-wordmark.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/images/logo-symbol-icon.svg b/app/javascript/images/logo-symbol-icon.svg index 56cf03921a..c4c14f098a 100644 --- a/app/javascript/images/logo-symbol-icon.svg +++ b/app/javascript/images/logo-symbol-icon.svg @@ -1,2 +1,2 @@ - + diff --git a/app/javascript/images/logo-symbol-wordmark.svg b/app/javascript/images/logo-symbol-wordmark.svg index 7e7f7b087c..ee0b636d93 100644 --- a/app/javascript/images/logo-symbol-wordmark.svg +++ b/app/javascript/images/logo-symbol-wordmark.svg @@ -7,5 +7,5 @@ - + From d412147d02e84cb76b252706a5357fe5d434c3db Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Fri, 16 Dec 2022 01:11:14 +0900 Subject: [PATCH 105/154] Save avatar or header correctly even if other one fails (#18465) * Save avatar or header correctly if other one fails * Fix test --- app/models/account.rb | 12 +++++++++--- spec/models/account_spec.rb | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index fc7359cfc4..a7bda15d3a 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -341,9 +341,15 @@ class Account < ApplicationRecord def save_with_optional_media! save! - rescue ActiveRecord::RecordInvalid - self.avatar = nil - self.header = nil + rescue ActiveRecord::RecordInvalid => e + errors = e.record.errors.errors + errors.each do |err| + if err.attribute == :avatar + self.avatar = nil + elsif err.attribute == :header + self.header = nil + end + end save! end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index edae05f9db..c9d782cee9 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -160,7 +160,7 @@ RSpec.describe Account, type: :model do expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar' expect(account.header_remote_url).to eq expectation.header_remote_url expect(account.avatar_file_name).to eq nil - expect(account.header_file_name).to eq nil + expect(account.header_file_name).to eq expectation.header_file_name end end end From 1b5d20713187465c13986eea0f3a487d254b6e63 Mon Sep 17 00:00:00 2001 From: David Vega Date: Thu, 15 Dec 2022 08:11:58 -0800 Subject: [PATCH 106/154] Fix single name variables on controller folder (#20092) Co-authored-by: petrokoriakin1 <116151189+petrokoriakin1@users.noreply.github.com> Co-authored-by: petrokoriakin1 <116151189+petrokoriakin1@users.noreply.github.com> Co-authored-by: Effy Elden --- app/controllers/auth/registrations_controller.rb | 4 ++-- app/controllers/concerns/rate_limit_headers.rb | 2 +- app/controllers/concerns/signature_verification.rb | 4 ++-- app/controllers/follower_accounts_controller.rb | 2 +- app/controllers/following_accounts_controller.rb | 2 +- app/controllers/media_controller.rb | 4 ++-- app/controllers/statuses_controller.rb | 4 ++-- app/controllers/tags_controller.rb | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index cd1c546b80..71c0cd8271 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -56,8 +56,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def configure_sign_up_params - devise_parameter_sanitizer.permit(:sign_up) do |u| - u.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) + devise_parameter_sanitizer.permit(:sign_up) do |user_params| + user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) end end diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb index 86fe58a71c..b8696df736 100644 --- a/app/controllers/concerns/rate_limit_headers.rb +++ b/app/controllers/concerns/rate_limit_headers.rb @@ -58,7 +58,7 @@ module RateLimitHeaders end def api_throttle_data - most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] - v[:count] } + most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_key, value| value[:limit] - value[:count] } request.env['rack.attack.throttle_data'][most_limited_type] end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 2394574b3b..4502da698c 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -28,8 +28,8 @@ module SignatureVerification end class SignatureParamsTransformer < Parslet::Transform - rule(params: subtree(:p)) do - (p.is_a?(Array) ? p : [p]).each_with_object({}) { |(key, val), h| h[key] = val } + rule(params: subtree(:param)) do + (param.is_a?(Array) ? param : [param]).each_with_object({}) { |(key, value), hash| hash[key] = value } end rule(param: { key: simple(:key), value: simple(:val) }) do diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index e4d8cc4950..9ced184491 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -63,7 +63,7 @@ class FollowerAccountsController < ApplicationController id: account_followers_url(@account, page: params.fetch(:page, 1)), type: :ordered, size: @account.followers_count, - items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }, + items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.account) }, part_of: account_followers_url(@account), next: next_page_url, prev: prev_page_url diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index f84dca1e5a..febd13c975 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -66,7 +66,7 @@ class FollowingAccountsController < ApplicationController id: account_following_index_url(@account, page: params.fetch(:page, 1)), type: :ordered, size: @account.following_count, - items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }, + items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.target_account) }, part_of: account_following_index_url(@account), next: next_page_url, prev: prev_page_url diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index ee82625a03..3cdd97f067 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -12,8 +12,8 @@ class MediaController < ApplicationController before_action :check_playable, only: :player before_action :allow_iframing, only: :player - content_security_policy only: :player do |p| - p.frame_ancestors(false) + content_security_policy only: :player do |policy| + policy.frame_ancestors(false) end def show diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 9eb7ad691d..0e0783b4b2 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -17,8 +17,8 @@ class StatusesController < ApplicationController skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? - content_security_policy only: :embed do |p| - p.frame_ancestors(false) + content_security_policy only: :embed do |policy| + policy.frame_ancestors(false) end def show diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index f0a0993506..65017acba3 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -65,7 +65,7 @@ class TagsController < ApplicationController id: tag_url(@tag), type: :ordered, size: @tag.statuses.count, - items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } + items: @statuses.map { |status| ActivityPub::TagManager.instance.uri_for(status) } ) end end From 303cd4038a3c9bf6640105eadd720d73a60b6050 Mon Sep 17 00:00:00 2001 From: fef Date: Thu, 15 Dec 2022 15:27:54 +0000 Subject: [PATCH 107/154] 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 cedf1383138b2e58ba6ff9aab46beddeaf1a1354 Mon Sep 17 00:00:00 2001 From: Mina Her Date: Fri, 16 Dec 2022 01:24:38 +0900 Subject: [PATCH 108/154] Fix margin for search field on medium window size (#21606) --- app/javascript/styles/mastodon/components.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0cde9f55c0..15fc6aa690 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2474,8 +2474,7 @@ $ui-header-height: 55px; height: calc(100% - 10px) !important; } - .getting-started__wrapper, - .search { + .getting-started__wrapper { margin-bottom: 10px; } @@ -4671,6 +4670,7 @@ a.status-card.compact:hover { } .search { + margin-bottom: 10px; position: relative; } From 3656a6b9cc353f7f08a2d8f00c1b3f2fd8e3fb21 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Fri, 16 Dec 2022 01:30:47 +0900 Subject: [PATCH 109/154] Add "disabled" user filter for admin/accounts UI (#21282) --- app/models/account_filter.rb | 2 +- app/views/admin/accounts/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index 3a4ac04923..d27bb46fcf 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -81,7 +81,7 @@ class AccountFilter when 'suspended' Account.suspended when 'disabled' - accounts_with_users.merge(User.disabled) + accounts_with_users.merge(User.disabled).without_suspended when 'silenced' Account.silenced when 'sensitized' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index f33f788ed2..d0897221db 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -13,7 +13,7 @@ .filter-subset.filter-subset--with-select %strong= t('admin.accounts.moderation.title') .input.select.optional - = select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all') + = select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.disabled'), 'disabled'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all') .filter-subset.filter-subset--with-select %strong= t('admin.accounts.role') .input.select.optional From 9f63c428e188a11f85741aac72a62ef2f7b5421b Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Thu, 15 Dec 2022 17:37:05 +0100 Subject: [PATCH 110/154] Don't autofocus the compose form (#16517) When opening a page such as /web/timelines/home in a desktop browser, the cursor was automatically placed in the textarea of the compose form. When using the keyboard for navigation (using a browser plugin like vimium or vim vixen, or just to hit 'space' to scroll down a page), you have remember to leave the field before using that. Since you only visit the page to write a new post some of the time, this PR attempts to have nothing focused initially (and require the user to click or e.g. use 'tab' to focus the textarea). Tested: * /web/timeslines/home no longer autofocuses the compose box * pressing the 'n' hotkey still focuses the compose box * clicking 'reply' for a post still focuses the compose box * replying to a CW'ed post still focuses the compose box * introducing the CW field still focuses the CW field * introducing the CW field for a reply still focuses the CW field * removing the CW field still focuses the compose box * /web/statuses/new still autofocuses the compose box fixes #15862 --- .../features/compose/components/compose_form.js | 13 ++++++------- .../compose/containers/compose_form_container.js | 1 - app/javascript/mastodon/features/compose/index.js | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 6a65f44da3..234a2afed7 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -16,7 +16,6 @@ import PollFormContainer from '../containers/poll_form_container'; import UploadFormContainer from '../containers/upload_form_container'; import WarningContainer from '../containers/warning_container'; import LanguageDropdown from '../containers/language_dropdown_container'; -import { isMobile } from '../../../is_mobile'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; import { countableText } from '../util/counter'; @@ -61,14 +60,14 @@ class ComposeForm extends ImmutablePureComponent { onChangeSpoilerText: PropTypes.func.isRequired, onPaste: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired, - showSearch: PropTypes.bool, + autoFocus: PropTypes.bool, anyMedia: PropTypes.bool, isInReply: PropTypes.bool, singleColumn: PropTypes.bool, }; static defaultProps = { - showSearch: false, + autoFocus: false, }; handleChange = (e) => { @@ -154,7 +153,7 @@ class ComposeForm extends ImmutablePureComponent { // - Replying to zero or one users, places the cursor at the end of the textbox. // - Replying to more than one user, selects any usernames past the first; // this provides a convenient shortcut to drop everyone else from the conversation. - if (this.props.focusDate !== prevProps.focusDate) { + if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) { let selectionEnd, selectionStart; if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) { @@ -180,7 +179,7 @@ class ComposeForm extends ImmutablePureComponent { } else if (this.props.spoiler !== prevProps.spoiler) { if (this.props.spoiler) { this.spoilerText.input.focus(); - } else { + } else if (prevProps.spoiler) { this.autosuggestTextarea.textarea.focus(); } } @@ -207,7 +206,7 @@ class ComposeForm extends ImmutablePureComponent { } render () { - const { intl, onPaste, showSearch } = this.props; + const { intl, onPaste, autoFocus } = this.props; const disabled = this.props.isSubmitting; let publishText = ''; @@ -257,7 +256,7 @@ class ComposeForm extends ImmutablePureComponent { onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionSelected={this.onSuggestionSelected} onPaste={onPaste} - autoFocus={!showSearch && !isMobile(window.innerWidth)} + autoFocus={autoFocus} > diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 1be7633ccd..14cf9230bc 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -24,7 +24,6 @@ const mapStateToProps = state => ({ isEditing: state.getIn(['compose', 'id']) !== null, isChangingUpload: state.getIn(['compose', 'is_changing_upload']), isUploading: state.getIn(['compose', 'is_uploading']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, isInReply: state.getIn(['compose', 'in_reply_to']) !== null, }); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index f744fc6115..aead7776ac 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -18,6 +18,7 @@ import Icon from 'mastodon/components/icon'; import { logOut } from 'mastodon/utils/log_out'; import Column from 'mastodon/components/column'; import { Helmet } from 'react-helmet'; +import { isMobile } from '../../is_mobile'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -115,7 +116,7 @@ class Compose extends React.PureComponent {
- +
From ff414a5489fd2c6d154b18effca24dde54d92958 Mon Sep 17 00:00:00 2001 From: Terence Eden Date: Thu, 15 Dec 2022 16:38:35 +0000 Subject: [PATCH 111/154] Add transparancy to modal background for accessibility (#18081) Fixes #18080 This keeps the `ui-base-lighter-color` but adds enough transparency so that text is more easily readable. Tested in Firefox and Chrome. --- app/javascript/styles/mastodon/modal.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/modal.scss b/app/javascript/styles/mastodon/modal.scss index 6c6de4206c..a333926dd1 100644 --- a/app/javascript/styles/mastodon/modal.scss +++ b/app/javascript/styles/mastodon/modal.scss @@ -1,5 +1,5 @@ .modal-layout { - background: $ui-base-color url('data:image/svg+xml;utf8,') repeat-x bottom fixed; + background: $ui-base-color url('data:image/svg+xml;utf8,') repeat-x bottom fixed; display: flex; flex-direction: column; height: 100vh; From 726c7dea31d2ee60b327afd327e945e3ece09ac4 Mon Sep 17 00:00:00 2001 From: Rens Groothuijsen Date: Thu, 15 Dec 2022 17:38:50 +0100 Subject: [PATCH 112/154] Display search popout at fixed screen position (#16463) * Display search popout at fixed screen position * Attach search popout to search box --- app/javascript/mastodon/features/compose/components/search.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 03e6dcf2c8..8254fb607a 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -140,8 +140,7 @@ class Search extends React.PureComponent {
- - +
From 8a56587d62d08606e5ed11eb1b93e238cf5aa2fe Mon Sep 17 00:00:00 2001 From: Avdi Grimm Date: Thu, 15 Dec 2022 10:40:36 -0600 Subject: [PATCH 113/154] Improve devcontainer for running tests (#22277) * Improve devcontainer for running tests - Pull devcontainer post-create out into its own script - Add asset precompilation - Add test-mode asset precompilation (needed to run tests without error) * Document Gemfile.lock re-checkout in devcontainer --- .devcontainer/devcontainer.json | 2 +- .devcontainer/post-create.sh | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100755 .devcontainer/post-create.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 47497794fb..5ac56c8428 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,7 +20,7 @@ "forwardPorts": [3000, 4000], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "bundle install --path vendor/bundle && yarn install && git checkout -- Gemfile.lock && ./bin/rails db:setup", + "postCreateCommand": ".devcontainer/post-create.sh", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 0000000000..02f488f120 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e # Fail the whole script on first error + +# Fetch Ruby gem dependencies +bundle install --path vendor/bundle --with='development test' + +# Fetch Javascript dependencies +yarn install + +# Make Gemfile.lock pristine again +git checkout -- Gemfile.lock + +# [re]create, migrate, and seed the test database +RAILS_ENV=test ./bin/rails db:setup + +# Precompile assets for development +RAILS_ENV=development ./bin/rails assets:precompile + +# Precompile assets for test +RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile From fb1d9789dba288599e05ba813af0c61f484e205c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 17:41:20 +0100 Subject: [PATCH 114/154] Fix attachment rendering of edited posts in OpenGraph (#22270) Fixes #22241 --- app/helpers/statuses_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 488eabeec5..d1e3fddafe 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -21,7 +21,7 @@ module StatusesHelper def media_summary(status) attachments = { image: 0, video: 0, audio: 0 } - status.media_attachments.each do |media| + status.ordered_media_attachments.each do |media| if media.video? attachments[:video] += 1 elsif media.audio? From 1e49be33289b969be64620b904d589158e7b579a Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Fri, 16 Dec 2022 03:43:26 +1100 Subject: [PATCH 115/154] Align everything to Node.js 16 (#22223) * Update nvmrc to Node.js 16 * Update package.json minimum Node engine to 16 * Update README requirements to Node.js 16 * Update devcontainer Node.js version to 16 * Update devcontainer Dockerfile Node.js choices to LTS versions that are still in support/maintenance * Pin CircleCI Node image to 16 * Fix YAML type issue * Update CircleCI Node.js to 16.18 to match #22019 --- .circleci/config.yml | 2 +- .devcontainer/Dockerfile | 2 +- .devcontainer/docker-compose.yml | 2 +- .nvmrc | 2 +- README.md | 2 +- package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bddfd2d27a..82d9f9ef63 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -221,5 +221,5 @@ workflows: pkg-manager: yarn requires: - build - version: lts + version: '16.18' yarn-run: test:jest diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ac495e1c91..425b86a6bb 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,7 +9,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} # The value is a comma-separated list of allowed domains ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev" -# [Choice] Node.js version: lts/*, 16, 14, 12, 10 +# [Choice] Node.js version: lts/*, 18, 16, 14 ARG NODE_VERSION="lts/*" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 46f42c4549..a61116fca4 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -11,7 +11,7 @@ services: # Use -bullseye variants on local arm64/Apple Silicon. VARIANT: '3.0-bullseye' # Optional Node.js version to install - NODE_VERSION: '14' + NODE_VERSION: '16' volumes: - ..:/workspaces/mastodon:cached environment: diff --git a/.nvmrc b/.nvmrc index 8351c19397..b6a7d89c68 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14 +16 diff --git a/README.md b/README.md index 277ae0cc16..ddd5e2c648 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre - **PostgreSQL** 9.5+ - **Redis** 4+ - **Ruby** 2.7+ -- **Node.js** 14+ +- **Node.js** 16+ The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. diff --git a/package.json b/package.json index dd93102ed6..9f0c836fda 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@mastodon/mastodon", "license": "AGPL-3.0-or-later", "engines": { - "node": ">=14" + "node": ">=16" }, "scripts": { "postversion": "git push --tags", From 8556a649d58a7291db6942a2594533f9b8c06165 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 17:45:02 +0100 Subject: [PATCH 116/154] Fix changing domain block severity not undoing individual account effects (#22135) * Fix changing domain block severity not undoing individual account effects Fixes #22133 * Add tests --- .../admin/domain_blocks_controller.rb | 8 +--- .../api/v1/admin/domain_blocks_controller.rb | 6 +-- .../admin/domain_blocks_controller_spec.rb | 47 +++++++++++++++++++ .../v1/admin/domain_blocks_controller_spec.rb | 47 +++++++++++++++++++ 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index e79f7a43e1..74764640b8 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -55,12 +55,8 @@ module Admin def update authorize :domain_block, :update? - @domain_block.update(update_params) - - severity_changed = @domain_block.severity_changed? - - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id, severity_changed) + if @domain_block.update(update_params) + DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') else diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index df5b1b3fcb..8b77e9717d 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -40,10 +40,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController def update authorize @domain_block, :update? - @domain_block.update(domain_block_params) - severity_changed = @domain_block.severity_changed? - @domain_block.save! - DomainBlockWorker.perform_async(@domain_block.id, severity_changed) + @domain_block.update!(domain_block_params) + DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer end diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index 98cda50047..f432060d98 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -70,6 +70,53 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do end end + describe 'PUT #update' do + let!(:remote_account) { Fabricate(:account, domain: 'example.com') } + let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) } + + before do + BlockDomainService.new.call(domain_block) + end + + let(:subject) do + post :update, params: { id: domain_block.id, domain_block: { domain: 'example.com', severity: new_severity } } + end + + context 'downgrading a domain suspension to silence' do + let(:original_severity) { 'suspend' } + let(:new_severity) { 'silence' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence') + end + + it 'undoes individual suspensions' do + expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false) + end + + it 'performs individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true) + end + end + + context 'upgrading a domain silence to suspend' do + let(:original_severity) { 'silence' } + let(:new_severity) { 'suspend' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend') + end + + it 'undoes individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false) + end + + it 'performs individual suspends' do + expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true) + end + end + end + describe 'DELETE #destroy' do it 'unblocks the domain' do service = double(call: true) diff --git a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb index f12285b2a6..606def602f 100644 --- a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb @@ -71,6 +71,53 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do end end + describe 'PUT #update' do + let!(:remote_account) { Fabricate(:account, domain: 'example.com') } + let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) } + + before do + BlockDomainService.new.call(domain_block) + end + + let(:subject) do + post :update, params: { id: domain_block.id, domain: 'example.com', severity: new_severity } + end + + context 'downgrading a domain suspension to silence' do + let(:original_severity) { 'suspend' } + let(:new_severity) { 'silence' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence') + end + + it 'undoes individual suspensions' do + expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false) + end + + it 'performs individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true) + end + end + + context 'upgrading a domain silence to suspend' do + let(:original_severity) { 'silence' } + let(:new_severity) { 'suspend' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend') + end + + it 'undoes individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false) + end + + it 'performs individual suspends' do + expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true) + end + end + end + describe 'DELETE #destroy' do let!(:block) { Fabricate(:domain_block) } From ebf1d74e409ece10864a8615691cd80c434c9055 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 17:47:43 +0100 Subject: [PATCH 117/154] Fix being stuck in edit mode when deleting the edited status (#22126) --- app/javascript/mastodon/reducers/compose.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 9496b56f87..60b0cfb577 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -431,6 +431,8 @@ export default function compose(state = initialState, action) { case TIMELINE_DELETE: if (action.id === state.get('in_reply_to')) { return state.set('in_reply_to', null); + } else if (action.id === state.get('id')) { + return state.set('id', null); } else { return state; } From 2d1294822089a8f1467723bed425eed51dd7db79 Mon Sep 17 00:00:00 2001 From: Brian Campbell Date: Thu, 15 Dec 2022 12:08:40 -0500 Subject: [PATCH 118/154] Fix idempotency when database writes are slow (#21840) There is an idempotency key generated by clients when authoring a post, and stored in Redis, to ensure that if a user or client retries posting the same status, we don't get a duplicate. Hachyderm.io has been experiencing some filesystem and database performance issues, causing database writes to be slow. This can mean that there are successful posts, but the reverse proxy returns 504 Gateway Timeout before the idempotency status has been updated; users or clients who retry (such as Tusky which retries automatically, see tuskyapp/Tusky#2951) can re-try the same post with the same idempotency key before it has actually been recorded in Redis, leading to duplicate posts. To address this issue, move all of the database updates after the initial transaction that creates the status into the `postprocess_status!` method, so we can insert the idempotency key immediately after the status has been created, significantly reducing the window in which the status could be created but the idempotency key not yet stored. Note: this has not yet been tested; I'm submitting this PR for discussion and to offer to the Hachyderm.io admins to try out to fix the multiple posting problem. Co-authored-by: Brian Campbell --- app/services/post_status_service.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index c132930a98..bd3b696329 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -37,12 +37,15 @@ class PostStatusService < BaseService schedule_status! else process_status! - postprocess_status! - bump_potential_friendship! end redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given? + unless scheduled? + postprocess_status! + bump_potential_friendship! + end + @status end @@ -66,9 +69,6 @@ class PostStatusService < BaseService ApplicationRecord.transaction do @status = @account.statuses.create!(status_attributes) end - - process_hashtags_service.call(@status) - process_mentions_service.call(@status) end def schedule_status! @@ -92,6 +92,8 @@ class PostStatusService < BaseService end def postprocess_status! + process_hashtags_service.call(@status) + process_mentions_service.call(@status) Trends.tags.register(@status) LinkCrawlWorker.perform_async(@status.id) DistributionWorker.perform_async(@status.id) From 2644a28cb30dfb57b9543dd045657e8ed660876a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 18:09:48 +0100 Subject: [PATCH 119/154] Change remote media files to be downloaded outside of transactions (#21796) --- app/models/media_attachment.rb | 2 + .../process_status_update_service.rb | 42 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 51b256482a..5916b0b4b3 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -210,6 +210,8 @@ class MediaAttachment < ApplicationRecord default_scope { order(id: :asc) } + attr_accessor :skip_download + def local? remote_url.blank? end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index fad19f87fd..11b38ab92b 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -45,6 +45,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService create_edits! end + download_media_files! queue_poll_notifications! next unless significant_changes? @@ -66,12 +67,12 @@ class ActivityPub::ProcessStatusUpdateService < BaseService def update_media_attachments! previous_media_attachments = @status.media_attachments.to_a previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id) - next_media_attachments = [] + @next_media_attachments = [] as_array(@json['attachment']).each do |attachment| media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment) - next if media_attachment_parser.remote_url.blank? || next_media_attachments.size > 4 + next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > 4 begin media_attachment = previous_media_attachments.find { |previous_media_attachment| previous_media_attachment.remote_url == media_attachment_parser.remote_url } @@ -87,34 +88,39 @@ class ActivityPub::ProcessStatusUpdateService < BaseService media_attachment.focus = media_attachment_parser.focus media_attachment.thumbnail_remote_url = media_attachment_parser.thumbnail_remote_url media_attachment.blurhash = media_attachment_parser.blurhash + media_attachment.status_id = @status.id + media_attachment.skip_download = unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download? media_attachment.save! - next_media_attachments << media_attachment - - next if unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download? - - begin - media_attachment.download_file! if media_attachment.remote_url_previously_changed? - media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed? - media_attachment.save - rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError - RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id) - end + @next_media_attachments << media_attachment rescue Addressable::URI::InvalidURIError => e Rails.logger.debug "Invalid URL in attachment: #{e}" end end - added_media_attachments = next_media_attachments - previous_media_attachments + added_media_attachments = @next_media_attachments - previous_media_attachments - MediaAttachment.where(id: added_media_attachments.map(&:id)).update_all(status_id: @status.id) - - @status.ordered_media_attachment_ids = next_media_attachments.map(&:id) - @status.media_attachments.reload + @status.ordered_media_attachment_ids = @next_media_attachments.map(&:id) @media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids end + def download_media_files! + @next_media_attachments.each do |media_attachment| + next if media_attachment.skip_download + + media_attachment.download_file! if media_attachment.remote_url_previously_changed? + media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed? + media_attachment.save + rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError + RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id) + rescue Seahorse::Client::NetworkingError => e + Rails.logger.warn "Error storing media attachment: #{e}" + end + + @status.media_attachments.reload + end + def update_poll!(allow_significant_changes: true) previous_poll = @status.preloadable_poll @previous_expires_at = previous_poll&.expires_at From 99d26930719bb032860f6dd125116d84b93a3d0c Mon Sep 17 00:00:00 2001 From: Shlee Date: Fri, 16 Dec 2022 03:43:13 +1030 Subject: [PATCH 120/154] Update circleci (#21880) * Update config.yml * Update config.yml --- .circleci/config.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 82d9f9ef63..a373d685e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,8 @@ version: 2.1 orbs: - ruby: circleci/ruby@1.4.1 - node: circleci/node@5.0.1 + ruby: circleci/ruby@2.0.0 + node: circleci/node@5.0.3 executors: default: @@ -19,11 +19,11 @@ executors: DB_USER: root DISABLE_SIMPLECOV: true RAILS_ENV: test - - image: cimg/postgres:14.0 + - image: cimg/postgres:14.5 environment: POSTGRES_USER: root POSTGRES_HOST_AUTH_METHOD: trust - - image: cimg/redis:6.2 + - image: cimg/redis:7.0 commands: install-system-dependencies: @@ -45,7 +45,7 @@ commands: bundle config without 'development production' name: Set bundler settings - ruby/install-deps: - bundler-version: '2.3.8' + bundler-version: '2.3.26' key: ruby<< parameters.ruby-version >>-gems-v1 wait-db: steps: From 7fbc17afa246c5cd384123ace51e07622204b67c Mon Sep 17 00:00:00 2001 From: Bramus! Date: Thu, 15 Dec 2022 18:37:47 +0100 Subject: [PATCH 121/154] Fix media markup (#21420) This brings the markup of the MediaItem component on par with the Item component from media_gallery. Co-authored-by: Effy Elden --- .../mastodon/features/account_gallery/components/media_item.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index f16fe07f1d..13fd7fe03d 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -104,6 +104,7 @@ export default class MediaItem extends ImmutablePureComponent {
+ {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && } +
{!suspended && info} diff --git a/app/javascript/mastodon/features/account/containers/follow_request_note_container.js b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js new file mode 100644 index 0000000000..c33c3de591 --- /dev/null +++ b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import FollowRequestNote from '../components/follow_request_note'; +import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts'; + +const mapDispatchToProps = (dispatch, { account }) => ({ + onAuthorize () { + dispatch(authorizeFollowRequest(account.get('id'))); + }, + + onReject () { + dispatch(rejectFollowRequest(account.get('id'))); + }, +}); + +export default connect(null, mapDispatchToProps)(FollowRequestNote); diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js index 53949258a3..850ece3516 100644 --- a/app/javascript/mastodon/reducers/relationships.js +++ b/app/javascript/mastodon/reducers/relationships.js @@ -1,3 +1,6 @@ +import { + NOTIFICATIONS_UPDATE, +} from '../actions/notifications'; import { ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_FOLLOW_REQUEST, @@ -12,6 +15,8 @@ import { ACCOUNT_PIN_SUCCESS, ACCOUNT_UNPIN_SUCCESS, RELATIONSHIPS_FETCH_SUCCESS, + FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + FOLLOW_REQUEST_REJECT_SUCCESS, } from '../actions/accounts'; import { DOMAIN_BLOCK_SUCCESS, @@ -44,6 +49,12 @@ const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { + case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: + return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false); + case FOLLOW_REQUEST_REJECT_SUCCESS: + return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false); + case NOTIFICATIONS_UPDATE: + return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state; case ACCOUNT_FOLLOW_REQUEST: return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true); case ACCOUNT_FOLLOW_FAIL: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 15fc6aa690..6a22f60095 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -166,6 +166,30 @@ &:disabled { opacity: 0.5; } + + &.button--confirmation { + color: $valid-value-color; + border-color: $valid-value-color; + + &:active, + &:focus, + &:hover { + background: $valid-value-color; + color: $primary-text-color; + } + } + + &.button--destructive { + color: $error-value-color; + border-color: $error-value-color; + + &:active, + &:focus, + &:hover { + background: $error-value-color; + color: $primary-text-color; + } + } } &.button--block { @@ -6722,7 +6746,8 @@ noscript { } } -.moved-account-banner { +.moved-account-banner, +.follow-request-banner { padding: 20px; background: lighten($ui-base-color, 4%); display: flex; @@ -6745,6 +6770,7 @@ noscript { justify-content: space-between; align-items: center; gap: 15px; + width: 100%; } .detailed-status__display-name { @@ -6752,6 +6778,10 @@ noscript { } } +.follow-request-banner .button { + width: 100%; +} + .column-inline-form { padding: 15px; display: flex; diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 15c49f2fec..de8bf338f2 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -44,6 +44,10 @@ module AccountInteractions end end + def requested_by_map(target_account_ids, account_id) + follow_mapping(FollowRequest.where(account_id: target_account_ids, target_account_id: account_id), :account_id) + end + def endorsed_map(target_account_ids, account_id) follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id) end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb index d662380f69..ab8bac4129 100644 --- a/app/presenters/account_relationships_presenter.rb +++ b/app/presenters/account_relationships_presenter.rb @@ -2,7 +2,7 @@ class AccountRelationshipsPresenter attr_reader :following, :followed_by, :blocking, :blocked_by, - :muting, :requested, :domain_blocking, + :muting, :requested, :requested_by, :domain_blocking, :endorsed, :account_note def initialize(account_ids, current_account_id, **options) @@ -15,6 +15,7 @@ class AccountRelationshipsPresenter @blocked_by = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id)) @muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id)) @requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id)) + @requested_by = cached[:requested_by].merge(Account.requested_by_map(@uncached_account_ids, @current_account_id)) @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id)) @endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id)) @account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id)) @@ -27,6 +28,7 @@ class AccountRelationshipsPresenter @blocked_by.merge!(options[:blocked_by_map] || {}) @muting.merge!(options[:muting_map] || {}) @requested.merge!(options[:requested_map] || {}) + @requested_by.merge!(options[:requested_by_map] || {}) @domain_blocking.merge!(options[:domain_blocking_map] || {}) @endorsed.merge!(options[:endorsed_map] || {}) @account_note.merge!(options[:account_note_map] || {}) @@ -44,6 +46,7 @@ class AccountRelationshipsPresenter blocked_by: {}, muting: {}, requested: {}, + requested_by: {}, domain_blocking: {}, endorsed: {}, account_note: {}, @@ -73,6 +76,7 @@ class AccountRelationshipsPresenter blocked_by: { account_id => blocked_by[account_id] }, muting: { account_id => muting[account_id] }, requested: { account_id => requested[account_id] }, + requested_by: { account_id => requested_by[account_id] }, domain_blocking: { account_id => domain_blocking[account_id] }, endorsed: { account_id => endorsed[account_id] }, account_note: { account_id => account_note[account_id] }, diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb index 31fc60eb25..b533874012 100644 --- a/app/serializers/rest/relationship_serializer.rb +++ b/app/serializers/rest/relationship_serializer.rb @@ -2,8 +2,8 @@ class REST::RelationshipSerializer < ActiveModel::Serializer attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by, - :blocking, :blocked_by, :muting, :muting_notifications, :requested, - :domain_blocking, :endorsed, :note + :blocking, :blocked_by, :muting, :muting_notifications, + :requested, :requested_by, :domain_blocking, :endorsed, :note def id object.id.to_s @@ -54,6 +54,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer instance_options[:relationships].requested[object.id] ? true : false end + def requested_by + instance_options[:relationships].requested_by[object.id] ? true : false + end + def domain_blocking instance_options[:relationships].domain_blocking[object.id] || false end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index c9d782cee9..6cd769dc84 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -658,6 +658,12 @@ RSpec.describe Account, type: :model do end end + describe '.requested_by_map' do + it 'returns an hash' do + expect(Account.requested_by_map([], 1)).to be_a Hash + end + end + describe 'MENTION_RE' do subject { Account::MENTION_RE } diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb index edfbbb3549..8a485d2b9a 100644 --- a/spec/presenters/account_relationships_presenter_spec.rb +++ b/spec/presenters/account_relationships_presenter_spec.rb @@ -10,6 +10,7 @@ RSpec.describe AccountRelationshipsPresenter do allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:requested_by_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map) end @@ -71,6 +72,14 @@ RSpec.describe AccountRelationshipsPresenter do end end + context 'options[:requested_by_map] is set' do + let(:options) { { requested_by_map: { 6 => true } } } + + it 'sets @requested merged with default_map and options[:requested_by_map]' do + expect(presenter.requested_by).to eq default_map.merge(options[:requested_by_map]) + end + end + context 'options[:domain_blocking_map] is set' do let(:options) { { domain_blocking_map: { 7 => true } } } From 2c7df002fa633ef69cf44c6212132f43daa40a56 Mon Sep 17 00:00:00 2001 From: Douglas Blank Date: Thu, 15 Dec 2022 09:53:37 -0800 Subject: [PATCH 128/154] Add left and right margin to emojis (#20464) --- app/javascript/styles/mastodon/widgets.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index 0e39dc87b4..7a25d121bb 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -39,6 +39,8 @@ width: 20px; height: 20px; margin: -3px 0 0; + margin-left: 0.075em; + margin-right: 0.075em; } p { From 10370d316ab1f707d1995260d135a485937c0e6d Mon Sep 17 00:00:00 2001 From: zunda Date: Thu, 15 Dec 2022 17:55:29 +0000 Subject: [PATCH 129/154] Remove packages that are provided by Heroku stacks (#19836) --- Aptfile | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Aptfile b/Aptfile index a52eef4e18..8f5bb72a25 100644 --- a/Aptfile +++ b/Aptfile @@ -1,26 +1,4 @@ ffmpeg -libicu[0-9][0-9] -libicu-dev -libidn12 -libidn-dev libpq-dev libxdamage1 libxfixes3 -zlib1g-dev -libcairo2 -libcroco3 -libdatrie1 -libgdk-pixbuf2.0-0 -libgraphite2-3 -libharfbuzz0b -libpango-1.0-0 -libpangocairo-1.0-0 -libpangoft2-1.0-0 -libpixman-1-0 -librsvg2-2 -libthai-data -libthai0 -libvpx[5-9] -libxcb-render0 -libxcb-shm0 -libxrender1 From d13702ac06e2a57d05e5938966ea190f33472fb9 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 18:55:55 +0100 Subject: [PATCH 130/154] Fix status cache hydration discrepancy (#19879) --- app/lib/status_cache_hydrator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 298d7851a6..a84d256947 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -11,7 +11,7 @@ class StatusCacheHydrator # If we're delivering to the author who disabled the display of the application used to create the # status, we need to hydrate the application, since it was not rendered for the basic payload - payload[:application] = ActiveModelSerializers::SerializableResource.new(@status.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json if payload[:application].nil? && @status.account_id == account_id && @status.application.present? + payload[:application] = @status.application.present? ? ActiveModelSerializers::SerializableResource.new(@status.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json : nil if payload[:application].nil? && @status.account_id == account_id # We take advantage of the fact that some relationships can only occur with an original status, not # the reblog that wraps it, so we can assume that some values are always false @@ -23,7 +23,7 @@ class StatusCacheHydrator # If the reblogged status is being delivered to the author who disabled the display of the application # used to create the status, we need to hydrate it here too - payload[:reblog][:application] = ActiveModelSerializers::SerializableResource.new(@status.reblog.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id && @status.reblog.application.present? + payload[:reblog][:application] = @status.reblog.application.present? ? ActiveModelSerializers::SerializableResource.new(@status.reblog.application, serializer: REST::StatusSerializer::ApplicationSerializer).as_json : nil if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id payload[:reblog][:favourited] = Favourite.where(account_id: account_id, status_id: @status.reblog_of_id).exists? payload[:reblog][:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.reblog_of_id).exists? From f847f67410c75036edb2c4b45d0db048af0481c9 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 16 Dec 2022 03:03:44 +0900 Subject: [PATCH 131/154] Add Western Frisian support (#18602) --- config/application.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.rb b/config/application.rb index 83124cfdac..929a44948f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -93,6 +93,7 @@ module Mastodon :fa, :fi, :fr, + :fy, :ga, :gd, :gl, From b70c2e2167d60dd23a4c5011cce3112127a2616c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 20:30:46 +0100 Subject: [PATCH 132/154] Fix issue with glitch-soc theming --- app/views/admin/export_domain_blocks/import.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/admin/export_domain_blocks/import.html.haml b/app/views/admin/export_domain_blocks/import.html.haml index 804e61199e..01add232d1 100644 --- a/app/views/admin/export_domain_blocks/import.html.haml +++ b/app/views/admin/export_domain_blocks/import.html.haml @@ -1,9 +1,6 @@ - content_for :page_title do = t('admin.export_domain_blocks.import.title') -- content_for :header_tags do - = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' - %p= t('admin.export_domain_blocks.import.description_html') - if defined?(@global_private_comment) && @global_private_comment.present? From 1e8aff072a7e7d356ee3a1894c6b74966d8e11af Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 14:07:34 +0100 Subject: [PATCH 133/154] [Glitch] Fix wasteful request to /api/v1/custom_emojis when not logged in Port 1f762f4271685e30373b15a2e204f8edff59d349 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/containers/mastodon.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index 6c92376d81..dd7623a813 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -27,8 +27,9 @@ store.dispatch(hydrateAction); // check for deprecated local settings store.dispatch(checkDeprecatedLocalSettings()); -// load custom emojis -store.dispatch(fetchCustomEmojis()); +if (initialState.meta.me) { + store.dispatch(fetchCustomEmojis()); +} const createIdentityContext = state => ({ signedIn: !!state.meta.me, From 65cc5cb891c46637d897e49731d2e96f4be35731 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 15:43:16 +0100 Subject: [PATCH 134/154] =?UTF-8?q?[Glitch]=20Change=20dropdown=20menu=20t?= =?UTF-8?q?o=20contain=20=E2=80=9CCopy=20link=20to=20post=E2=80=9D=20even?= =?UTF-8?q?=20for=20non-public=20posts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port fe9eab51d140ee0e0343eb07982f0a7ce825398c to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/components/status_action_bar.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 977c98ccbc..7949b2db3b 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -212,11 +212,13 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); + if (publicStatus && isRemote) { + menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); + } + + menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); + if (publicStatus) { - if (isRemote) { - menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); - } - menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } From 8cd7b9555996f03d7fb747f17470c4f5749065dc Mon Sep 17 00:00:00 2001 From: Riedler Date: Thu, 15 Dec 2022 15:57:02 +0100 Subject: [PATCH 135/154] [Glitch] Fix profile avatar being slightly offset into left border Port c3388f4ab151a2603fabd67dadea435f851eaf12 to glitch-soc Co-authored-by: Riedler Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/components/accounts.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index ed1d3d660f..2a955f426d 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -523,7 +523,6 @@ display: block; flex: 0 0 auto; width: 94px; - margin-left: -2px; .account__avatar { background: darken($ui-base-color, 8%); @@ -540,6 +539,7 @@ margin-top: -55px; gap: 8px; overflow: hidden; + margin-left: -2px; // aligns the pfp with content below &__buttons { display: flex; From aad42cfc35447b9fccb401471cbf335185c55a97 Mon Sep 17 00:00:00 2001 From: Alex Stine Date: Thu, 15 Dec 2022 09:20:21 -0600 Subject: [PATCH 136/154] [Glitch] Fix hidden label causing accessibility issue for search inputs Port 903e5a3f459d28b093438dc4827b2fb976aef406 to glitch-soc Signed-off-by: Claire --- .../features/compose/components/search.js | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index 326fe5b707..a1963c15d5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -144,28 +144,20 @@ class Search extends React.PureComponent { return (
- + -
+
From 98f7b3657a202c51a40cc079ad7f04ce96d3a37c Mon Sep 17 00:00:00 2001 From: Pleclown Date: Thu, 15 Dec 2022 16:20:34 +0100 Subject: [PATCH 137/154] [Glitch] Adding 12 hours option for polls Port 3a59ffde8d1600729bf51ff42f1d646062edfc8d to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/compose/components/poll_form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.js b/app/javascript/flavours/glitch/features/compose/components/poll_form.js index d5edccff33..afb5da0977 100644 --- a/app/javascript/flavours/glitch/features/compose/components/poll_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.js @@ -153,6 +153,7 @@ class PollForm extends ImmutablePureComponent { + From 9f3cc9e5551a0c47cff9ebeb6e6398472107afa4 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 16 Dec 2022 00:20:46 +0900 Subject: [PATCH 138/154] [Glitch] `FormattedMessage` must be used directly Port 58200132d07bc0b641c95af614b9ac02ce48c9fc to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/explore/index.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/javascript/flavours/glitch/features/explore/index.js b/app/javascript/flavours/glitch/features/explore/index.js index ba435d7e3d..da0dc7f7c1 100644 --- a/app/javascript/flavours/glitch/features/explore/index.js +++ b/app/javascript/flavours/glitch/features/explore/index.js @@ -24,16 +24,6 @@ const mapStateToProps = state => ({ isSearching: state.getIn(['search', 'submitted']) || !showTrends, }); -// Fix strange bug on Safari where (rendered by FormattedMessage) disappears -// after clicking around Explore top bar (issue #20885). -// Removing width=100% from
also fixes it, as well as replacing with
-// We're choosing to wrap span with div to keep the changes local only to this tool bar. -const WrapFormattedMessage = ({ children, ...props }) =>
{children}
; -WrapFormattedMessage.propTypes = { - children: PropTypes.any, -}; - - export default @connect(mapStateToProps) @injectIntl class Explore extends React.PureComponent { @@ -78,12 +68,22 @@ class Explore extends React.PureComponent { {isSearching ? ( ) : ( - + <>
- - - - {signedIn && } + + + + + + + + + + {signedIn && ( + + + + )}
@@ -97,7 +97,7 @@ class Explore extends React.PureComponent { {intl.formatMessage(messages.title)} -
+ )}
From b22da94a65fcdafe8df13fcb2bf6c88a139785c6 Mon Sep 17 00:00:00 2001 From: Francis Murillo Date: Thu, 15 Dec 2022 15:35:25 +0000 Subject: [PATCH 139/154] [Glitch] Render current day formats in the client timezone Port c50e9d078aa3c353afc140669f1cedcd354ee53e to glitch-soc Co-authored-by: Effy Elden Signed-off-by: Claire --- .../flavours/glitch/packs/public.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index 843fd51632..b256fdbd56 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -42,6 +42,18 @@ function main() { minute: 'numeric', }); + const dateFormat = new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'short', + day: 'numeric', + timeFormat: false, + }); + + const timeFormat = new Intl.DateTimeFormat(locale, { + timeStyle: 'short', + hour12: false, + }); + [].forEach.call(document.querySelectorAll('.emojify'), (content) => { content.innerHTML = emojify(content.innerHTML); }); @@ -54,6 +66,32 @@ function main() { content.textContent = formattedDate; }); + const isToday = date => { + const today = new Date(); + + return date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear(); + }; + const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale); + + [].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => { + const datetime = new Date(content.getAttribute('datetime')); + + let formattedContent; + + if (isToday(datetime)) { + const formattedTime = timeFormat.format(datetime); + + formattedContent = todayFormat.format({ time: formattedTime }); + } else { + formattedContent = dateFormat.format(datetime); + } + + content.title = formattedContent; + content.textContent = formattedContent; + }); + [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { const datetime = new Date(content.getAttribute('datetime')); const now = new Date(); From e76fb9b2c46db07d830e2fa2c0282e5f1f087434 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 15 Dec 2022 08:37:07 -0700 Subject: [PATCH 140/154] [Glitch] Increase the width of the unread notification border. Port 08c0e43b6fca879d7435f63b1b2c718a53c6cacc to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/components/status.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 7cc6f5386b..39184ba7a0 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -1067,7 +1067,7 @@ a.status-card.compact:hover { pointer-events: 0; width: 100%; height: 100%; - border-left: 2px solid $highlight-text-color; + border-left: 4px solid $highlight-text-color; pointer-events: none; } } From 57eab6dbee32be6efca5b93c7e848a108ada9261 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Thu, 15 Dec 2022 10:37:17 -0500 Subject: [PATCH 141/154] [Glitch] Fix typo in handler function call name Port 72a8af80886f270a27bd686f1fa86ef478748963 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/ui/components/columns_area.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index bf3e79c24a..993a507969 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -99,7 +99,7 @@ export default class ColumnsArea extends ImmutablePureComponent { if (this.mediaQuery.removeEventListener) { this.mediaQuery.removeEventListener('change', this.handleLayoutChange); } else { - this.mediaQuery.removeListener(this.handleLayouteChange); + this.mediaQuery.removeListener(this.handleLayoutChange); } } } From 602f18103ccf716667a9736f4665428a9b65c34d Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 16:40:45 +0100 Subject: [PATCH 142/154] [Glitch] Fix the top action bar appearing in multi-column layout Port 38596e49d4ea00b9a538f32fd443afc6aa29824a to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/styles/components/single_column.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index 45d57aedd7..992a42d787 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -281,7 +281,7 @@ } } - .ui__header { + .layout-single-column .ui__header { display: flex; background: $ui-base-color; border-bottom: 1px solid lighten($ui-base-color, 8%); From 7883ba34bb49da19904642be973014b3f26b22f3 Mon Sep 17 00:00:00 2001 From: Meisam <39205857+MFTabriz@users.noreply.github.com> Date: Thu, 15 Dec 2022 17:04:52 +0100 Subject: [PATCH 143/154] [Glitch] set activation for tag follow button Port 059d64a59ea27c40ca6b5b9bf34487169d2a741f to glitch-soc Co-authored-by: meisam Signed-off-by: Claire --- .../flavours/glitch/features/hashtag_timeline/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index 4428fdecf7..219dc0ec60 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -194,7 +194,7 @@ class HashtagTimeline extends React.PureComponent { const following = tag.get('following'); followButton = ( - ); From 0f5ecb3860f0bf5add422b30f5683b578c4205bc Mon Sep 17 00:00:00 2001 From: Mina Her Date: Fri, 16 Dec 2022 01:24:38 +0900 Subject: [PATCH 144/154] [Glitch] Fix margin for search field on medium window size Port cedf1383138b2e58ba6ff9aab46beddeaf1a1354 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/components/search.scss | 1 + .../flavours/glitch/styles/components/single_column.scss | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index 70af0f651b..b8078bdb60 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -1,4 +1,5 @@ .search { + margin-bottom: 10px; position: relative; } diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index 992a42d787..74e5d0884c 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -227,8 +227,7 @@ height: calc(100% - 10px) !important; } - .getting-started__wrapper, - .search { + .getting-started__wrapper { margin-bottom: 10px; } From 89d3d85cf21df93c04e09cb1182884c1534a8f48 Mon Sep 17 00:00:00 2001 From: Terence Eden Date: Thu, 15 Dec 2022 16:38:35 +0000 Subject: [PATCH 145/154] [Glitch] Add transparancy to modal background for accessibility Port ff414a5489fd2c6d154b18effca24dde54d92958 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/modal.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/modal.scss b/app/javascript/flavours/glitch/styles/modal.scss index 6c6de4206c..a333926dd1 100644 --- a/app/javascript/flavours/glitch/styles/modal.scss +++ b/app/javascript/flavours/glitch/styles/modal.scss @@ -1,5 +1,5 @@ .modal-layout { - background: $ui-base-color url('data:image/svg+xml;utf8,') repeat-x bottom fixed; + background: $ui-base-color url('data:image/svg+xml;utf8,') repeat-x bottom fixed; display: flex; flex-direction: column; height: 100vh; From d7a6a9393a8b4ba3e578bf3ecaa08dd34100e8b8 Mon Sep 17 00:00:00 2001 From: Rens Groothuijsen Date: Thu, 15 Dec 2022 17:38:50 +0100 Subject: [PATCH 146/154] [Glitch] Display search popout at fixed screen position Port 726c7dea31d2ee60b327afd327e945e3ece09ac4 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/compose/components/search.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index a1963c15d5..9f90a767d0 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -161,8 +161,7 @@ class Search extends React.PureComponent {
- - +
From cdba1ec5f4edd3e3095c161f34f8d1cc3beebaef Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 17:47:43 +0100 Subject: [PATCH 147/154] [Glitch] Fix being stuck in edit mode when deleting the edited status Port ebf1d74e409ece10864a8615691cd80c434c9055 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/reducers/compose.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index ddc3fa41e3..814b6a1a72 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -538,6 +538,8 @@ export default function compose(state = initialState, action) { case TIMELINE_DELETE: if (action.id === state.get('in_reply_to')) { return state.set('in_reply_to', null); + } else if (action.id === state.get('id')) { + return state.set('id', null); } else { return state; } From 1ced365371bebe667b05eb1cd593f97a1851a335 Mon Sep 17 00:00:00 2001 From: Bramus! Date: Thu, 15 Dec 2022 18:37:47 +0100 Subject: [PATCH 148/154] [Glitch] Fix media markup Port 7fbc17afa246c5cd384123ace51e07622204b67c to glitch-soc Co-authored-by: Effy Elden Signed-off-by: Claire --- .../glitch/features/account_gallery/components/media_item.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index f7a7fd467f..f20ee685e5 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -104,6 +104,7 @@ export default class MediaItem extends ImmutablePureComponent {