From 7ff696d84879b19e3e44c117f6f3a85f7fca07c2 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Sat, 8 Jul 2023 00:12:31 +0200 Subject: [PATCH 001/289] Fix translate button position (#25807) --- app/javascript/mastodon/components/status_content.jsx | 2 +- app/javascript/styles/contrast/diff.scss | 3 ++- app/javascript/styles/mastodon/components.scss | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 688a456319..84a698810f 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -44,7 +44,7 @@ class TranslateButton extends PureComponent { } return ( - ); diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index 4fa1a03616..1c2386f02d 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -15,7 +15,8 @@ .status__content a, .link-footer a, .reply-indicator__content a, -.status__content__read-more-button { +.status__content__read-more-button, +.status__content__translate-button { text-decoration: underline; &:hover, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index e66f8bdfe6..5e261e1ee9 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -981,7 +981,8 @@ body > [data-popper-placement] { max-height: 22px * 15; // 15 lines is roughly above 500 characters } -.status__content__read-more-button { +.status__content__read-more-button, +.status__content__translate-button { display: block; font-size: 15px; line-height: 22px; From 3f04f0c37be8c8ed0f85ea9b5d89c4b8fca9f910 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 8 Jul 2023 05:07:19 -0400 Subject: [PATCH 002/289] Regenerate brakeman ignore, pruning warnings (#25749) --- config/brakeman.ignore | 92 +++++++++++++----------------------------- 1 file changed, 28 insertions(+), 64 deletions(-) diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 80c5f6d4e5..d89591cfe6 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -18,6 +18,9 @@ }, "user_input": "id", "confidence": "Weak", + "cwe_id": [ + 89 + ], "note": "" }, { @@ -38,26 +41,9 @@ }, "user_input": "ids.join(\",\")", "confidence": "Weak", - "note": "" - }, - { - "warning_type": "Redirect", - "warning_code": 18, - "fingerprint": "5fad11cd67f905fab9b1d5739d01384a1748ebe78c5af5ac31518201925265a7", - "check_name": "Redirect", - "message": "Possible unprotected redirect", - "file": "app/controllers/remote_interaction_controller.rb", - "line": 24, - "link": "https://brakemanscanner.org/docs/warning_types/redirect/", - "code": "redirect_to(RemoteFollow.new(resource_params).interact_address_for(Status.find(params[:id])))", - "render_path": null, - "location": { - "type": "method", - "class": "RemoteInteractionController", - "method": "create" - }, - "user_input": "RemoteFollow.new(resource_params).interact_address_for(Status.find(params[:id]))", - "confidence": "High", + "cwe_id": [ + 89 + ], "note": "" }, { @@ -88,6 +74,9 @@ }, "user_input": "(Unresolved Model).new.strike", "confidence": "Weak", + "cwe_id": [ + 79 + ], "note": "" }, { @@ -108,26 +97,9 @@ }, "user_input": "SecureRandom.hex(16)", "confidence": "Medium", - "note": "" - }, - { - "warning_type": "Mass Assignment", - "warning_code": 105, - "fingerprint": "7631e93d0099506e7c3e5c91ba8d88523b00a41a0834ae30031a5a4e8bb3020a", - "check_name": "PermitAttributes", - "message": "Potentially dangerous key allowed for mass assignment", - "file": "app/controllers/api/v2/search_controller.rb", - "line": 28, - "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", - "code": "params.permit(:type, :offset, :min_id, :max_id, :account_id)", - "render_path": null, - "location": { - "type": "method", - "class": "Api::V2::SearchController", - "method": "search_params" - }, - "user_input": ":account_id", - "confidence": "High", + "cwe_id": [ + 89 + ], "note": "" }, { @@ -137,7 +109,7 @@ "check_name": "PermitAttributes", "message": "Potentially dangerous key allowed for mass assignment", "file": "app/controllers/api/v1/admin/reports_controller.rb", - "line": 90, + "line": 88, "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", "code": "params.permit(:resolved, :account_id, :target_account_id)", "render_path": null, @@ -148,6 +120,9 @@ }, "user_input": ":account_id", "confidence": "High", + "cwe_id": [ + 915 + ], "note": "" }, { @@ -157,7 +132,7 @@ "check_name": "PermitAttributes", "message": "Potentially dangerous key allowed for mass assignment", "file": "app/controllers/api/v1/notifications_controller.rb", - "line": 81, + "line": 77, "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", "code": "params.permit(:account_id, :types => ([]), :exclude_types => ([]))", "render_path": null, @@ -168,26 +143,9 @@ }, "user_input": ":account_id", "confidence": "High", - "note": "" - }, - { - "warning_type": "Redirect", - "warning_code": 18, - "fingerprint": "ba568ac09683f98740f663f3d850c31785900215992e8c090497d359a2563d50", - "check_name": "Redirect", - "message": "Possible unprotected redirect", - "file": "app/controllers/remote_follow_controller.rb", - "line": 21, - "link": "https://brakemanscanner.org/docs/warning_types/redirect/", - "code": "redirect_to(RemoteFollow.new(resource_params).subscribe_address_for(@account))", - "render_path": null, - "location": { - "type": "method", - "class": "RemoteFollowController", - "method": "create" - }, - "user_input": "RemoteFollow.new(resource_params).subscribe_address_for(@account)", - "confidence": "High", + "cwe_id": [ + 915 + ], "note": "" }, { @@ -218,6 +176,9 @@ }, "user_input": "(Unresolved Model).new.url", "confidence": "Weak", + "cwe_id": [ + 79 + ], "note": "" }, { @@ -238,9 +199,12 @@ }, "user_input": ":account_id", "confidence": "High", + "cwe_id": [ + 915 + ], "note": "" } ], - "updated": "2022-03-22 07:48:32 +0100", - "brakeman_version": "5.2.1" + "updated": "2023-07-05 14:34:42 -0400", + "brakeman_version": "5.4.1" } From 4106a801a061898aa7fec1e6587310daa5d6382f Mon Sep 17 00:00:00 2001 From: fusagiko / takayamaki <24884114+takayamaki@users.noreply.github.com> Date: Sat, 8 Jul 2023 18:11:22 +0900 Subject: [PATCH 003/289] simplify counters (#25541) --- .../mastodon/components/account.jsx | 4 +- .../mastodon/components/common_counter.jsx | 60 ------------------- .../mastodon/components/counters.tsx | 45 ++++++++++++++ .../features/account/components/header.jsx | 8 +-- 4 files changed, 51 insertions(+), 66 deletions(-) delete mode 100644 app/javascript/mastodon/components/common_counter.jsx create mode 100644 app/javascript/mastodon/components/counters.tsx diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index dd5aff1d8e..fbcd4cfb3d 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -8,7 +8,6 @@ import { Link } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { counterRenderer } from 'mastodon/components/common_counter'; import { EmptyAccount } from 'mastodon/components/empty_account'; import ShortNumber from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; @@ -17,6 +16,7 @@ import { me } from '../initial_state'; import { Avatar } from './avatar'; import Button from './button'; +import { FollowersCounter } from './counters'; import { DisplayName } from './display_name'; import { IconButton } from './icon_button'; import { RelativeTimestamp } from './relative_timestamp'; @@ -160,7 +160,7 @@ class Account extends ImmutablePureComponent { {!minimal && (
- {verification} {muteTimeRemaining} + {verification} {muteTimeRemaining}
)} diff --git a/app/javascript/mastodon/components/common_counter.jsx b/app/javascript/mastodon/components/common_counter.jsx deleted file mode 100644 index 23e1f22638..0000000000 --- a/app/javascript/mastodon/components/common_counter.jsx +++ /dev/null @@ -1,60 +0,0 @@ -// @ts-check -import { FormattedMessage } from 'react-intl'; - -/** - * Returns custom renderer for one of the common counter types - * @param {"statuses" | "following" | "followers"} counterType - * Type of the counter - * @param {boolean} isBold Whether display number must be displayed in bold - * @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} - * Renderer function - * @throws If counterType is not covered by this function - */ -export function counterRenderer(counterType, isBold = true) { - /** - * @type {(displayNumber: JSX.Element) => JSX.Element} - */ - const renderCounter = isBold - ? (displayNumber) => {displayNumber} - : (displayNumber) => displayNumber; - - switch (counterType) { - case 'statuses': { - return (displayNumber, pluralReady) => ( - - ); - } - case 'following': { - return (displayNumber, pluralReady) => ( - - ); - } - case 'followers': { - return (displayNumber, pluralReady) => ( - - ); - } - default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`); - } -} diff --git a/app/javascript/mastodon/components/counters.tsx b/app/javascript/mastodon/components/counters.tsx new file mode 100644 index 0000000000..e0c818f247 --- /dev/null +++ b/app/javascript/mastodon/components/counters.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +export const StatusesCounter = ( + displayNumber: React.ReactNode, + pluralReady: number +) => ( + {displayNumber}, + }} + /> +); + +export const FollowingCounter = ( + displayNumber: React.ReactNode, + pluralReady: number +) => ( + {displayNumber}, + }} + /> +); + +export const FollowersCounter = ( + displayNumber: React.ReactNode, + pluralReady: number +) => ( + {displayNumber}, + }} + /> +); diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index b718e860d0..f7ebc34bca 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -11,7 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { Avatar } from 'mastodon/components/avatar'; import Button from 'mastodon/components/button'; -import { counterRenderer } from 'mastodon/components/common_counter'; +import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters'; import { Icon } from 'mastodon/components/icon'; import { IconButton } from 'mastodon/components/icon_button'; import ShortNumber from 'mastodon/components/short_number'; @@ -451,21 +451,21 @@ class Header extends ImmutablePureComponent { From b5c5e2bf3625cc337e555103c6286ec791d81e70 Mon Sep 17 00:00:00 2001 From: alfe Date: Sat, 8 Jul 2023 18:11:58 +0900 Subject: [PATCH 004/289] Rewrite `` as FC and TS (#25492) --- .../mastodon/components/account.jsx | 2 +- .../mastodon/components/animated_number.tsx | 2 +- .../components/autosuggest_hashtag.tsx | 2 +- .../mastodon/components/hashtag.jsx | 2 +- .../mastodon/components/server_banner.jsx | 2 +- .../mastodon/components/short_number.jsx | 115 ------------------ .../mastodon/components/short_number.tsx | 90 ++++++++++++++ .../features/account/components/header.jsx | 2 +- .../directory/components/account_card.jsx | 2 +- .../features/explore/components/story.jsx | 2 +- 10 files changed, 98 insertions(+), 123 deletions(-) delete mode 100644 app/javascript/mastodon/components/short_number.jsx create mode 100644 app/javascript/mastodon/components/short_number.tsx diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index fbcd4cfb3d..fd5ea60407 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -9,7 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { EmptyAccount } from 'mastodon/components/empty_account'; -import ShortNumber from 'mastodon/components/short_number'; +import { ShortNumber } from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { me } from '../initial_state'; diff --git a/app/javascript/mastodon/components/animated_number.tsx b/app/javascript/mastodon/components/animated_number.tsx index ad985a29ea..3122d6421e 100644 --- a/app/javascript/mastodon/components/animated_number.tsx +++ b/app/javascript/mastodon/components/animated_number.tsx @@ -4,7 +4,7 @@ import { TransitionMotion, spring } from 'react-motion'; import { reduceMotion } from '../initial_state'; -import ShortNumber from './short_number'; +import { ShortNumber } from './short_number'; const obfuscatedCount = (count: number) => { if (count < 0) { diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.tsx b/app/javascript/mastodon/components/autosuggest_hashtag.tsx index c6798054db..59d66ec878 100644 --- a/app/javascript/mastodon/components/autosuggest_hashtag.tsx +++ b/app/javascript/mastodon/components/autosuggest_hashtag.tsx @@ -1,6 +1,6 @@ import { FormattedMessage } from 'react-intl'; -import ShortNumber from 'mastodon/components/short_number'; +import { ShortNumber } from 'mastodon/components/short_number'; interface Props { tag: { diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx index 4a7b9ef719..14bb4ddc64 100644 --- a/app/javascript/mastodon/components/hashtag.jsx +++ b/app/javascript/mastodon/components/hashtag.jsx @@ -11,7 +11,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import ShortNumber from 'mastodon/components/short_number'; +import { ShortNumber } from 'mastodon/components/short_number'; import { Skeleton } from 'mastodon/components/skeleton'; class SilentErrorBoundary extends Component { diff --git a/app/javascript/mastodon/components/server_banner.jsx b/app/javascript/mastodon/components/server_banner.jsx index 9982378601..63eec53492 100644 --- a/app/javascript/mastodon/components/server_banner.jsx +++ b/app/javascript/mastodon/components/server_banner.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { fetchServer } from 'mastodon/actions/server'; import { ServerHeroImage } from 'mastodon/components/server_hero_image'; -import ShortNumber from 'mastodon/components/short_number'; +import { ShortNumber } from 'mastodon/components/short_number'; import { Skeleton } from 'mastodon/components/skeleton'; import Account from 'mastodon/containers/account_container'; import { domain } from 'mastodon/initial_state'; diff --git a/app/javascript/mastodon/components/short_number.jsx b/app/javascript/mastodon/components/short_number.jsx deleted file mode 100644 index b7ac4f5fd5..0000000000 --- a/app/javascript/mastodon/components/short_number.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import PropTypes from 'prop-types'; -import { memo } from 'react'; - -import { FormattedMessage, FormattedNumber } from 'react-intl'; - -import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; - -// @ts-check - -/** - * @callback ShortNumberRenderer - * @param {JSX.Element} displayNumber Number to display - * @param {number} pluralReady Number used for pluralization - * @returns {JSX.Element} Final render of number - */ - -/** - * @typedef {object} ShortNumberProps - * @property {number} value Number to display in short variant - * @property {ShortNumberRenderer} [renderer] - * Custom renderer for numbers, provided as a prop. If another renderer - * passed as a child of this component, this prop won't be used. - * @property {ShortNumberRenderer} [children] - * Custom renderer for numbers, provided as a child. If another renderer - * passed as a prop of this component, this one will be used instead. - */ - -/** - * Component that renders short big number to a shorter version - * @param {ShortNumberProps} param0 Props for the component - * @returns {JSX.Element} Rendered number - */ -function ShortNumber({ value, renderer, children }) { - const shortNumber = toShortNumber(value); - const [, division] = shortNumber; - - if (children != null && renderer != null) { - console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'); - } - - const customRenderer = children != null ? children : renderer; - - const displayNumber = ; - - return customRenderer != null - ? customRenderer(displayNumber, pluralReady(value, division)) - : displayNumber; -} - -ShortNumber.propTypes = { - value: PropTypes.number.isRequired, - renderer: PropTypes.func, - children: PropTypes.func, -}; - -/** - * @typedef {object} ShortNumberCounterProps - * @property {import('../utils/number').ShortNumber} value Short number - */ - -/** - * Renders short number into corresponding localizable react fragment - * @param {ShortNumberCounterProps} param0 Props for the component - * @returns {JSX.Element} FormattedMessage ready to be embedded in code - */ -function ShortNumberCounter({ value }) { - const [rawNumber, unit, maxFractionDigits = 0] = value; - - const count = ( - - ); - - let values = { count, rawNumber }; - - switch (unit) { - case DECIMAL_UNITS.THOUSAND: { - return ( - - ); - } - case DECIMAL_UNITS.MILLION: { - return ( - - ); - } - case DECIMAL_UNITS.BILLION: { - return ( - - ); - } - // Not sure if we should go farther - @Sasha-Sorokin - default: return count; - } -} - -ShortNumberCounter.propTypes = { - value: PropTypes.arrayOf(PropTypes.number), -}; - -export default memo(ShortNumber); diff --git a/app/javascript/mastodon/components/short_number.tsx b/app/javascript/mastodon/components/short_number.tsx new file mode 100644 index 0000000000..010586c04f --- /dev/null +++ b/app/javascript/mastodon/components/short_number.tsx @@ -0,0 +1,90 @@ +import { memo } from 'react'; + +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; + +type ShortNumberRenderer = ( + displayNumber: JSX.Element, + pluralReady: number +) => JSX.Element; + +interface ShortNumberProps { + value: number; + renderer?: ShortNumberRenderer; + children?: ShortNumberRenderer; +} + +export const ShortNumberRenderer: React.FC = ({ + value, + renderer, + children, +}) => { + const shortNumber = toShortNumber(value); + const [, division] = shortNumber; + + if (children && renderer) { + console.warn( + 'Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.' + ); + } + + const customRenderer = children || renderer || null; + + const displayNumber = ; + + return ( + customRenderer?.(displayNumber, pluralReady(value, division)) || + displayNumber + ); +}; +export const ShortNumber = memo(ShortNumberRenderer); + +interface ShortNumberCounterProps { + value: number[]; +} +const ShortNumberCounter: React.FC = ({ value }) => { + const [rawNumber, unit, maxFractionDigits = 0] = value; + + const count = ( + + ); + + const values = { count, rawNumber }; + + switch (unit) { + case DECIMAL_UNITS.THOUSAND: { + return ( + + ); + } + case DECIMAL_UNITS.MILLION: { + return ( + + ); + } + case DECIMAL_UNITS.BILLION: { + return ( + + ); + } + // Not sure if we should go farther - @Sasha-Sorokin + default: + return count; + } +}; diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index f7ebc34bca..5e30205b0a 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -14,7 +14,7 @@ import Button from 'mastodon/components/button'; import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters'; import { Icon } from 'mastodon/components/icon'; import { IconButton } from 'mastodon/components/icon_button'; -import ShortNumber from 'mastodon/components/short_number'; +import { ShortNumber } from 'mastodon/components/short_number'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import { autoPlayGif, me, domain } from 'mastodon/initial_state'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; diff --git a/app/javascript/mastodon/features/directory/components/account_card.jsx b/app/javascript/mastodon/features/directory/components/account_card.jsx index cf1c63f9e4..7959795303 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.jsx +++ b/app/javascript/mastodon/features/directory/components/account_card.jsx @@ -19,7 +19,7 @@ import { openModal } from 'mastodon/actions/modal'; import { Avatar } from 'mastodon/components/avatar'; import Button from 'mastodon/components/button'; import { DisplayName } from 'mastodon/components/display_name'; -import ShortNumber from 'mastodon/components/short_number'; +import { ShortNumber } from 'mastodon/components/short_number'; import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state'; import { makeGetAccount } from 'mastodon/selectors'; diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx index 0a9fbb1905..73ec99c14b 100644 --- a/app/javascript/mastodon/features/explore/components/story.jsx +++ b/app/javascript/mastodon/features/explore/components/story.jsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; import { Blurhash } from 'mastodon/components/blurhash'; import { accountsCountRenderer } from 'mastodon/components/hashtag'; -import ShortNumber from 'mastodon/components/short_number'; +import { ShortNumber } from 'mastodon/components/short_number'; import { Skeleton } from 'mastodon/components/skeleton'; export default class Story extends PureComponent { From 9203ab410c09efbd9facf202f2570cdd0def8de0 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Sat, 8 Jul 2023 11:12:20 +0200 Subject: [PATCH 005/289] Convert `` to Typescript (#25582) --- .../components/dismissable_banner.jsx | 55 ------------------- .../components/dismissable_banner.tsx | 47 ++++++++++++++++ .../features/community_timeline/index.jsx | 2 +- .../mastodon/features/explore/links.jsx | 2 +- .../mastodon/features/explore/statuses.jsx | 2 +- .../mastodon/features/explore/tags.jsx | 2 +- .../mastodon/features/firehose/index.jsx | 2 +- .../components/explore_prompt.jsx | 2 +- .../features/public_timeline/index.jsx | 2 +- 9 files changed, 54 insertions(+), 62 deletions(-) delete mode 100644 app/javascript/mastodon/components/dismissable_banner.jsx create mode 100644 app/javascript/mastodon/components/dismissable_banner.tsx diff --git a/app/javascript/mastodon/components/dismissable_banner.jsx b/app/javascript/mastodon/components/dismissable_banner.jsx deleted file mode 100644 index 5aecc88b16..0000000000 --- a/app/javascript/mastodon/components/dismissable_banner.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { injectIntl, defineMessages } from 'react-intl'; - -import { bannerSettings } from 'mastodon/settings'; - -import { IconButton } from './icon_button'; - -const messages = defineMessages({ - dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' }, -}); - -class DismissableBanner extends PureComponent { - - static propTypes = { - id: PropTypes.string.isRequired, - children: PropTypes.node, - intl: PropTypes.object.isRequired, - }; - - state = { - visible: !bannerSettings.get(this.props.id), - }; - - handleDismiss = () => { - const { id } = this.props; - this.setState({ visible: false }, () => bannerSettings.set(id, true)); - }; - - render () { - const { visible } = this.state; - - if (!visible) { - return null; - } - - const { children, intl } = this.props; - - return ( -
-
- {children} -
- -
- -
-
- ); - } - -} - -export default injectIntl(DismissableBanner); diff --git a/app/javascript/mastodon/components/dismissable_banner.tsx b/app/javascript/mastodon/components/dismissable_banner.tsx new file mode 100644 index 0000000000..d5cdb07503 --- /dev/null +++ b/app/javascript/mastodon/components/dismissable_banner.tsx @@ -0,0 +1,47 @@ +import type { PropsWithChildren } from 'react'; +import { useCallback, useState } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { bannerSettings } from 'mastodon/settings'; + +import { IconButton } from './icon_button'; + +const messages = defineMessages({ + dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' }, +}); + +interface Props { + id: string; +} + +export const DismissableBanner: React.FC> = ({ + id, + children, +}) => { + const [visible, setVisible] = useState(!bannerSettings.get(id)); + const intl = useIntl(); + + const handleDismiss = useCallback(() => { + setVisible(false); + bannerSettings.set(id, true); + }, [id]); + + if (!visible) { + return null; + } + + return ( +
+
{children}
+ +
+ +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/community_timeline/index.jsx b/app/javascript/mastodon/features/community_timeline/index.jsx index 7e3b9babe9..2d94cabed2 100644 --- a/app/javascript/mastodon/features/community_timeline/index.jsx +++ b/app/javascript/mastodon/features/community_timeline/index.jsx @@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; -import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { DismissableBanner } from 'mastodon/components/dismissable_banner'; import { domain } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; diff --git a/app/javascript/mastodon/features/explore/links.jsx b/app/javascript/mastodon/features/explore/links.jsx index 49c667f027..8b199bf47c 100644 --- a/app/javascript/mastodon/features/explore/links.jsx +++ b/app/javascript/mastodon/features/explore/links.jsx @@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { fetchTrendingLinks } from 'mastodon/actions/trends'; -import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { DismissableBanner } from 'mastodon/components/dismissable_banner'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import Story from './components/story'; diff --git a/app/javascript/mastodon/features/explore/statuses.jsx b/app/javascript/mastodon/features/explore/statuses.jsx index eb2fe777a6..3271929db5 100644 --- a/app/javascript/mastodon/features/explore/statuses.jsx +++ b/app/javascript/mastodon/features/explore/statuses.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { debounce } from 'lodash'; import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends'; -import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { DismissableBanner } from 'mastodon/components/dismissable_banner'; import StatusList from 'mastodon/components/status_list'; import { getStatusList } from 'mastodon/selectors'; diff --git a/app/javascript/mastodon/features/explore/tags.jsx b/app/javascript/mastodon/features/explore/tags.jsx index f558b48a60..1a4d259690 100644 --- a/app/javascript/mastodon/features/explore/tags.jsx +++ b/app/javascript/mastodon/features/explore/tags.jsx @@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { fetchTrendingHashtags } from 'mastodon/actions/trends'; -import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { DismissableBanner } from 'mastodon/components/dismissable_banner'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; diff --git a/app/javascript/mastodon/features/firehose/index.jsx b/app/javascript/mastodon/features/firehose/index.jsx index 9ba4fd5b2b..e5b47d3fe0 100644 --- a/app/javascript/mastodon/features/firehose/index.jsx +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -10,7 +10,7 @@ import { addColumn } from 'mastodon/actions/columns'; import { changeSetting } from 'mastodon/actions/settings'; import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming'; import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines'; -import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { DismissableBanner } from 'mastodon/components/dismissable_banner'; import initialState, { domain } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; diff --git a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx index a6993c6418..2af85b6d54 100644 --- a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx +++ b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import background from 'mastodon/../images/friends-cropped.png'; -import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { DismissableBanner } from 'mastodon/components/dismissable_banner'; export const ExplorePrompt = () => ( diff --git a/app/javascript/mastodon/features/public_timeline/index.jsx b/app/javascript/mastodon/features/public_timeline/index.jsx index 352baa8336..3bfb25ba73 100644 --- a/app/javascript/mastodon/features/public_timeline/index.jsx +++ b/app/javascript/mastodon/features/public_timeline/index.jsx @@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; -import DismissableBanner from 'mastodon/components/dismissable_banner'; +import { DismissableBanner } from 'mastodon/components/dismissable_banner'; import { domain } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; From a27f2686ca1738aec06100847dd55fb550148ad9 Mon Sep 17 00:00:00 2001 From: Kurtis Rainbolt-Greene Date: Sat, 8 Jul 2023 10:45:36 -0700 Subject: [PATCH 006/289] First pass at multi-database for read replica using Rails native adapter (#25693) Co-authored-by: emilweth <7402764+emilweth@users.noreply.github.com> --- .rubocop_todo.yml | 1 - Gemfile | 1 - Gemfile.lock | 3 --- .../api/v1/timelines/home_controller.rb | 7 ++++-- app/workers/feed_insert_worker.rb | 24 +++++++++++-------- config/database.yml | 24 +++++++++++++------ config/initializers/makara.rb | 2 -- 7 files changed, 36 insertions(+), 26 deletions(-) delete mode 100644 config/initializers/makara.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 975c9d28fb..24f02d4d3b 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -809,7 +809,6 @@ Style/FrozenStringLiteralComment: - 'config/initializers/httplog.rb' - 'config/initializers/inflections.rb' - 'config/initializers/mail_delivery_job.rb' - - 'config/initializers/makara.rb' - 'config/initializers/mime_types.rb' - 'config/initializers/oj.rb' - 'config/initializers/omniauth.rb' diff --git a/Gemfile b/Gemfile index 3feb3f9548..24cb43e657 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,6 @@ gem 'rack', '~> 2.2.7' gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' -gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' diff --git a/Gemfile.lock b/Gemfile.lock index 985e36c20c..9bd708d61e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -399,8 +399,6 @@ GEM net-imap net-pop net-smtp - makara (0.5.1) - activerecord (>= 5.2.0) marcel (1.0.2) mario-redis-lock (1.2.1) redis (>= 3.0.5) @@ -815,7 +813,6 @@ DEPENDENCIES letter_opener_web (~> 2.0) link_header (~> 0.0) lograge (~> 0.12) - makara (~> 0.5) mario-redis-lock (~> 1.2) memory_profiler mime-types (~> 3.4.1) diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index ae6dbcb8b3..0ee28ef042 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -6,11 +6,14 @@ class Api::V1::Timelines::HomeController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show - @statuses = load_statuses + ApplicationRecord.connected_to(role: :read, prevent_writes: true) do + @statuses = load_statuses + @relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + end render json: @statuses, each_serializer: REST::StatusSerializer, - relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), + relationships: @relationships, status: account_home_feed.regenerating? ? 206 : 200 end diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb index 758cebd4ba..47826c2111 100644 --- a/app/workers/feed_insert_worker.rb +++ b/app/workers/feed_insert_worker.rb @@ -4,19 +4,23 @@ class FeedInsertWorker include Sidekiq::Worker def perform(status_id, id, type = 'home', options = {}) - @type = type.to_sym - @status = Status.find(status_id) - @options = options.symbolize_keys + ApplicationRecord.connected_to(role: :primary) do + @type = type.to_sym + @status = Status.find(status_id) + @options = options.symbolize_keys - case @type - when :home, :tags - @follower = Account.find(id) - when :list - @list = List.find(id) - @follower = @list.account + case @type + when :home, :tags + @follower = Account.find(id) + when :list + @list = List.find(id) + @follower = @list.account + end end - check_and_insert + ApplicationRecord.connected_to(role: :read, prevent_writes: true) do + check_and_insert + end rescue ActiveRecord::RecordNotFound true end diff --git a/config/database.yml b/config/database.yml index 34acf2f19a..f7ecbd9814 100644 --- a/config/database.yml +++ b/config/database.yml @@ -27,10 +27,20 @@ test: port: <%= ENV['DB_PORT'] %> production: - <<: *default - database: <%= ENV['DB_NAME'] || 'mastodon_production' %> - username: <%= ENV['DB_USER'] || 'mastodon' %> - password: <%= (ENV['DB_PASS'] || '').to_json %> - host: <%= ENV['DB_HOST'] || 'localhost' %> - port: <%= ENV['DB_PORT'] || 5432 %> - prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %> + primary: + <<: *default + database: <%= ENV['DB_NAME'] || 'mastodon_production' %> + username: <%= ENV['DB_USER'] || 'mastodon' %> + password: <%= (ENV['DB_PASS'] || '').to_json %> + host: <%= ENV['DB_HOST'] || 'localhost' %> + port: <%= ENV['DB_PORT'] || 5432 %> + prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %> + read: + <<: *default + database: <%= ENV['DB_REPLICA_NAME'] ||ENV['DB_NAME'] || 'mastodon_production' %> + username: <%= ENV['DB_REPLICA_USER'] ||ENV['DB_USER'] || 'mastodon' %> + password: <%= (ENV['DB_REPLICA_PASS'] || ENV['DB_PASS'] || '').to_json %> + host: <%= ENV['DB_REPLICA_HOST'] ||ENV['DB_HOST'] || 'localhost' %> + port: <%= ENV['DB_REPLICA_PORT'] ||ENV['DB_PORT'] || 5432 %> + prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %> + replica: true diff --git a/config/initializers/makara.rb b/config/initializers/makara.rb deleted file mode 100644 index dc88fa63cd..0000000000 --- a/config/initializers/makara.rb +++ /dev/null @@ -1,2 +0,0 @@ -Makara::Cookie::DEFAULT_OPTIONS[:same_site] = :lax -Makara::Cookie::DEFAULT_OPTIONS[:secure] = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true' From 6894f5c1144a45815ac715f3e14e651cd0259995 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2023 20:00:02 +0200 Subject: [PATCH 007/289] Add forwarding of reported replies to servers being replied to (#25341) --- app/lib/activitypub/activity/flag.rb | 9 +++--- app/services/report_service.rb | 14 +++++---- spec/services/report_service_spec.rb | 43 +++++++++++++++++++++------- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb index dc1932f597..304cf0ad2f 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -4,13 +4,14 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity def perform return if skip_reports? - target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) }.select(&:local?) - target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.select(&:local?).group_by(&:account_id) + target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) } + target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.group_by(&:account_id) target_accounts.each do |target_account| - target_statuses = target_statuses_by_account[target_account.id] + target_statuses = target_statuses_by_account[target_account.id] + replied_to_accounts = Account.local.where(id: target_statuses.filter_map(&:in_reply_to_account_id)) - next if target_account.suspended? + next if target_account.suspended? || (!target_account.local? && replied_to_accounts.none?) ReportService.new.call( @account, diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 0ce525b071..3444e1dfaf 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -45,11 +45,15 @@ class ReportService < BaseService end def forward_to_origin! - ActivityPub::DeliveryWorker.perform_async( - payload, - some_local_account.id, - @target_account.inbox_url - ) + # Send report to the server where the account originates from + ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, @target_account.inbox_url) + + # Send report to servers to which the account was replying to, so they also have a chance to act + inbox_urls = Account.remote.where(id: Status.where(id: reported_status_ids).where.not(in_reply_to_account_id: nil).select(:in_reply_to_account_id)).inboxes - [@target_account.inbox_url] + + inbox_urls.each do |inbox_url| + ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) + end end def forward? diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb index b8ceedb851..660ce3db23 100644 --- a/spec/services/report_service_spec.rb +++ b/spec/services/report_service_spec.rb @@ -17,24 +17,45 @@ RSpec.describe ReportService, type: :service do context 'with a remote account' do let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } + let(:forward) { false } before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) end - it 'sends ActivityPub payload when forward is true' do - subject.call(source_account, remote_account, forward: true) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made + context 'when forward is true' do + let(:forward) { true } + + it 'sends ActivityPub payload when forward is true' do + subject.call(source_account, remote_account, forward: forward) + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made + end + + it 'has an uri' do + report = subject.call(source_account, remote_account, forward: forward) + expect(report.uri).to_not be_nil + end + + context 'when reporting a reply' do + let(:remote_thread_account) { Fabricate(:account, domain: 'foo.com', protocol: :activitypub, inbox_url: 'http://foo.com/inbox') } + let(:reported_status) { Fabricate(:status, account: remote_account, thread: Fabricate(:status, account: remote_thread_account)) } + + before do + stub_request(:post, 'http://foo.com/inbox').to_return(status: 200) + end + + it 'sends ActivityPub payload to the author of the replied-to post' do + subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward) + expect(a_request(:post, 'http://foo.com/inbox')).to have_been_made + end + end end - it 'does not send anything when forward is false' do - subject.call(source_account, remote_account, forward: false) - expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made - end - - it 'has an uri' do - report = subject.call(source_account, remote_account, forward: true) - expect(report.uri).to_not be_nil + context 'when forward is false' do + it 'does not send anything' do + subject.call(source_account, remote_account, forward: forward) + expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made + end end end From 9f5eab728b2d0a530e40f079fb174a76f1aed053 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2023 20:00:12 +0200 Subject: [PATCH 008/289] Fix explore page being inaccessible when opted-out of trends in web UI (#25716) --- app/javascript/mastodon/features/explore/index.jsx | 4 ++-- .../mastodon/features/ui/components/navigation_panel.jsx | 4 ++-- app/javascript/mastodon/features/ui/index.jsx | 4 ++-- app/javascript/mastodon/initial_state.js | 6 ++++-- app/serializers/initial_state_serializer.rb | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/javascript/mastodon/features/explore/index.jsx b/app/javascript/mastodon/features/explore/index.jsx index 185db0732a..1a66adc87c 100644 --- a/app/javascript/mastodon/features/explore/index.jsx +++ b/app/javascript/mastodon/features/explore/index.jsx @@ -11,7 +11,7 @@ import { connect } from 'react-redux'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; import Search from 'mastodon/features/compose/containers/search_container'; -import { showTrends } from 'mastodon/initial_state'; +import { trendsEnabled } from 'mastodon/initial_state'; import Links from './links'; import SearchResults from './results'; @@ -26,7 +26,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ layout: state.getIn(['meta', 'layout']), - isSearching: state.getIn(['search', 'submitted']) || !showTrends, + isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled, }); class Explore extends PureComponent { diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index d5e98461aa..dc406fa55c 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; import { WordmarkLogo } from 'mastodon/components/logo'; import NavigationPortal from 'mastodon/components/navigation_portal'; -import { timelinePreview, showTrends } from 'mastodon/initial_state'; +import { timelinePreview, trendsEnabled } from 'mastodon/initial_state'; import ColumnLink from './column_link'; import DisabledAccountBanner from './disabled_account_banner'; @@ -65,7 +65,7 @@ class NavigationPanel extends Component { )} - {showTrends ? ( + {trendsEnabled ? ( ) : ( diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 59327f0496..b38acfc14d 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -22,7 +22,7 @@ import { clearHeight } from '../../actions/height_cache'; import { expandNotifications } from '../../actions/notifications'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state'; +import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import Header from './components/header'; @@ -170,7 +170,7 @@ class SwitchingColumnsArea extends PureComponent { } } else if (singleUserMode && owner && initialState?.accounts[owner]) { redirect = ; - } else if (showTrends && trendsAsLanding) { + } else if (trendsEnabled && trendsAsLanding) { redirect = ; } else { redirect = ; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 1f0f9d5b13..5ad61e1f6b 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -69,12 +69,13 @@ * @property {boolean} reduce_motion * @property {string} repository * @property {boolean} search_enabled + * @property {boolean} trends_enabled * @property {boolean} single_user_mode * @property {string} source_url * @property {string} streaming_api_base_url * @property {boolean} timeline_preview * @property {string} title - * @property {boolean} trends + * @property {boolean} show_trends * @property {boolean} trends_as_landing_page * @property {boolean} unfollow_modal * @property {boolean} use_blurhash @@ -121,7 +122,8 @@ export const reduceMotion = getMeta('reduce_motion'); export const registrationsOpen = getMeta('registrations_open'); export const repository = getMeta('repository'); export const searchEnabled = getMeta('search_enabled'); -export const showTrends = getMeta('trends'); +export const trendsEnabled = getMeta('trends_enabled'); +export const showTrends = getMeta('show_trends'); export const singleUserMode = getMeta('single_user_mode'); export const source_url = getMeta('source_url'); export const timelinePreview = getMeta('timeline_preview'); diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 769ba653ed..7676942a74 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -25,7 +25,7 @@ class InitialStateSerializer < ActiveModel::Serializer limited_federation_mode: Rails.configuration.x.whitelist_mode, mascot: instance_presenter.mascot&.file&.url, profile_directory: Setting.profile_directory, - trends: Setting.trends, + trends_enabled: Setting.trends, registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode, timeline_preview: Setting.timeline_preview, activity_api_enabled: Setting.activity_api_enabled, @@ -47,7 +47,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:advanced_layout] = object.current_account.user.setting_advanced_layout store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:use_pending_items] = object.current_account.user.setting_use_pending_items - store[:trends] = Setting.trends && object.current_account.user.setting_trends + store[:show_trends] = Setting.trends && object.current_account.user.setting_trends store[:crop_images] = object.current_account.user.setting_crop_images else store[:auto_play_gif] = Setting.auto_play_gif From 3df957104ae805675e4c2576d01d79a2a2b701ba Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2023 20:00:52 +0200 Subject: [PATCH 009/289] Fix dropdowns being disabled for logged out users in web UI (#25714) --- .../mastodon/components/status_action_bar.jsx | 114 +++++++++--------- .../features/account/components/header.jsx | 1 - .../features/status/components/action_bar.jsx | 109 +++++++++-------- 3 files changed, 113 insertions(+), 111 deletions(-) diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 8b3c20f824..ab9dac27e5 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -237,7 +237,6 @@ class StatusActionBar extends ImmutablePureComponent { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.context.identity; - const anonymousAccess = !signedIn; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); @@ -263,71 +262,73 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } - menu.push(null); - - menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); - - if (writtenByMe && pinnableStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } - - menu.push(null); - - if (writtenByMe || withDismiss) { - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - } - - if (writtenByMe) { - menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true }); - menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); + if (signedIn) { menu.push(null); - if (relationship && relationship.get('muting')) { - menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); + menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); + + if (writtenByMe && pinnableStatus) { + menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + } + + menu.push(null); + + if (writtenByMe || withDismiss) { + menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); + menu.push(null); + } + + if (writtenByMe) { + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true }); } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true }); - } - - if (relationship && relationship.get('blocking')) { - menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); - } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true }); - } - - if (!this.props.onFilter) { - menu.push(null); - menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true }); - menu.push(null); - } - - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true }); - - if (account.get('acct') !== account.get('username')) { - const domain = account.get('acct').split('@')[1]; - + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); menu.push(null); - if (relationship && relationship.get('domain_blocking')) { - menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); + if (relationship && relationship.get('muting')) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true }); } - } - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { - menu.push(null); - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { - menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); - menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + if (relationship && relationship.get('blocking')) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true }); } - if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + + if (!this.props.onFilter) { + menu.push(null); + menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true }); + menu.push(null); + } + + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true }); + + if (account.get('acct') !== account.get('username')) { const domain = account.get('acct').split('@')[1]; - menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); + + menu.push(null); + + if (relationship && relationship.get('domain_blocking')) { + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); + } else { + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true }); + } + } + + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { + menu.push(null); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); + menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + } + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + const domain = account.get('acct').split('@')[1]; + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); + } } } } @@ -371,7 +372,6 @@ class StatusActionBar extends ImmutablePureComponent {
- +
); From 703ff7554976a1c87a73bf1257f732aa5987d14e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2023 20:01:08 +0200 Subject: [PATCH 010/289] Add toast with option to open post after publishing in web UI (#25564) --- app/javascript/mastodon/actions/alerts.js | 64 +++++++++---------- app/javascript/mastodon/actions/compose.js | 18 ++++-- .../containers/column_settings_container.js | 4 +- .../ui/containers/notifications_container.js | 37 +++++------ app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/reducers/alerts.js | 19 +++--- app/javascript/mastodon/selectors/index.js | 28 +++----- .../styles/mastodon/components.scss | 59 +++++++++++++++++ 8 files changed, 146 insertions(+), 85 deletions(-) diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js index 0220b0af58..051a9675b3 100644 --- a/app/javascript/mastodon/actions/alerts.js +++ b/app/javascript/mastodon/actions/alerts.js @@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS'; export const ALERT_CLEAR = 'ALERT_CLEAR'; export const ALERT_NOOP = 'ALERT_NOOP'; -export function dismissAlert(alert) { - return { - type: ALERT_DISMISS, - alert, - }; -} +export const dismissAlert = alert => ({ + type: ALERT_DISMISS, + alert, +}); -export function clearAlert() { - return { - type: ALERT_CLEAR, - }; -} +export const clearAlert = () => ({ + type: ALERT_CLEAR, +}); -export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) { - return { - type: ALERT_SHOW, - title, - message, - message_values, - }; -} +export const showAlert = alert => ({ + type: ALERT_SHOW, + alert, +}); -export function showAlertForError(error, skipNotFound = false) { +export const showAlertForError = (error, skipNotFound = false) => { if (error.response) { const { data, status, statusText, headers } = error.response; + // Skip these errors as they are reflected in the UI if (skipNotFound && (status === 404 || status === 410)) { - // Skip these errors as they are reflected in the UI return { type: ALERT_NOOP }; } + // Rate limit errors if (status === 429 && headers['x-ratelimit-reset']) { - const reset_date = new Date(headers['x-ratelimit-reset']); - return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date }); + return showAlert({ + title: messages.rateLimitedTitle, + message: messages.rateLimitedMessage, + values: { 'retry_time': new Date(headers['x-ratelimit-reset']) }, + }); } - let message = statusText; - let title = `${status}`; - - if (data.error) { - message = data.error; - } - - return showAlert(title, message); - } else { - console.error(error); - return showAlert(); + return showAlert({ + title: `${status}`, + message: data.error || statusText, + }); } + + console.error(error); + + return showAlert({ + title: messages.unexpectedTitle, + message: messages.unexpectedMessage, + }); } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 99610ac31f..260fb43f08 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -82,6 +82,8 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS'; const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, + open: { id: 'compose.published.open', defaultMessage: 'Open' }, + published: { id: 'compose.published.body', defaultMessage: 'Post published.' }, }); export const ensureComposeIsVisible = (getState, routerHistory) => { @@ -240,6 +242,13 @@ export function submitCompose(routerHistory) { insertIfOnline('public'); insertIfOnline(`account:${response.data.account.id}`); } + + dispatch(showAlert({ + message: messages.published, + action: messages.open, + dismissAfter: 10000, + onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`), + })); }).catch(function (error) { dispatch(submitComposeFail(error)); }); @@ -269,18 +278,19 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function (dispatch, getState) { const uploadLimit = 4; - const media = getState().getIn(['compose', 'media_attachments']); - const pending = getState().getIn(['compose', 'pending_media_attachments']); + const media = getState().getIn(['compose', 'media_attachments']); + const pending = getState().getIn(['compose', 'pending_media_attachments']); const progress = new Array(files.length).fill(0); + let total = Array.from(files).reduce((a, v) => a + v.size, 0); if (files.length + media.size + pending > uploadLimit) { - dispatch(showAlert(undefined, messages.uploadErrorLimit)); + dispatch(showAlert({ message: messages.uploadErrorLimit })); return; } if (getState().getIn(['compose', 'poll'])) { - dispatch(showAlert(undefined, messages.uploadErrorPoll)); + dispatch(showAlert({ message: messages.uploadErrorPoll })); return; } diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index b63796a8b2..1e62ed9a5a 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (permission === 'granted') { dispatch(changePushNotifications(path.slice(1), checked)); } else { - dispatch(showAlert(undefined, messages.permissionDenied)); + dispatch(showAlert({ message: messages.permissionDenied })); } })); } else { @@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (permission === 'granted') { dispatch(changeSetting(['notifications', ...path], checked)); } else { - dispatch(showAlert(undefined, messages.permissionDenied)); + dispatch(showAlert({ message: messages.permissionDenied })); } })); } else { diff --git a/app/javascript/mastodon/features/ui/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js index c1d19f7100..3d60cfdad1 100644 --- a/app/javascript/mastodon/features/ui/containers/notifications_container.js +++ b/app/javascript/mastodon/features/ui/containers/notifications_container.js @@ -7,26 +7,27 @@ import { NotificationStack } from 'react-notification'; import { dismissAlert } from '../../../actions/alerts'; import { getAlerts } from '../../../selectors'; -const mapStateToProps = (state, { intl }) => { - const notifications = getAlerts(state); +const formatIfNeeded = (intl, message, values) => { + if (typeof message === 'object') { + return intl.formatMessage(message, values); + } - notifications.forEach(notification => ['title', 'message'].forEach(key => { - const value = notification[key]; - - if (typeof value === 'object') { - notification[key] = intl.formatMessage(value, notification[`${key}_values`]); - } - })); - - return { notifications }; + return message; }; -const mapDispatchToProps = (dispatch) => { - return { - onDismiss: alert => { - dispatch(dismissAlert(alert)); - }, - }; -}; +const mapStateToProps = (state, { intl }) => ({ + notifications: getAlerts(state).map(alert => ({ + ...alert, + action: formatIfNeeded(intl, alert.action, alert.values), + title: formatIfNeeded(intl, alert.title, alert.values), + message: formatIfNeeded(intl, alert.message, alert.values), + })), +}); + +const mapDispatchToProps = (dispatch) => ({ + onDismiss (alert) { + dispatch(dismissAlert(alert)); + }, +}); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack)); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 2afac7e7e8..8705e6cd68 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -135,6 +135,8 @@ "community.column_settings.remote_only": "Remote only", "compose.language.change": "Change language", "compose.language.search": "Search languages...", + "compose.published.body": "Post published.", + "compose.published.open": "Open", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.", diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js index bd49d748f9..1ca9b62a02 100644 --- a/app/javascript/mastodon/reducers/alerts.js +++ b/app/javascript/mastodon/reducers/alerts.js @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { List as ImmutableList } from 'immutable'; import { ALERT_SHOW, @@ -8,17 +8,20 @@ import { const initialState = ImmutableList([]); +let id = 0; + +const addAlert = (state, alert) => + state.push({ + key: id++, + ...alert, + }); + export default function alerts(state = initialState, action) { switch(action.type) { case ALERT_SHOW: - return state.push(ImmutableMap({ - key: state.size > 0 ? state.last().get('key') + 1 : 0, - title: action.title, - message: action.message, - message_values: action.message_values, - })); + return addAlert(state, action.alert); case ALERT_DISMISS: - return state.filterNot(item => item.get('key') === action.alert.key); + return state.filterNot(item => item.key === action.alert.key); case ALERT_CLEAR: return state.clear(); default: diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index f92e7fe48d..0968fb090b 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -84,26 +84,16 @@ export const makeGetPictureInPicture = () => { })); }; -const getAlertsBase = state => state.get('alerts'); +const ALERT_DEFAULTS = { + dismissAfter: 5000, + style: false, +}; -export const getAlerts = createSelector([getAlertsBase], (base) => { - let arr = []; - - base.forEach(item => { - arr.push({ - message: item.get('message'), - message_values: item.get('message_values'), - title: item.get('title'), - key: item.get('key'), - dismissAfter: 5000, - barStyle: { - zIndex: 200, - }, - }); - }); - - return arr; -}); +export const getAlerts = createSelector(state => state.get('alerts'), alerts => + alerts.map(item => ({ + ...ALERT_DEFAULTS, + ...item, + })).toArray()); export const makeGetNotification = () => createSelector([ (_, base) => base, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5e261e1ee9..434a2f542d 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -9077,3 +9077,62 @@ noscript { } } } + +.notification-list { + position: fixed; + bottom: 2rem; + inset-inline-start: 0; + z-index: 999; + display: flex; + flex-direction: column; + gap: 4px; +} + +.notification-bar { + flex: 0 0 auto; + position: relative; + inset-inline-start: -100%; + width: auto; + padding: 15px; + margin: 0; + color: $primary-text-color; + background: rgba($black, 0.85); + backdrop-filter: blur(8px); + border: 1px solid rgba(lighten($ui-base-color, 4%), 0.85); + border-radius: 8px; + box-shadow: 0 10px 15px -3px rgba($base-shadow-color, 0.25), + 0 4px 6px -4px rgba($base-shadow-color, 0.25); + cursor: default; + transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1); + transform: translateZ(0); + font-size: 15px; + line-height: 21px; + + &.notification-bar-active { + inset-inline-start: 1rem; + } +} + +.notification-bar-title { + margin-inline-end: 5px; +} + +.notification-bar-title, +.notification-bar-action { + font-weight: 700; +} + +.notification-bar-action { + text-transform: uppercase; + margin-inline-start: 10px; + cursor: pointer; + color: $highlight-text-color; + border-radius: 4px; + padding: 0 4px; + + &:hover, + &:focus, + &:active { + background: rgba($ui-base-color, 0.85); + } +} From e75e734ac2d972ee82d344ebb7632a26d0746cb7 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Sat, 8 Jul 2023 20:02:14 +0200 Subject: [PATCH 011/289] Remove unused `missed_update` state (#25832) --- app/javascript/mastodon/reducers/index.ts | 2 -- .../mastodon/reducers/missed_updates.ts | 33 ------------------- 2 files changed, 35 deletions(-) delete mode 100644 app/javascript/mastodon/reducers/missed_updates.ts diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index 67aa5f6c5e..ad3077e37d 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -26,7 +26,6 @@ import lists from './lists'; import markers from './markers'; import media_attachments from './media_attachments'; import meta from './meta'; -import { missedUpdatesReducer } from './missed_updates'; import { modalReducer } from './modal'; import mutes from './mutes'; import notifications from './notifications'; @@ -82,7 +81,6 @@ const reducers = { suggestions, polls, trends, - missed_updates: missedUpdatesReducer, markers, picture_in_picture, history, diff --git a/app/javascript/mastodon/reducers/missed_updates.ts b/app/javascript/mastodon/reducers/missed_updates.ts deleted file mode 100644 index a587fcb036..0000000000 --- a/app/javascript/mastodon/reducers/missed_updates.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Record } from 'immutable'; - -import type { Action } from 'redux'; - -import { focusApp, unfocusApp } from '../actions/app'; -import { NOTIFICATIONS_UPDATE } from '../actions/notifications'; - -interface MissedUpdatesState { - focused: boolean; - unread: number; -} -const initialState = Record({ - focused: true, - unread: 0, -})(); - -export function missedUpdatesReducer( - state = initialState, - action: Action -) { - switch (action.type) { - case focusApp.type: - return state.set('focused', true).set('unread', 0); - case unfocusApp.type: - return state.set('focused', false); - case NOTIFICATIONS_UPDATE: - return state.get('focused') - ? state - : state.update('unread', (x) => x + 1); - default: - return state; - } -} From 6555c33503ae58a327c29a54b91a5d88fc7d8b55 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 8 Jul 2023 14:03:38 -0400 Subject: [PATCH 012/289] Admin mailer parameterization (#25759) --- app/mailers/admin_mailer.rb | 45 +++++++++++-------- app/models/trends.rb | 2 +- app/models/user.rb | 2 +- app/services/appeal_service.rb | 2 +- app/services/report_service.rb | 2 +- config/i18n-tasks.yml | 1 + .../api/v1/reports_controller_spec.rb | 4 +- .../disputes/appeals_controller_spec.rb | 4 +- spec/mailers/admin_mailer_spec.rb | 8 ++-- spec/mailers/previews/admin_mailer_preview.rb | 6 +-- 10 files changed, 40 insertions(+), 36 deletions(-) diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index bc6d87ae6f..5baf9b38a5 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -6,45 +6,52 @@ class AdminMailer < ApplicationMailer helper :accounts helper :languages - def new_report(recipient, report) - @report = report - @me = recipient - @instance = Rails.configuration.x.local_domain + before_action :process_params + before_action :set_instance + + default to: -> { @me.user_email } + + def new_report(report) + @report = report locale_for_account(@me) do - mail to: @me.user_email, subject: I18n.t('admin_mailer.new_report.subject', instance: @instance, id: @report.id) + mail subject: default_i18n_subject(instance: @instance, id: @report.id) end end - def new_appeal(recipient, appeal) - @appeal = appeal - @me = recipient - @instance = Rails.configuration.x.local_domain + def new_appeal(appeal) + @appeal = appeal locale_for_account(@me) do - mail to: @me.user_email, subject: I18n.t('admin_mailer.new_appeal.subject', instance: @instance, username: @appeal.account.username) + mail subject: default_i18n_subject(instance: @instance, username: @appeal.account.username) end end - def new_pending_account(recipient, user) - @account = user.account - @me = recipient - @instance = Rails.configuration.x.local_domain + def new_pending_account(user) + @account = user.account locale_for_account(@me) do - mail to: @me.user_email, subject: I18n.t('admin_mailer.new_pending_account.subject', instance: @instance, username: @account.username) + mail subject: default_i18n_subject(instance: @instance, username: @account.username) end end - def new_trends(recipient, links, tags, statuses) + def new_trends(links, tags, statuses) @links = links @tags = tags @statuses = statuses - @me = recipient - @instance = Rails.configuration.x.local_domain locale_for_account(@me) do - mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trends.subject', instance: @instance) + mail subject: default_i18n_subject(instance: @instance) end end + + private + + def process_params + @me = params[:recipient] + end + + def set_instance + @instance = Rails.configuration.x.local_domain + end end diff --git a/app/models/trends.rb b/app/models/trends.rb index d07d62b71b..7ca51e0b3d 100644 --- a/app/models/trends.rb +++ b/app/models/trends.rb @@ -35,7 +35,7 @@ module Trends return if links_requiring_review.empty? && tags_requiring_review.empty? && statuses_requiring_review.empty? User.those_who_can(:manage_taxonomies).includes(:account).find_each do |user| - AdminMailer.new_trends(user.account, links_requiring_review, tags_requiring_review, statuses_requiring_review).deliver_later! if user.allows_trends_review_emails? + AdminMailer.with(recipient: user.account).new_trends(links_requiring_review, tags_requiring_review, statuses_requiring_review).deliver_later! if user.allows_trends_review_emails? end end diff --git a/app/models/user.rb b/app/models/user.rb index 5ee14bbdaf..fa445af811 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -475,7 +475,7 @@ class User < ApplicationRecord User.those_who_can(:manage_users).includes(:account).find_each do |u| next unless u.allows_pending_account_emails? - AdminMailer.new_pending_account(u.account, self).deliver_later + AdminMailer.with(recipient: u.account).new_pending_account(self).deliver_later end end diff --git a/app/services/appeal_service.rb b/app/services/appeal_service.rb index 399a053d6a..ef052e3547 100644 --- a/app/services/appeal_service.rb +++ b/app/services/appeal_service.rb @@ -23,7 +23,7 @@ class AppealService < BaseService def notify_staff! User.those_who_can(:manage_appeals).includes(:account).each do |u| - AdminMailer.new_appeal(u.account, @appeal).deliver_later if u.allows_appeal_emails? + AdminMailer.with(recipient: u.account).new_appeal(@appeal).deliver_later if u.allows_appeal_emails? end end end diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 3444e1dfaf..39ebd5cd8a 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -40,7 +40,7 @@ class ReportService < BaseService User.those_who_can(:manage_reports).includes(:account).each do |u| LocalNotificationWorker.perform_async(u.account_id, @report.id, 'Report', 'admin.report') - AdminMailer.new_report(u.account, @report).deliver_later if u.allows_report_emails? + AdminMailer.with(recipient: u.account).new_report(@report).deliver_later if u.allows_report_emails? end end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 035a0e999d..cb00a62d5f 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -63,6 +63,7 @@ ignore_unused: - 'admin_mailer.new_appeal.actions.*' - 'statuses.attached.*' - 'move_handler.carry_{mutes,blocks}_over_text' + - 'admin_mailer.*.subject' - 'notification_mailer.*' - 'imports.overwrite_preambles.{following,blocking,muting,domain_blocking,bookmarks}_html' - 'imports.preambles.{following,blocking,muting,domain_blocking,bookmarks}_html' diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb index 01b7e4a71c..f923ff0794 100644 --- a/spec/controllers/api/v1/reports_controller_spec.rb +++ b/spec/controllers/api/v1/reports_controller_spec.rb @@ -23,8 +23,6 @@ RSpec.describe Api::V1::ReportsController do let(:rule_ids) { nil } before do - allow(AdminMailer).to receive(:new_report) - .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) post :create, params: { status_ids: [status.id], account_id: target_account.id, comment: 'reasons', category: category, rule_ids: rule_ids, forward: forward } end @@ -41,7 +39,7 @@ RSpec.describe Api::V1::ReportsController do end it 'sends e-mails to admins' do - expect(AdminMailer).to have_received(:new_report).with(admin.account, Report) + expect(ActionMailer::Base.deliveries.first.to).to eq([admin.email]) end context 'when a status does not belong to the reported account' do diff --git a/spec/controllers/disputes/appeals_controller_spec.rb b/spec/controllers/disputes/appeals_controller_spec.rb index a0f9c7b910..c8444a2a96 100644 --- a/spec/controllers/disputes/appeals_controller_spec.rb +++ b/spec/controllers/disputes/appeals_controller_spec.rb @@ -14,13 +14,11 @@ RSpec.describe Disputes::AppealsController do let(:strike) { Fabricate(:account_warning, target_account: current_user.account) } before do - allow(AdminMailer).to receive(:new_appeal) - .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } } end it 'notifies staff about new appeal' do - expect(AdminMailer).to have_received(:new_appeal).with(admin.account, Appeal.last) + expect(ActionMailer::Base.deliveries.first.to).to eq([admin.email]) end it 'redirects back to the strike page' do diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index 8e2eec40fd..9123804a48 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -7,7 +7,7 @@ RSpec.describe AdminMailer do let(:sender) { Fabricate(:account, username: 'John') } let(:recipient) { Fabricate(:account, username: 'Mike') } let(:report) { Fabricate(:report, account: sender, target_account: recipient) } - let(:mail) { described_class.new_report(recipient, report) } + let(:mail) { described_class.with(recipient: recipient).new_report(report) } before do recipient.user.update(locale: :en) @@ -27,7 +27,7 @@ RSpec.describe AdminMailer do describe '.new_appeal' do let(:appeal) { Fabricate(:appeal) } let(:recipient) { Fabricate(:account, username: 'Kurt') } - let(:mail) { described_class.new_appeal(recipient, appeal) } + let(:mail) { described_class.with(recipient: recipient).new_appeal(appeal) } before do recipient.user.update(locale: :en) @@ -47,7 +47,7 @@ RSpec.describe AdminMailer do describe '.new_pending_account' do let(:recipient) { Fabricate(:account, username: 'Barklums') } let(:user) { Fabricate(:user) } - let(:mail) { described_class.new_pending_account(recipient, user) } + let(:mail) { described_class.with(recipient: recipient).new_pending_account(user) } before do recipient.user.update(locale: :en) @@ -69,7 +69,7 @@ RSpec.describe AdminMailer do let(:links) { [] } let(:statuses) { [] } let(:tags) { [] } - let(:mail) { described_class.new_trends(recipient, links, tags, statuses) } + let(:mail) { described_class.with(recipient: recipient).new_trends(links, tags, statuses) } before do recipient.user.update(locale: :en) diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb index 9572768cd7..bc8f0193b9 100644 --- a/spec/mailers/previews/admin_mailer_preview.rb +++ b/spec/mailers/previews/admin_mailer_preview.rb @@ -5,16 +5,16 @@ class AdminMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_pending_account def new_pending_account - AdminMailer.new_pending_account(Account.first, User.pending.first) + AdminMailer.with(recipient: Account.first).new_pending_account(User.pending.first) end # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trends def new_trends - AdminMailer.new_trends(Account.first, PreviewCard.joins(:trend).limit(3), Tag.limit(3), Status.joins(:trend).where(reblog_of_id: nil).limit(3)) + AdminMailer.with(recipient: Account.first).new_trends(PreviewCard.joins(:trend).limit(3), Tag.limit(3), Status.joins(:trend).where(reblog_of_id: nil).limit(3)) end # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal def new_appeal - AdminMailer.new_appeal(Account.first, Appeal.first) + AdminMailer.with(recipient: Account.first).new_appeal(Appeal.first) end end From b63d71fd489b440665f2afed0d17597ad9fb220d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 8 Jul 2023 14:04:21 -0400 Subject: [PATCH 013/289] Remove unused `NotificationMailer#digest` preview (#25719) --- spec/mailers/previews/notification_mailer_preview.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/mailers/previews/notification_mailer_preview.rb b/spec/mailers/previews/notification_mailer_preview.rb index bc41662a16..214161881b 100644 --- a/spec/mailers/previews/notification_mailer_preview.rb +++ b/spec/mailers/previews/notification_mailer_preview.rb @@ -32,9 +32,4 @@ class NotificationMailerPreview < ActionMailer::Preview r = Status.where.not(reblog_of_id: nil).first NotificationMailer.reblog(r.reblog.account, Notification.find_by(activity: r)) end - - # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/digest - def digest - NotificationMailer.digest(Account.first, since: 90.days.ago) - end end From 7496c1b6556303ddad063edff023d792140cf470 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2023 20:05:33 +0200 Subject: [PATCH 014/289] Change label and design of sensitive and unavailable media in web UI (#25712) --- .../mastodon/components/media_gallery.jsx | 10 ++++-- app/javascript/mastodon/locales/en.json | 4 ++- .../styles/mastodon/components.scss | 33 +++++++++---------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index 1044b729b3..e3c0065c95 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -321,7 +321,10 @@ class MediaGallery extends PureComponent { if (uncached) { spoilerButton = ( ); } else if (visible) { @@ -329,7 +332,10 @@ class MediaGallery extends PureComponent { } else { spoilerButton = ( ); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 8705e6cd68..edecaf60f3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -618,6 +618,8 @@ "status.history.created": "{name} created {date}", "status.history.edited": "{name} edited {date}", "status.load_more": "Load more", + "status.media.open": "Click to open", + "status.media.show": "Click to show", "status.media_hidden": "Media hidden", "status.mention": "Mention @{name}", "status.more": "More", @@ -648,7 +650,7 @@ "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}", "status.translate": "Translate", "status.translated_from_with": "Translated from {lang} using {provider}", - "status.uncached_media_warning": "Not available", + "status.uncached_media_warning": "Preview not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 434a2f542d..0d816ba8d8 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4213,34 +4213,31 @@ a.status-card.compact:hover { } &__overlay { - display: block; - background: transparent; + display: flex; + align-items: center; + justify-content: center; + background: rgba($black, 0.5); width: 100%; height: 100%; + padding: 0; + margin: 0; border: 0; + border-radius: 4px; &__label { - display: inline-block; - background: rgba($base-overlay-background, 0.5); - border-radius: 8px; - padding: 8px 12px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + flex-direction: column; color: $primary-text-color; font-weight: 500; font-size: 14px; } - &:hover, - &:focus, - &:active { - .spoiler-button__overlay__label { - background: rgba($base-overlay-background, 0.8); - } - } - - &:disabled { - .spoiler-button__overlay__label { - background: rgba($base-overlay-background, 0.5); - } + &__action { + font-weight: 400; + font-size: 13px; } } } From b945f16ddf0c87654d5dc5d0f46f475913247dab Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2023 20:16:48 +0200 Subject: [PATCH 015/289] Fix trend calculation working on too many items at a time (#25835) --- app/models/trends/links.rb | 26 +++++++++++++++++++------- app/models/trends/statuses.rb | 26 +++++++++++++++++++------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/models/trends/links.rb b/app/models/trends/links.rb index c94f7c0237..fcbdb1a5f5 100644 --- a/app/models/trends/links.rb +++ b/app/models/trends/links.rb @@ -3,6 +3,8 @@ class Trends::Links < Trends::Base PREFIX = 'trending_links' + BATCH_SIZE = 100 + self.default_options = { threshold: 5, review_threshold: 3, @@ -67,8 +69,21 @@ class Trends::Links < Trends::Base end def refresh(at_time = Time.now.utc) - preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + PreviewCardTrend.pluck(:preview_card_id)).uniq) - calculate_scores(preview_cards, at_time) + # First, recalculate scores for links that were trending previously. We split the queries + # to avoid having to load all of the IDs into Ruby just to send them back into Postgres + PreviewCard.where(id: PreviewCardTrend.select(:preview_card_id)).find_in_batches(batch_size: BATCH_SIZE) do |preview_cards| + calculate_scores(preview_cards, at_time) + end + + # Then, calculate scores for links that were used today. There are potentially some + # duplicate items here that we might process one more time, but that should be fine + PreviewCard.where(id: recently_used_ids(at_time)).find_in_batches(batch_size: BATCH_SIZE) do |preview_cards| + calculate_scores(preview_cards, at_time) + end + + # Now that all trends have up-to-date scores, and all the ones below the threshold have + # been removed, we can recalculate their positions + PreviewCardTrend.connection.exec_update('UPDATE preview_card_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM preview_card_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE preview_card_trends.id = t0.id') end def request_review @@ -139,10 +154,7 @@ class Trends::Links < Trends::Base to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] } to_delete = items.filter { |(score, _)| score < options[:decay_threshold] } - PreviewCardTrend.transaction do - PreviewCardTrend.upsert_all(to_insert.map { |(score, preview_card)| { preview_card_id: preview_card.id, score: score, language: preview_card.language, allowed: preview_card.trendable? || false } }, unique_by: :preview_card_id) if to_insert.any? - PreviewCardTrend.where(preview_card_id: to_delete.map { |(_, preview_card)| preview_card.id }).delete_all if to_delete.any? - PreviewCardTrend.connection.exec_update('UPDATE preview_card_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM preview_card_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE preview_card_trends.id = t0.id') - end + PreviewCardTrend.upsert_all(to_insert.map { |(score, preview_card)| { preview_card_id: preview_card.id, score: score, language: preview_card.language, allowed: preview_card.trendable? || false } }, unique_by: :preview_card_id) if to_insert.any? + PreviewCardTrend.where(preview_card_id: to_delete.map { |(_, preview_card)| preview_card.id }).delete_all if to_delete.any? end end diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb index 84bff9c027..5cd352a6f2 100644 --- a/app/models/trends/statuses.rb +++ b/app/models/trends/statuses.rb @@ -3,6 +3,8 @@ class Trends::Statuses < Trends::Base PREFIX = 'trending_statuses' + BATCH_SIZE = 100 + self.default_options = { threshold: 5, review_threshold: 3, @@ -58,8 +60,21 @@ class Trends::Statuses < Trends::Base end def refresh(at_time = Time.now.utc) - statuses = Status.where(id: (recently_used_ids(at_time) + StatusTrend.pluck(:status_id)).uniq).includes(:status_stat, :account) - calculate_scores(statuses, at_time) + # First, recalculate scores for statuses that were trending previously. We split the queries + # to avoid having to load all of the IDs into Ruby just to send them back into Postgres + Status.where(id: StatusTrend.select(:status_id)).includes(:status_stat, :account).find_in_batches(batch_size: BATCH_SIZE) do |statuses| + calculate_scores(statuses, at_time) + end + + # Then, calculate scores for statuses that were used today. There are potentially some + # duplicate items here that we might process one more time, but that should be fine + Status.where(id: recently_used_ids(at_time)).includes(:status_stat, :account).find_in_batches(batch_size: BATCH_SIZE) do |statuses| + calculate_scores(statuses, at_time) + end + + # Now that all trends have up-to-date scores, and all the ones below the threshold have + # been removed, we can recalculate their positions + StatusTrend.connection.exec_update('UPDATE status_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM status_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE status_trends.id = t0.id') end def request_review @@ -117,10 +132,7 @@ class Trends::Statuses < Trends::Base to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] } to_delete = items.filter { |(score, _)| score < options[:decay_threshold] } - StatusTrend.transaction do - StatusTrend.upsert_all(to_insert.map { |(score, status)| { status_id: status.id, account_id: status.account_id, score: score, language: status.language, allowed: status.trendable? || false } }, unique_by: :status_id) if to_insert.any? - StatusTrend.where(status_id: to_delete.map { |(_, status)| status.id }).delete_all if to_delete.any? - StatusTrend.connection.exec_update('UPDATE status_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM status_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE status_trends.id = t0.id') - end + StatusTrend.upsert_all(to_insert.map { |(score, status)| { status_id: status.id, account_id: status.account_id, score: score, language: status.language, allowed: status.trendable? || false } }, unique_by: :status_id) if to_insert.any? + StatusTrend.where(status_id: to_delete.map { |(_, status)| status.id }).delete_all if to_delete.any? end end From 896f4ef2f65adeae84e244b71995018d87892dd9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 10 Jul 2023 03:06:09 +0200 Subject: [PATCH 016/289] Change feed merge, unmerge and regeneration workers to use a replica (#25849) --- app/workers/merge_worker.rb | 9 ++++++++- app/workers/regeneration_worker.rb | 9 +++++++-- app/workers/unmerge_worker.rb | 9 ++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index e526d2887b..50cfcc3f06 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -5,7 +5,14 @@ class MergeWorker include Redisable def perform(from_account_id, into_account_id) - FeedManager.instance.merge_into_home(Account.find(from_account_id), Account.find(into_account_id)) + ApplicationRecord.connected_to(role: :primary) do + @from_account = Account.find(from_account_id) + @into_account = Account.find(into_account_id) + end + + ApplicationRecord.connected_to(role: :read, prevent_writes: true) do + FeedManager.instance.merge_into_home(@from_account, @into_account) + end rescue ActiveRecord::RecordNotFound true ensure diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb index 5c13c894fe..5ac095e65f 100644 --- a/app/workers/regeneration_worker.rb +++ b/app/workers/regeneration_worker.rb @@ -6,8 +6,13 @@ class RegenerationWorker sidekiq_options lock: :until_executed def perform(account_id, _ = :home) - account = Account.find(account_id) - PrecomputeFeedService.new.call(account) + ApplicationRecord.connected_to(role: :primary) do + @account = Account.find(account_id) + end + + ApplicationRecord.connected_to(role: :read, prevent_writes: true) do + PrecomputeFeedService.new.call(@account) + end rescue ActiveRecord::RecordNotFound true end diff --git a/app/workers/unmerge_worker.rb b/app/workers/unmerge_worker.rb index 1a23faae51..f911ea2f91 100644 --- a/app/workers/unmerge_worker.rb +++ b/app/workers/unmerge_worker.rb @@ -6,7 +6,14 @@ class UnmergeWorker sidekiq_options queue: 'pull' def perform(from_account_id, into_account_id) - FeedManager.instance.unmerge_from_home(Account.find(from_account_id), Account.find(into_account_id)) + ApplicationRecord.connected_to(role: :primary) do + @from_account = Account.find(from_account_id) + @into_account = Account.find(into_account_id) + end + + ApplicationRecord.connected_to(role: :read, prevent_writes: true) do + FeedManager.instance.unmerge_from_home(@from_account, @into_account) + end rescue ActiveRecord::RecordNotFound true end From 482071a9761b02d0937875fb49165fc763713ccc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sun, 9 Jul 2023 21:06:22 -0400 Subject: [PATCH 017/289] Refactor `NotificationMailer` to use parameterization (#25718) --- app/mailers/notification_mailer.rb | 71 +++++++++---------- app/services/notify_service.rb | 7 +- spec/mailers/notification_mailer_spec.rb | 21 ++++-- .../previews/notification_mailer_preview.rb | 29 +++++--- 4 files changed, 73 insertions(+), 55 deletions(-) diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 7cd3bab1af..277612366b 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -1,83 +1,76 @@ # frozen_string_literal: true class NotificationMailer < ApplicationMailer - helper :accounts - helper :statuses + helper :accounts, + :statuses, + :routing - helper RoutingHelper + before_action :process_params + before_action :set_status, only: [:mention, :favourite, :reblog] + before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request] - def mention(recipient, notification) - @me = recipient - @user = recipient.user - @type = 'mention' - @status = notification.target_status + default to: -> { email_address_with_name(@user.email, @me.username) } + def mention return unless @user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) - mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct) + mail subject: default_i18n_subject(name: @status.account.acct) end end - def follow(recipient, notification) - @me = recipient - @user = recipient.user - @type = 'follow' - @account = notification.from_account - + def follow return unless @user.functional? locale_for_account(@me) do - mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) + mail subject: default_i18n_subject(name: @account.acct) end end - def favourite(recipient, notification) - @me = recipient - @user = recipient.user - @type = 'favourite' - @account = notification.from_account - @status = notification.target_status - + def favourite return unless @user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) - mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct) + mail subject: default_i18n_subject(name: @account.acct) end end - def reblog(recipient, notification) - @me = recipient - @user = recipient.user - @type = 'reblog' - @account = notification.from_account - @status = notification.target_status - + def reblog return unless @user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) - mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct) + mail subject: default_i18n_subject(name: @account.acct) end end - def follow_request(recipient, notification) - @me = recipient - @user = recipient.user - @type = 'follow_request' - @account = notification.from_account - + def follow_request return unless @user.functional? locale_for_account(@me) do - mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) + mail subject: default_i18n_subject(name: @account.acct) end end private + def process_params + @notification = params[:notification] + @me = params[:recipient] + @user = @me.user + @type = action_name + end + + def set_status + @status = @notification.target_status + end + + def set_account + @account = @notification.from_account + end + def thread_by_conversation(conversation) return if conversation.nil? diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index ad9e6e3d63..06b48d5586 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -162,7 +162,12 @@ class NotifyService < BaseService end def send_email! - NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later(wait: 2.minutes) if NotificationMailer.respond_to?(@notification.type) + return unless NotificationMailer.respond_to?(@notification.type) + + NotificationMailer + .with(recipient: @recipient, notification: @notification) + .public_send(@notification.type) + .deliver_later(wait: 2.minutes) end def email_needed? diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index bf364b6253..3efb97cb18 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -23,7 +23,8 @@ RSpec.describe NotificationMailer do describe 'mention' do let(:mention) { Mention.create!(account: receiver.account, status: foreign_status) } - let(:mail) { described_class.mention(receiver.account, Notification.create!(account: receiver.account, activity: mention)) } + let(:notification) { Notification.create!(account: receiver.account, activity: mention) } + let(:mail) { prepared_mailer_for(receiver.account).mention } include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob' @@ -40,7 +41,8 @@ RSpec.describe NotificationMailer do describe 'follow' do let(:follow) { sender.follow!(receiver.account) } - let(:mail) { described_class.follow(receiver.account, Notification.create!(account: receiver.account, activity: follow)) } + let(:notification) { Notification.create!(account: receiver.account, activity: follow) } + let(:mail) { prepared_mailer_for(receiver.account).follow } include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob' @@ -56,7 +58,8 @@ RSpec.describe NotificationMailer do describe 'favourite' do let(:favourite) { Favourite.create!(account: sender, status: own_status) } - let(:mail) { described_class.favourite(own_status.account, Notification.create!(account: receiver.account, activity: favourite)) } + let(:notification) { Notification.create!(account: receiver.account, activity: favourite) } + let(:mail) { prepared_mailer_for(own_status.account).favourite } include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob' @@ -73,7 +76,8 @@ RSpec.describe NotificationMailer do describe 'reblog' do let(:reblog) { Status.create!(account: sender, reblog: own_status) } - let(:mail) { described_class.reblog(own_status.account, Notification.create!(account: receiver.account, activity: reblog)) } + let(:notification) { Notification.create!(account: receiver.account, activity: reblog) } + let(:mail) { prepared_mailer_for(own_status.account).reblog } include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob' @@ -90,7 +94,8 @@ RSpec.describe NotificationMailer do describe 'follow_request' do let(:follow_request) { Fabricate(:follow_request, account: sender, target_account: receiver.account) } - let(:mail) { described_class.follow_request(receiver.account, Notification.create!(account: receiver.account, activity: follow_request)) } + let(:notification) { Notification.create!(account: receiver.account, activity: follow_request) } + let(:mail) { prepared_mailer_for(receiver.account).follow_request } include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob' @@ -103,4 +108,10 @@ RSpec.describe NotificationMailer do expect(mail.body.encoded).to match('bob has requested to follow you') end end + + private + + def prepared_mailer_for(recipient) + described_class.with(recipient: recipient, notification: notification) + end end diff --git a/spec/mailers/previews/notification_mailer_preview.rb b/spec/mailers/previews/notification_mailer_preview.rb index 214161881b..a63c20c27c 100644 --- a/spec/mailers/previews/notification_mailer_preview.rb +++ b/spec/mailers/previews/notification_mailer_preview.rb @@ -5,31 +5,40 @@ class NotificationMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/mention def mention - m = Mention.last - NotificationMailer.mention(m.account, Notification.find_by(activity: m)) + activity = Mention.last + mailer_for(activity.account, activity).mention end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow def follow - f = Follow.last - NotificationMailer.follow(f.target_account, Notification.find_by(activity: f)) + activity = Follow.last + mailer_for(activity.target_account, activity).follow end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/follow_request def follow_request - f = Follow.last - NotificationMailer.follow_request(f.target_account, Notification.find_by(activity: f)) + activity = Follow.last + mailer_for(activity.target_account, activity).follow_request end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/favourite def favourite - f = Favourite.last - NotificationMailer.favourite(f.status.account, Notification.find_by(activity: f)) + activity = Favourite.last + mailer_for(activity.status.account, activity).favourite end # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/reblog def reblog - r = Status.where.not(reblog_of_id: nil).first - NotificationMailer.reblog(r.reblog.account, Notification.find_by(activity: r)) + activity = Status.where.not(reblog_of_id: nil).first + mailer_for(activity.reblog.account, activity).reblog + end + + private + + def mailer_for(account, activity) + NotificationMailer.with( + recipient: account, + notification: Notification.find_by(activity: activity) + ) end end From 66693177ff14baeb91a0c8f3c3d98771c63ea0ef Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 10 Jul 2023 18:26:56 +0200 Subject: [PATCH 018/289] Add `forward_to_domains` parameter to `POST /api/v1/reports` (#25866) --- app/controllers/api/v1/reports_controller.rb | 2 +- .../mastodon/features/report/comment.jsx | 150 +++++++++++------- .../features/ui/components/report_modal.jsx | 34 ++-- .../styles/mastodon/components.scss | 1 + app/services/report_service.rb | 22 ++- spec/services/report_service_spec.rb | 24 ++- 6 files changed, 153 insertions(+), 80 deletions(-) diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 8ff6c8fe5c..300c9faa3f 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -23,6 +23,6 @@ class Api::V1::ReportsController < Api::BaseController end def report_params - params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: []) + params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: []) end end diff --git a/app/javascript/mastodon/features/report/comment.jsx b/app/javascript/mastodon/features/report/comment.jsx index 4888b76bcb..98ac4caa0a 100644 --- a/app/javascript/mastodon/features/report/comment.jsx +++ b/app/javascript/mastodon/features/report/comment.jsx @@ -1,87 +1,121 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { OrderedSet, List as ImmutableList } from 'immutable'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { shallowEqual } from 'react-redux'; +import { createSelector } from 'reselect'; import Toggle from 'react-toggle'; +import { fetchAccount } from 'mastodon/actions/accounts'; import Button from 'mastodon/components/button'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; const messages = defineMessages({ placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' }, }); -class Comment extends PureComponent { +const selectRepliedToAccountIds = createSelector( + [ + (state) => state.get('statuses'), + (_, statusIds) => statusIds, + ], + (statusesMap, statusIds) => statusIds.map((statusId) => statusesMap.getIn([statusId, 'in_reply_to_account_id'])), + { + resultEqualityCheck: shallowEqual, + } +); - static propTypes = { - onSubmit: PropTypes.func.isRequired, - comment: PropTypes.string.isRequired, - onChangeComment: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - isSubmitting: PropTypes.bool, - forward: PropTypes.bool, - isRemote: PropTypes.bool, - domain: PropTypes.string, - onChangeForward: PropTypes.func.isRequired, - }; +const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedDomains, onSubmit, onChangeComment, onToggleDomain }) => { + const intl = useIntl(); - handleClick = () => { - const { onSubmit } = this.props; - onSubmit(); - }; + const dispatch = useAppDispatch(); + const loadedRef = useRef(false); - handleChange = e => { - const { onChangeComment } = this.props; - onChangeComment(e.target.value); - }; + const handleClick = useCallback(() => onSubmit(), [onSubmit]); + const handleChange = useCallback((e) => onChangeComment(e.target.value), [onChangeComment]); + const handleToggleDomain = useCallback(e => onToggleDomain(e.target.value, e.target.checked), [onToggleDomain]); - handleKeyDown = e => { + const handleKeyDown = useCallback((e) => { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleClick(); + handleClick(); } - }; + }, [handleClick]); - handleForwardChange = e => { - const { onChangeForward } = this.props; - onChangeForward(e.target.checked); - }; + // Memoize accountIds since we don't want it to trigger `useEffect` on each render + const accountIds = useAppSelector((state) => domain ? selectRepliedToAccountIds(state, statusIds) : ImmutableList()); - render () { - const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props; + // While we could memoize `availableDomains`, it is pretty inexpensive to recompute + const accountsMap = useAppSelector((state) => state.get('accounts')); + const availableDomains = domain ? OrderedSet([domain]).union(accountIds.map((accountId) => accountsMap.getIn([accountId, 'acct'], '').split('@')[1]).filter(domain => !!domain)) : OrderedSet(); - return ( - <> -

+ useEffect(() => { + if (loadedRef.current) { + return; + } -