From 855c3be3d70a9c9bd6c47b45ec6057ec322165ea Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 25 Sep 2025 18:29:45 +0200 Subject: [PATCH] Revert "Merge upstream changes up to df72a2dbbec8173515568c02427076ebff5c2297" --- Gemfile | 2 +- Gemfile.lock | 4 +- .../interaction_policies_controller.rb | 5 + app/controllers/api/v1/statuses_controller.rb | 2 + .../api/interaction_policies_concern.rb | 2 + .../glitch/components/alert/alert.stories.tsx | 15 -- .../glitch/components/alert/index.tsx | 19 +- .../components/display_name/no-domain.tsx | 5 +- .../components/exit_animation_wrapper.tsx | 53 ------ .../flavours/glitch/components/status.jsx | 2 - .../status/boost_button.stories.tsx | 16 +- .../glitch/components/status/boost_button.tsx | 70 +++++++- .../components/status_action_bar/index.jsx | 3 +- .../glitch/components/status_content.jsx | 36 +++- .../glitch/containers/status_container.js | 5 +- .../components/account_header.tsx | 34 +++- .../compose/components/visibility_button.tsx | 5 + .../components/conversation.jsx | 29 ++- .../directory/components/account_card.tsx | 38 +++- .../glitch/features/emoji/emoji_html.tsx | 22 +-- .../glitch/features/emoji/handlers.ts | 61 ------- .../components/announcements.jsx | 48 +++-- .../features/keyboard_shortcuts/index.jsx | 11 +- .../components/embedded_status.tsx | 30 +++- .../features/status/components/action_bar.jsx | 3 +- .../status/components/refresh_controller.tsx | 166 +++++------------- .../flavours/glitch/features/status/index.jsx | 22 +-- .../flavours/glitch/features/ui/index.jsx | 10 +- .../flavours/glitch/styles/admin.scss | 19 +- .../flavours/glitch/styles/components.scss | 50 +----- .../flavours/glitch/utils/environment.ts | 6 +- .../components/alert/alert.stories.tsx | 15 -- .../mastodon/components/alert/index.tsx | 19 +- .../components/display_name/no-domain.tsx | 5 +- .../components/exit_animation_wrapper.tsx | 53 ------ app/javascript/mastodon/components/status.jsx | 2 - .../status/boost_button.stories.tsx | 16 +- .../components/status/boost_button.tsx | 70 +++++++- .../components/status_action_bar/index.jsx | 3 +- .../mastodon/components/status_content.jsx | 36 +++- .../mastodon/containers/status_container.jsx | 6 +- .../components/account_header.tsx | 34 +++- .../compose/components/visibility_button.tsx | 5 + .../components/conversation.jsx | 29 ++- .../directory/components/account_card.tsx | 38 +++- .../mastodon/features/emoji/emoji_html.tsx | 22 +-- .../mastodon/features/emoji/handlers.ts | 61 ------- .../components/announcements.jsx | 48 +++-- .../features/keyboard_shortcuts/index.jsx | 11 +- .../components/embedded_status.tsx | 30 +++- .../features/status/components/action_bar.jsx | 3 +- .../status/components/refresh_controller.tsx | 166 +++++------------- .../mastodon/features/status/index.jsx | 20 +-- app/javascript/mastodon/features/ui/index.jsx | 10 +- app/javascript/mastodon/locales/cs.json | 2 - app/javascript/mastodon/locales/cy.json | 2 - app/javascript/mastodon/locales/da.json | 2 - app/javascript/mastodon/locales/de.json | 2 - app/javascript/mastodon/locales/el.json | 4 +- app/javascript/mastodon/locales/en.json | 9 +- app/javascript/mastodon/locales/es-AR.json | 2 - app/javascript/mastodon/locales/es-MX.json | 2 - app/javascript/mastodon/locales/es.json | 2 - app/javascript/mastodon/locales/et.json | 2 - app/javascript/mastodon/locales/fi.json | 2 - app/javascript/mastodon/locales/fo.json | 2 - app/javascript/mastodon/locales/ga.json | 2 - app/javascript/mastodon/locales/gl.json | 2 - app/javascript/mastodon/locales/he.json | 2 - app/javascript/mastodon/locales/hu.json | 2 - app/javascript/mastodon/locales/ja.json | 11 +- app/javascript/mastodon/locales/nl.json | 2 - app/javascript/mastodon/locales/tr.json | 2 - app/javascript/mastodon/locales/vi.json | 2 - app/javascript/mastodon/locales/zh-CN.json | 2 - app/javascript/mastodon/locales/zh-TW.json | 2 - app/javascript/mastodon/utils/environment.ts | 6 +- app/javascript/styles/mastodon/admin.scss | 19 +- .../styles/mastodon/components.scss | 50 +----- app/lib/activitypub/activity/quote_request.rb | 2 +- .../activitypub/note_serializer.rb | 2 +- .../rest/shallow_status_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 2 +- app/services/post_status_service.rb | 2 +- .../announcements/_announcement.html.haml | 2 +- app/views/admin/roles/_role.html.haml | 2 +- app/views/admin/rules/_rule.html.haml | 2 +- .../warning_presets/_warning_preset.html.haml | 2 +- app/views/admin/webhooks/_webhook.html.haml | 2 +- app/views/filters/_filter.html.haml | 6 +- .../authorized_applications/index.html.haml | 2 +- config/locales/el.yml | 4 +- config/locales/ja.yml | 6 - config/puma.rb | 4 +- lib/mastodon/version.rb | 2 +- .../activity/quote_request_spec.rb | 2 +- spec/lib/status_cache_hydrator_spec.rb | 2 +- .../v1/statuses/interaction_policies_spec.rb | 2 +- spec/requests/api/v1/statuses_spec.rb | 12 +- .../activitypub/note_serializer_spec.rb | 2 +- spec/services/post_status_service_spec.rb | 8 - 101 files changed, 759 insertions(+), 945 deletions(-) delete mode 100644 app/javascript/flavours/glitch/components/exit_animation_wrapper.tsx delete mode 100644 app/javascript/flavours/glitch/features/emoji/handlers.ts delete mode 100644 app/javascript/mastodon/components/exit_animation_wrapper.tsx delete mode 100644 app/javascript/mastodon/features/emoji/handlers.ts diff --git a/Gemfile b/Gemfile index 126d73f9ca..5dd5d5bf26 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' ruby '>= 3.2.0', '< 3.5.0' gem 'propshaft' -gem 'puma', '~> 7.0' +gem 'puma', '~> 6.3' gem 'rails', '~> 8.0' gem 'thor', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 64ef3057d8..b3364ba38d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -637,7 +637,7 @@ GEM date stringio public_suffix (6.0.2) - puma (7.0.3) + puma (6.6.1) nio4r (~> 2.0) pundit (2.5.1) activesupport (>= 3.0.0) @@ -1052,7 +1052,7 @@ DEPENDENCIES prometheus_exporter (~> 2.2) propshaft public_suffix (~> 6.0) - puma (~> 7.0) + puma (~> 6.3) pundit (~> 2.3) rack-attack (~> 6.6) rack-cors diff --git a/app/controllers/api/v1/statuses/interaction_policies_controller.rb b/app/controllers/api/v1/statuses/interaction_policies_controller.rb index 6e2745806d..b8ec4fe140 100644 --- a/app/controllers/api/v1/statuses/interaction_policies_controller.rb +++ b/app/controllers/api/v1/statuses/interaction_policies_controller.rb @@ -4,6 +4,7 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base include Api::InteractionPoliciesConcern before_action -> { doorkeeper_authorize! :write, :'write:statuses' } + before_action -> { check_feature_enabled } def update authorize @status, :update? @@ -21,6 +22,10 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base params.permit(:quote_approval_policy) end + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled? + end + def broadcast_updates! DistributionWorker.perform_async(@status.id, { 'update' => true }) ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 }) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 6619899041..6b5b397152 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -159,6 +159,8 @@ class Api::V1::StatusesController < Api::BaseController end def set_quoted_status + return unless Mastodon::Feature.outgoing_quotes_enabled? + @quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present? authorize(@quoted_status, :quote?) if @quoted_status.present? rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError diff --git a/app/controllers/concerns/api/interaction_policies_concern.rb b/app/controllers/concerns/api/interaction_policies_concern.rb index f1e1480c0c..5b63705a9b 100644 --- a/app/controllers/concerns/api/interaction_policies_concern.rb +++ b/app/controllers/concerns/api/interaction_policies_concern.rb @@ -4,6 +4,8 @@ module Api::InteractionPoliciesConcern extend ActiveSupport::Concern def quote_approval_policy + return nil unless Mastodon::Feature.outgoing_quotes_enabled? + case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy when 'public' Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 diff --git a/app/javascript/flavours/glitch/components/alert/alert.stories.tsx b/app/javascript/flavours/glitch/components/alert/alert.stories.tsx index f12f06751d..4d5f8acb65 100644 --- a/app/javascript/flavours/glitch/components/alert/alert.stories.tsx +++ b/app/javascript/flavours/glitch/components/alert/alert.stories.tsx @@ -8,7 +8,6 @@ const meta = { component: Alert, args: { isActive: true, - isLoading: false, animateFrom: 'side', title: '', message: '', @@ -21,12 +20,6 @@ const meta = { type: 'boolean', description: 'Animate to the active (displayed) state of the alert', }, - isLoading: { - control: 'boolean', - type: 'boolean', - description: - 'Display a loading indicator in the alert, replacing the dismiss button if present', - }, animateFrom: { control: 'radio', type: 'string', @@ -115,11 +108,3 @@ export const InSizedContainer: Story = { ), }; - -export const WithLoadingIndicator: Story = { - args: { - ...WithDismissButton.args, - isLoading: true, - }, - render: InSizedContainer.render, -}; diff --git a/app/javascript/flavours/glitch/components/alert/index.tsx b/app/javascript/flavours/glitch/components/alert/index.tsx index eb0abcb518..1009e77524 100644 --- a/app/javascript/flavours/glitch/components/alert/index.tsx +++ b/app/javascript/flavours/glitch/components/alert/index.tsx @@ -3,7 +3,6 @@ import { useIntl } from 'react-intl'; import classNames from 'classnames'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { IconButton } from '../icon_button'; @@ -11,23 +10,21 @@ import { IconButton } from '../icon_button'; * Snackbar/Toast-style notification component. */ export const Alert: React.FC<{ + isActive?: boolean; + animateFrom?: 'side' | 'below'; title?: string; message: string; action?: string; onActionClick?: () => void; onDismiss?: () => void; - isActive?: boolean; - isLoading?: boolean; - animateFrom?: 'side' | 'below'; }> = ({ + isActive, + animateFrom = 'side', title, message, action, onActionClick, onDismiss, - isActive, - isLoading, - animateFrom = 'side', }) => { const intl = useIntl(); @@ -54,13 +51,7 @@ export const Alert: React.FC<{ )} - {isLoading && ( - - - - )} - - {onDismiss && !isLoading && ( + {onDismiss && ( > = ({ account, className, children, ...props }) => { return ( - + {account ? ( React.ReactNode; -}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => { - const [delayedIsActive, setDelayedIsActive] = useState(false); - - useEffect(() => { - if (isActive && !withEntryDelay) { - setDelayedIsActive(true); - - return () => ''; - } else { - const timeout = setTimeout(() => { - setDelayedIsActive(isActive); - }, delayMs); - - return () => { - clearTimeout(timeout); - }; - } - }, [isActive, delayMs, withEntryDelay]); - - if (!isActive && !delayedIsActive) { - return null; - } - - return children(isActive && delayedIsActive); -}; diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 4362b70437..575eefc846 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -118,7 +118,6 @@ class Status extends ImmutablePureComponent { prepend: PropTypes.string, withDismiss: PropTypes.bool, isQuotedPost: PropTypes.bool, - shouldHighlightOnMount: PropTypes.bool, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, expanded: PropTypes.bool, @@ -706,7 +705,6 @@ class Status extends ImmutablePureComponent { muted: this.props.muted, 'status--is-quote': isQuotedPost, 'status--has-quote': !!status.get('quote'), - 'status--highlighted-entry': this.props.shouldHighlightOnMount, }) } data-id={status.get('id')} diff --git a/app/javascript/flavours/glitch/components/status/boost_button.stories.tsx b/app/javascript/flavours/glitch/components/status/boost_button.stories.tsx index 0376518124..ba8736fe7f 100644 --- a/app/javascript/flavours/glitch/components/status/boost_button.stories.tsx +++ b/app/javascript/flavours/glitch/components/status/boost_button.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses'; import { statusFactoryState } from '@/testing/factories'; -import { BoostButton } from './boost_button'; +import { LegacyReblogButton, StatusBoostButton } from './boost_button'; interface StoryProps { visibility: StatusVisibility; @@ -38,7 +38,10 @@ const meta = { }, }, render: (args) => ( - 0} /> + 0} + /> ), } satisfies Meta; @@ -75,3 +78,12 @@ export const Mine: Story = { }, }, }; + +export const Legacy: Story = { + render: (args) => ( + 0} + /> + ), +}; diff --git a/app/javascript/flavours/glitch/components/status/boost_button.tsx b/app/javascript/flavours/glitch/components/status/boost_button.tsx index 17ebcbc768..7d6580d52d 100644 --- a/app/javascript/flavours/glitch/components/status/boost_button.tsx +++ b/app/javascript/flavours/glitch/components/status/boost_button.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import type { FC, KeyboardEvent, MouseEvent } from 'react'; +import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler } from 'react'; import { useIntl } from 'react-intl'; @@ -11,6 +11,7 @@ import { openModal } from '@/flavours/glitch/actions/modal'; import type { ActionMenuItem } from '@/flavours/glitch/models/dropdown_menu'; import type { Status } from '@/flavours/glitch/models/status'; import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; +import { isFeatureEnabled } from '@/flavours/glitch/utils/environment'; import type { SomeRequired } from '@/flavours/glitch/utils/types'; import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu'; @@ -46,7 +47,10 @@ interface ReblogButtonProps { type ActionMenuItemWithIcon = SomeRequired; -export const BoostButton: FC = ({ status, counters }) => { +export const StatusBoostButton: FC = ({ + status, + counters, +}) => { const intl = useIntl(); const dispatch = useAppDispatch(); const statusState = useAppSelector((state) => @@ -188,3 +192,65 @@ const ReblogMenuItem: FC = ({ ); }; + +// Legacy helpers + +// Switch between the legacy and new reblog button based on feature flag. +export const BoostButton: FC = (props) => { + if (isFeatureEnabled('outgoing_quotes')) { + return ; + } + return ; +}; + +export const LegacyReblogButton: FC = ({ + status, + counters, +}) => { + const intl = useIntl(); + const statusState = useAppSelector((state) => + selectStatusState(state, status), + ); + + const { title, meta, iconComponent, disabled } = useMemo( + () => boostItemState(statusState), + [statusState], + ); + + const dispatch = useAppDispatch(); + const handleClick: MouseEventHandler = useCallback( + (event) => { + if (statusState.isLoggedIn) { + dispatch(toggleReblog(status.get('id') as string, event.shiftKey)); + } else { + dispatch( + openModal({ + modalType: 'INTERACTION', + modalProps: { + accountId: status.getIn(['account', 'id']), + url: status.get('uri'), + }, + }), + ); + } + }, + [dispatch, status, statusState.isLoggedIn], + ); + + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/status_action_bar/index.jsx b/app/javascript/flavours/glitch/components/status_action_bar/index.jsx index 9284fb17f0..04585cb835 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar/index.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar/index.jsx @@ -26,6 +26,7 @@ import { me } from '../../initial_state'; import { IconButton } from '../icon_button'; import { RelativeTimestamp } from '../relative_timestamp'; +import { isFeatureEnabled } from '../../utils/environment'; import { BoostButton } from '../status/boost_button'; import { RemoveQuoteHint } from './remove_quote_hint'; @@ -253,7 +254,7 @@ class StatusActionBar extends ImmutablePureComponent { if (writtenByMe || withDismiss) { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - if (writtenByMe && !['private', 'direct'].includes(status.get('visibility'))) { + if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) { menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange }); } menu.push(null); diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index fb52fd68cf..f1b8520122 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -237,6 +237,32 @@ class StatusContent extends PureComponent { } } + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } + }; + + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + }; + componentDidMount () { this._updateStatusLinks(); } @@ -328,13 +354,7 @@ class StatusContent extends PureComponent { if (this.props.onClick) { return ( <> -
+
+
({ }, onQuote (status) { - dispatch(quoteComposeById(status.get('id'))); + if (isFeatureEnabled('outgoing_quotes')) { + dispatch(quoteComposeById(status.get('id'))); + } }, onReblog (status, e) { diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx index 163faf0a51..88a421bc61 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx @@ -383,6 +383,36 @@ export const AccountHeader: React.FC<{ }); }, [account]); + const handleMouseEnter = useCallback( + ({ currentTarget }: React.MouseEvent) => { + if (autoPlayGif) { + return; + } + + currentTarget + .querySelectorAll('.custom-emoji') + .forEach((emoji) => { + emoji.src = emoji.getAttribute('data-original') ?? ''; + }); + }, + [], + ); + + const handleMouseLeave = useCallback( + ({ currentTarget }: React.MouseEvent) => { + if (autoPlayGif) { + return; + } + + currentTarget + .querySelectorAll('.custom-emoji') + .forEach((emoji) => { + emoji.src = emoji.getAttribute('data-static') ?? ''; + }); + }, + [], + ); + const suspended = account?.suspended; const isRemote = account?.acct !== account?.username; const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; @@ -782,9 +812,11 @@ export const AccountHeader: React.FC<{ )}
{!(suspended || hidden || account.moved) && relationship?.requested_by && ( diff --git a/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx b/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx index 1e6462ecd3..78d77ca536 100644 --- a/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx +++ b/app/javascript/flavours/glitch/features/compose/components/visibility_button.tsx @@ -12,12 +12,14 @@ import type { ApiQuotePolicy } from '@/flavours/glitch/api_types/quotes'; import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses'; import { Icon } from '@/flavours/glitch/components/icon'; import { useAppSelector, useAppDispatch } from '@/flavours/glitch/store'; +import { isFeatureEnabled } from '@/flavours/glitch/utils/environment'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import type { VisibilityModalCallback } from '../../ui/components/visibility_modal'; +import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import { messages as privacyMessages } from './privacy_dropdown'; @@ -41,6 +43,9 @@ interface PrivacyDropdownProps { } export const VisibilityButton: FC = (props) => { + if (!isFeatureEnabled('outgoing_quotes')) { + return ; + } return ; }; diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx index cf35e28620..ff682c296d 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx @@ -23,6 +23,7 @@ import { IconButton } from 'flavours/glitch/components/icon_button'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import StatusContent from 'flavours/glitch/components/status_content'; import { Dropdown } from 'flavours/glitch/components/dropdown_menu'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; import { makeGetStatus } from 'flavours/glitch/selectors'; import { LinkedDisplayName } from '@/flavours/glitch/components/display_name'; @@ -60,6 +61,32 @@ export const Conversation = ({ conversation, scrollKey }) => { const sharedCWState = useSelector(state => state.getIn(['state', 'content_warnings', 'shared_state'])); const [expanded, setExpanded] = useState(undefined); + const handleMouseEnter = useCallback(({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } + }, []); + + const handleMouseLeave = useCallback(({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + }, []); + const handleClick = useCallback(() => { if (unread) { dispatch(markConversationRead(id)); @@ -144,7 +171,7 @@ export const Conversation = ({ conversation, scrollKey }) => { {unread && }
-
+
{names} }} />
diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.tsx b/app/javascript/flavours/glitch/features/directory/components/account_card.tsx index deb4f4832d..8afb9f59d5 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.tsx +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.tsx @@ -1,3 +1,4 @@ +import type { MouseEventHandler } from 'react'; import { useCallback } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; @@ -43,6 +44,39 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { const account = useAppSelector((s) => getAccount(s, accountId)); const dispatch = useAppDispatch(); + const handleMouseEnter = useCallback( + ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + emojis.forEach((emoji) => { + const original = emoji.getAttribute('data-original'); + if (original) emoji.src = original; + }); + }, + [], + ); + + const handleMouseLeave = useCallback( + ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + emojis.forEach((emoji) => { + const staticUrl = emoji.getAttribute('data-static'); + if (staticUrl) emoji.src = staticUrl; + }); + }, + [], + ); + const handleFollow = useCallback(() => { if (!account) return; @@ -155,7 +189,9 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { {account.get('note').length > 0 && (
)} diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx b/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx index 4fa671431c..f620193ab7 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx +++ b/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx @@ -1,7 +1,5 @@ import type { ComponentPropsWithoutRef, ElementType } from 'react'; -import classNames from 'classnames'; - import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment'; import { useEmojify } from './hooks'; @@ -9,13 +7,12 @@ import type { CustomEmojiMapArg } from './types'; type EmojiHTMLProps = Omit< ComponentPropsWithoutRef, - 'dangerouslySetInnerHTML' | 'className' + 'dangerouslySetInnerHTML' > & { htmlString: string; extraEmojis?: CustomEmojiMapArg; as?: Element; shallow?: boolean; - className?: string; }; export const ModernEmojiHTML = ({ @@ -23,7 +20,6 @@ export const ModernEmojiHTML = ({ htmlString, as: Wrapper = 'div', // Rename for syntax highlighting shallow, - className = '', ...props }: EmojiHTMLProps) => { const emojifiedHtml = useEmojify({ @@ -37,11 +33,7 @@ export const ModernEmojiHTML = ({ } return ( - + ); }; @@ -51,13 +43,7 @@ export const EmojiHTML = ( if (isModernEmojiEnabled()) { return ; } - const { as: asElement, htmlString, extraEmojis, className, ...rest } = props; + const { as: asElement, htmlString, extraEmojis, ...rest } = props; const Wrapper = asElement ?? 'div'; - return ( - - ); + return ; }; diff --git a/app/javascript/flavours/glitch/features/emoji/handlers.ts b/app/javascript/flavours/glitch/features/emoji/handlers.ts deleted file mode 100644 index dbfe194fa4..0000000000 --- a/app/javascript/flavours/glitch/features/emoji/handlers.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { autoPlayGif } from '@/flavours/glitch/initial_state'; - -const PARENT_MAX_DEPTH = 10; - -export function handleAnimateGif(event: MouseEvent) { - // We already check this in ui/index.jsx, but just to be sure. - if (autoPlayGif) { - return; - } - - const { target, type } = event; - const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate. - - if (target instanceof HTMLImageElement) { - setAnimateGif(target, animate); - } else if (!(target instanceof HTMLElement) || target === document.body) { - return; - } - - let parent: HTMLElement | null = null; - let iter = 0; - - if (target.classList.contains('animate-parent')) { - parent = target; - } else { - // Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'. - let current: HTMLElement | null = target; - while (current) { - if (iter >= PARENT_MAX_DEPTH) { - return; // We can just exit right now. - } - current = current.parentElement; - if (current?.classList.contains('animate-parent')) { - parent = current; - break; - } - iter++; - } - } - - // Affect all animated children within the parent. - if (parent) { - const animatedChildren = - parent.querySelectorAll('img.custom-emoji'); - for (const child of animatedChildren) { - setAnimateGif(child, animate); - } - } -} - -function setAnimateGif(image: HTMLImageElement, animate: boolean) { - const { classList, dataset } = image; - if ( - !classList.contains('custom-emoji') || - !dataset.static || - !dataset.original - ) { - return; - } - image.src = animate ? dataset.original : dataset.static; -} diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx index 5bfba51957..66567fb709 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx @@ -111,14 +111,42 @@ class ContentWithRouter extends ImmutablePureComponent { } }; + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } + }; + + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + }; + render () { const { announcement } = this.props; return (
); } @@ -210,21 +238,9 @@ class Reaction extends ImmutablePureComponent { } return ( - - - - - - - + + + ); } diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx index e424568586..1794aa9a24 100644 --- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx +++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx @@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; +import { isFeatureEnabled } from 'flavours/glitch/utils/environment'; const messages = defineMessages({ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, @@ -62,10 +63,12 @@ class KeyboardShortcuts extends ImmutablePureComponent { b - - q - - + {isFeatureEnabled('outgoing_quotes') && ( + + q + + + )} d diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx index edfb82403c..9a4a791692 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/embedded_status.tsx @@ -76,6 +76,32 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ [clickCoordinatesRef, statusId, account, history], ); + const handleMouseEnter = useCallback>( + ({ currentTarget }) => { + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + for (const emoji of emojis) { + const newSrc = emoji.getAttribute('data-original'); + if (newSrc) emoji.src = newSrc; + } + }, + [], + ); + + const handleMouseLeave = useCallback>( + ({ currentTarget }) => { + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + for (const emoji of emojis) { + const newSrc = emoji.getAttribute('data-static'); + if (newSrc) emoji.src = newSrc; + } + }, + [], + ); + const handleContentWarningClick = useCallback(() => { dispatch(toggleStatusSpoilers(statusId)); }, [dispatch, statusId]); @@ -97,11 +123,13 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ return (
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx index 488916dc1e..28af52cd00 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -20,6 +20,7 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend import { IconButton } from '../../../components/icon_button'; import { Dropdown } from 'flavours/glitch/components/dropdown_menu'; import { me } from '../../../initial_state'; +import { isFeatureEnabled } from '@/flavours/glitch/utils/environment'; import { BoostButton } from '@/flavours/glitch/components/status/boost_button'; const messages = defineMessages({ @@ -198,7 +199,7 @@ class ActionBar extends PureComponent { } menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - if (!['private', 'direct'].includes(status.get('visibility'))) { + if (isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) { menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange }); } menu.push(null); diff --git a/app/javascript/flavours/glitch/features/status/components/refresh_controller.tsx b/app/javascript/flavours/glitch/features/status/components/refresh_controller.tsx index 1bf5b5b3ef..6af5428af0 100644 --- a/app/javascript/flavours/glitch/features/status/components/refresh_controller.tsx +++ b/app/javascript/flavours/glitch/features/status/components/refresh_controller.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from 'react'; -import { useIntl, defineMessages } from 'react-intl'; +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { fetchContext, @@ -8,80 +8,31 @@ import { } from 'flavours/glitch/actions/statuses'; import type { AsyncRefreshHeader } from 'flavours/glitch/api'; import { apiGetAsyncRefresh } from 'flavours/glitch/api/async_refreshes'; -import { Alert } from 'flavours/glitch/components/alert'; -import { ExitAnimationWrapper } from 'flavours/glitch/components/exit_animation_wrapper'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; -const AnimatedAlert: React.FC< - React.ComponentPropsWithoutRef & { withEntryDelay?: boolean } -> = ({ isActive = false, withEntryDelay, ...props }) => ( - - {(delayedIsActive) => } - -); - const messages = defineMessages({ - moreFound: { - id: 'status.context.more_replies_found', - defaultMessage: 'More replies found', - }, - show: { - id: 'status.context.show', - defaultMessage: 'Show', - }, - loadingInitial: { + loading: { id: 'status.context.loading', - defaultMessage: 'Loading', - }, - loadingMore: { - id: 'status.context.loading_more', - defaultMessage: 'Loading more replies', - }, - success: { - id: 'status.context.loading_success', - defaultMessage: 'All replies loaded', - }, - error: { - id: 'status.context.loading_error', - defaultMessage: "Couldn't load new replies", - }, - retry: { - id: 'status.context.retry', - defaultMessage: 'Retry', + defaultMessage: 'Checking for more replies', }, }); -type LoadingState = - | 'idle' - | 'more-available' - | 'loading-initial' - | 'loading-more' - | 'success' - | 'error'; - export const RefreshController: React.FC<{ statusId: string; }> = ({ statusId }) => { const refresh = useAppSelector( (state) => state.contexts.refreshing[statusId], ); - const currentReplyCount = useAppSelector( - (state) => state.contexts.replies[statusId]?.length ?? 0, + const autoRefresh = useAppSelector( + (state) => + !state.contexts.replies[statusId] || + state.contexts.replies[statusId].length === 0, ); - const autoRefresh = !currentReplyCount; const dispatch = useAppDispatch(); const intl = useIntl(); - - const [loadingState, setLoadingState] = useState( - refresh && autoRefresh ? 'loading-initial' : 'idle', - ); - - const [wasDismissed, setWasDismissed] = useState(false); - const dismissPrompt = useCallback(() => { - setWasDismissed(true); - setLoadingState('idle'); - }, []); + const [ready, setReady] = useState(false); + const [loading, setLoading] = useState(false); useEffect(() => { let timeoutId: ReturnType; @@ -94,104 +45,67 @@ export const RefreshController: React.FC<{ if (result.async_refresh.result_count > 0) { if (autoRefresh) { - void dispatch(fetchContext({ statusId })).then(() => { - setLoadingState('idle'); - }); - } else { - setLoadingState('more-available'); + void dispatch(fetchContext({ statusId })); + return ''; } - } else { - setLoadingState('idle'); + + setReady(true); } } else { scheduleRefresh(refresh); } + + return ''; }); }, refresh.retry * 1000); }; - if (refresh && !wasDismissed) { + if (refresh) { scheduleRefresh(refresh); - setLoadingState('loading-initial'); } return () => { clearTimeout(timeoutId); }; - }, [dispatch, statusId, refresh, autoRefresh, wasDismissed]); - - useEffect(() => { - // Hide success message after a short delay - if (loadingState === 'success') { - const timeoutId = setTimeout(() => { - setLoadingState('idle'); - }, 3000); - - return () => { - clearTimeout(timeoutId); - }; - } - return () => ''; - }, [loadingState]); + }, [dispatch, setReady, statusId, refresh, autoRefresh]); const handleClick = useCallback(() => { - setLoadingState('loading-more'); + setLoading(true); + setReady(false); dispatch(fetchContext({ statusId })) .then(() => { - setLoadingState('success'); + setLoading(false); return ''; }) .catch(() => { - setLoadingState('error'); + setLoading(false); }); - }, [dispatch, statusId]); + }, [dispatch, setReady, statusId]); - if (loadingState === 'loading-initial') { + if (ready && !loading) { return ( -
- -
+ ); } + if (!refresh && !loading) { + return null; + } + return ( -
- - - - +
+
); }; diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index d196dff11a..a5ef88057f 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -5,7 +5,6 @@ import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { withRouter } from 'react-router-dom'; -import { difference } from 'lodash'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -149,14 +148,9 @@ class Status extends ImmutablePureComponent { isExpanded: undefined, threadExpanded: undefined, statusId: undefined, - showMedia: undefined, loadedStatusId: undefined, + showMedia: undefined, revealBehindCW: undefined, - /** - * Holds the ids of newly added replies, excluding the initial load. - * Used to highlight newly added replies in the UI - */ - newRepliesIds: [], }; componentDidMount () { @@ -485,7 +479,6 @@ class Status extends ImmutablePureComponent { previousId={i > 0 ? list[i - 1] : undefined} nextId={list[i + 1] || (ancestors && statusId)} rootId={statusId} - shouldHighlightOnMount={this.state.newRepliesIds.includes(id)} /> )); } @@ -527,20 +520,11 @@ class Status extends ImmutablePureComponent { } componentDidUpdate (prevProps) { - const { status, ancestorsIds, descendantsIds } = this.props; + const { status, ancestorsIds } = this.props; if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) { this._scrollStatusIntoView(); } - - // Only highlight replies after the initial load - if (prevProps.descendantsIds.length) { - const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds); - - if (newRepliesIds.length) { - this.setState({newRepliesIds}); - } - } } componentWillUnmount () { @@ -678,8 +662,8 @@ class Status extends ImmutablePureComponent {
- {descendants} {remoteHint} + {descendants}
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index d38e570d3c..ce48b2fd1b 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -25,12 +25,11 @@ import { layoutFromWindow } from 'flavours/glitch/is_mobile'; import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; -import { handleAnimateGif } from '../emoji/handlers'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; +import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; @@ -393,11 +392,6 @@ class UI extends PureComponent { window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); - if (!autoPlayGif) { - window.addEventListener('mouseover', handleAnimateGif, { passive: true }); - window.addEventListener('mouseout', handleAnimateGif, { passive: true }); - } - document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); @@ -456,8 +450,6 @@ class UI extends PureComponent { window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('resize', this.handleResize); - window.removeEventListener('mouseover', handleAnimateGif); - window.removeEventListener('mouseout', handleAnimateGif); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 81e441a187..12c7042a17 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -163,7 +163,7 @@ $content-width: 840px; flex: 1 1 auto; } - @media screen and (max-width: ($content-width + $sidebar-width)) { + @media screen and (max-width: $content-width + $sidebar-width) { .sidebar-wrapper--empty { display: none; } @@ -1086,17 +1086,6 @@ a.name-tag, } } - &__action-bar { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; - - &:not(.no-wrap) { - flex-wrap: wrap; - } - } - &__meta { padding: 0 15px; color: $dark-text-color; @@ -1113,8 +1102,10 @@ a.name-tag, } } - &__actions { - margin-inline-start: auto; + &__action-bar { + display: flex; + justify-content: space-between; + align-items: center; } &__permissions { diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 72c11300e6..17c178850b 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -1657,16 +1657,6 @@ } } } - - .no-reduce-motion &--highlighted-entry::before { - content: ''; - position: absolute; - inset: 0; - background: rgb(from $ui-highlight-color r g b / 20%); - opacity: 0; - animation: fade 0.7s reverse both 0.3s; - pointer-events: none; - } } .status__relative-time { @@ -3035,6 +3025,7 @@ a.account__display-name { flex: 1 1 auto; flex-direction: row; justify-content: flex-start; + overflow-x: auto; position: relative; &.unscrollable { @@ -3210,29 +3201,6 @@ a.account__display-name { } } -.column__alert { - position: sticky; - bottom: 1rem; - z-index: 10; - box-sizing: border-box; - display: grid; - width: 100%; - max-width: 360px; - padding-inline: 10px; - margin-top: 1rem; - margin-inline: auto; - - @media (max-width: #{$mobile-menu-breakpoint - 1}) { - bottom: 4rem; - } - - & > * { - // Make all nested alerts occupy the same space - // rather than stack - grid-area: 1 / 1; - } -} - .ui { --mobile-bottom-nav-height: 55px; --last-content-item-border-width: 2px; @@ -3273,6 +3241,7 @@ a.account__display-name { .column, .drawer { flex: 1 1 100%; + overflow: hidden; } @media screen and (width > $mobile-breakpoint) { @@ -10728,21 +10697,6 @@ noscript { } } -.notification-bar__loading-indicator { - --spinner-size: 22px; - - position: relative; - height: var(--spinner-size); - width: var(--spinner-size); - margin-inline-start: 2px; - - svg { - color: $white; - height: var(--spinner-size); - width: var(--spinner-size); - } -} - .hashtag-header { border-bottom: 1px solid var(--background-border-color); padding: 15px; diff --git a/app/javascript/flavours/glitch/utils/environment.ts b/app/javascript/flavours/glitch/utils/environment.ts index 2d544417e3..fc4448740f 100644 --- a/app/javascript/flavours/glitch/utils/environment.ts +++ b/app/javascript/flavours/glitch/utils/environment.ts @@ -12,7 +12,11 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures'; +export type Features = + | 'modern_emojis' + | 'outgoing_quotes' + | 'fasp' + | 'http_message_signatures'; export function isFeatureEnabled(feature: Features) { return initialState?.features.includes(feature) ?? false; diff --git a/app/javascript/mastodon/components/alert/alert.stories.tsx b/app/javascript/mastodon/components/alert/alert.stories.tsx index f12f06751d..4d5f8acb65 100644 --- a/app/javascript/mastodon/components/alert/alert.stories.tsx +++ b/app/javascript/mastodon/components/alert/alert.stories.tsx @@ -8,7 +8,6 @@ const meta = { component: Alert, args: { isActive: true, - isLoading: false, animateFrom: 'side', title: '', message: '', @@ -21,12 +20,6 @@ const meta = { type: 'boolean', description: 'Animate to the active (displayed) state of the alert', }, - isLoading: { - control: 'boolean', - type: 'boolean', - description: - 'Display a loading indicator in the alert, replacing the dismiss button if present', - }, animateFrom: { control: 'radio', type: 'string', @@ -115,11 +108,3 @@ export const InSizedContainer: Story = {
), }; - -export const WithLoadingIndicator: Story = { - args: { - ...WithDismissButton.args, - isLoading: true, - }, - render: InSizedContainer.render, -}; diff --git a/app/javascript/mastodon/components/alert/index.tsx b/app/javascript/mastodon/components/alert/index.tsx index 72fee0a4a3..1009e77524 100644 --- a/app/javascript/mastodon/components/alert/index.tsx +++ b/app/javascript/mastodon/components/alert/index.tsx @@ -3,7 +3,6 @@ import { useIntl } from 'react-intl'; import classNames from 'classnames'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { IconButton } from '../icon_button'; @@ -11,23 +10,21 @@ import { IconButton } from '../icon_button'; * Snackbar/Toast-style notification component. */ export const Alert: React.FC<{ + isActive?: boolean; + animateFrom?: 'side' | 'below'; title?: string; message: string; action?: string; onActionClick?: () => void; onDismiss?: () => void; - isActive?: boolean; - isLoading?: boolean; - animateFrom?: 'side' | 'below'; }> = ({ + isActive, + animateFrom = 'side', title, message, action, onActionClick, onDismiss, - isActive, - isLoading, - animateFrom = 'side', }) => { const intl = useIntl(); @@ -54,13 +51,7 @@ export const Alert: React.FC<{ )} - {isLoading && ( - - - - )} - - {onDismiss && !isLoading && ( + {onDismiss && ( > = ({ account, className, children, ...props }) => { return ( - + {account ? ( React.ReactNode; -}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => { - const [delayedIsActive, setDelayedIsActive] = useState(false); - - useEffect(() => { - if (isActive && !withEntryDelay) { - setDelayedIsActive(true); - - return () => ''; - } else { - const timeout = setTimeout(() => { - setDelayedIsActive(isActive); - }, delayMs); - - return () => { - clearTimeout(timeout); - }; - } - }, [isActive, delayMs, withEntryDelay]); - - if (!isActive && !delayedIsActive) { - return null; - } - - return children(isActive && delayedIsActive); -}; diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 196da7c99a..8664320abe 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -118,7 +118,6 @@ class Status extends ImmutablePureComponent { unread: PropTypes.bool, showThread: PropTypes.bool, isQuotedPost: PropTypes.bool, - shouldHighlightOnMount: PropTypes.bool, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, cacheMediaWidth: PropTypes.func, @@ -568,7 +567,6 @@ class Status extends ImmutablePureComponent { 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'status--is-quote': isQuotedPost, 'status--has-quote': !!status.get('quote'), - 'status--highlighted-entry': this.props.shouldHighlightOnMount, }) } data-id={status.get('id')} diff --git a/app/javascript/mastodon/components/status/boost_button.stories.tsx b/app/javascript/mastodon/components/status/boost_button.stories.tsx index 402695a829..e81d334a93 100644 --- a/app/javascript/mastodon/components/status/boost_button.stories.tsx +++ b/app/javascript/mastodon/components/status/boost_button.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import type { StatusVisibility } from '@/mastodon/api_types/statuses'; import { statusFactoryState } from '@/testing/factories'; -import { BoostButton } from './boost_button'; +import { LegacyReblogButton, StatusBoostButton } from './boost_button'; interface StoryProps { visibility: StatusVisibility; @@ -38,7 +38,10 @@ const meta = { }, }, render: (args) => ( - 0} /> + 0} + /> ), } satisfies Meta; @@ -75,3 +78,12 @@ export const Mine: Story = { }, }, }; + +export const Legacy: Story = { + render: (args) => ( + 0} + /> + ), +}; diff --git a/app/javascript/mastodon/components/status/boost_button.tsx b/app/javascript/mastodon/components/status/boost_button.tsx index 337eca5071..49bdc953e1 100644 --- a/app/javascript/mastodon/components/status/boost_button.tsx +++ b/app/javascript/mastodon/components/status/boost_button.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import type { FC, KeyboardEvent, MouseEvent } from 'react'; +import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler } from 'react'; import { useIntl } from 'react-intl'; @@ -11,6 +11,7 @@ import { openModal } from '@/mastodon/actions/modal'; import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu'; import type { Status } from '@/mastodon/models/status'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { isFeatureEnabled } from '@/mastodon/utils/environment'; import type { SomeRequired } from '@/mastodon/utils/types'; import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu'; @@ -46,7 +47,10 @@ interface ReblogButtonProps { type ActionMenuItemWithIcon = SomeRequired; -export const BoostButton: FC = ({ status, counters }) => { +export const StatusBoostButton: FC = ({ + status, + counters, +}) => { const intl = useIntl(); const dispatch = useAppDispatch(); const statusState = useAppSelector((state) => @@ -188,3 +192,65 @@ const ReblogMenuItem: FC = ({ ); }; + +// Legacy helpers + +// Switch between the legacy and new reblog button based on feature flag. +export const BoostButton: FC = (props) => { + if (isFeatureEnabled('outgoing_quotes')) { + return ; + } + return ; +}; + +export const LegacyReblogButton: FC = ({ + status, + counters, +}) => { + const intl = useIntl(); + const statusState = useAppSelector((state) => + selectStatusState(state, status), + ); + + const { title, meta, iconComponent, disabled } = useMemo( + () => boostItemState(statusState), + [statusState], + ); + + const dispatch = useAppDispatch(); + const handleClick: MouseEventHandler = useCallback( + (event) => { + if (statusState.isLoggedIn) { + dispatch(toggleReblog(status.get('id') as string, event.shiftKey)); + } else { + dispatch( + openModal({ + modalType: 'INTERACTION', + modalProps: { + accountId: status.getIn(['account', 'id']), + url: status.get('uri'), + }, + }), + ); + } + }, + [dispatch, status, statusState.isLoggedIn], + ); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/status_action_bar/index.jsx b/app/javascript/mastodon/components/status_action_bar/index.jsx index 3e82912ab1..0e72a8cefe 100644 --- a/app/javascript/mastodon/components/status_action_bar/index.jsx +++ b/app/javascript/mastodon/components/status_action_bar/index.jsx @@ -23,6 +23,7 @@ import { Dropdown } from 'mastodon/components/dropdown_menu'; import { me } from '../../initial_state'; import { IconButton } from '../icon_button'; +import { isFeatureEnabled } from '../../utils/environment'; import { BoostButton } from '../status/boost_button'; import { RemoveQuoteHint } from './remove_quote_hint'; @@ -280,7 +281,7 @@ class StatusActionBar extends ImmutablePureComponent { if (writtenByMe || withDismiss) { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - if (writtenByMe && !['private', 'direct'].includes(status.get('visibility'))) { + if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) { menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange }); } menu.push(null); diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index af0059c7d6..5f0f7079ae 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -140,6 +140,32 @@ class StatusContent extends PureComponent { } } + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } + }; + + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + }; + componentDidMount () { this._updateStatusLinks(); } @@ -231,13 +257,7 @@ class StatusContent extends PureComponent { if (this.props.onClick) { return ( <> -
+
+
{ const getStatus = makeGetStatus(); const getPictureInPicture = makeGetPictureInPicture(); @@ -79,7 +81,9 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ }, onQuote (status) { - dispatch(quoteComposeById(status.get('id'))); + if (isFeatureEnabled('outgoing_quotes')) { + dispatch(quoteComposeById(status.get('id'))); + } }, onFavourite (status) { diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index f58f1f4a8c..ed6d9cb83e 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -379,6 +379,36 @@ export const AccountHeader: React.FC<{ }); }, [account]); + const handleMouseEnter = useCallback( + ({ currentTarget }: React.MouseEvent) => { + if (autoPlayGif) { + return; + } + + currentTarget + .querySelectorAll('.custom-emoji') + .forEach((emoji) => { + emoji.src = emoji.getAttribute('data-original') ?? ''; + }); + }, + [], + ); + + const handleMouseLeave = useCallback( + ({ currentTarget }: React.MouseEvent) => { + if (autoPlayGif) { + return; + } + + currentTarget + .querySelectorAll('.custom-emoji') + .forEach((emoji) => { + emoji.src = emoji.getAttribute('data-static') ?? ''; + }); + }, + [], + ); + const suspended = account?.suspended; const isRemote = account?.acct !== account?.username; const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; @@ -778,9 +808,11 @@ export const AccountHeader: React.FC<{ )}
{!(suspended || hidden || account.moved) && relationship?.requested_by && ( diff --git a/app/javascript/mastodon/features/compose/components/visibility_button.tsx b/app/javascript/mastodon/features/compose/components/visibility_button.tsx index 1ea504ab1a..fadb896b5e 100644 --- a/app/javascript/mastodon/features/compose/components/visibility_button.tsx +++ b/app/javascript/mastodon/features/compose/components/visibility_button.tsx @@ -12,12 +12,14 @@ import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes'; import type { StatusVisibility } from '@/mastodon/api_types/statuses'; import { Icon } from '@/mastodon/components/icon'; import { useAppSelector, useAppDispatch } from '@/mastodon/store'; +import { isFeatureEnabled } from '@/mastodon/utils/environment'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import type { VisibilityModalCallback } from '../../ui/components/visibility_modal'; +import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import { messages as privacyMessages } from './privacy_dropdown'; @@ -41,6 +43,9 @@ interface PrivacyDropdownProps { } export const VisibilityButton: FC = (props) => { + if (!isFeatureEnabled('outgoing_quotes')) { + return ; + } return ; }; diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index bb0815087b..9aae588bcc 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -23,6 +23,7 @@ import { IconButton } from 'mastodon/components/icon_button'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import StatusContent from 'mastodon/components/status_content'; import { Dropdown } from 'mastodon/components/dropdown_menu'; +import { autoPlayGif } from 'mastodon/initial_state'; import { makeGetStatus } from 'mastodon/selectors'; import { LinkedDisplayName } from '@/mastodon/components/display_name'; @@ -56,6 +57,32 @@ export const Conversation = ({ conversation, scrollKey }) => { const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId })); const accounts = useSelector(state => getAccounts(state, accountIds)); + const handleMouseEnter = useCallback(({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } + }, []); + + const handleMouseLeave = useCallback(({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + }, []); + const handleClick = useCallback(() => { if (unread) { dispatch(markConversationRead(id)); @@ -136,7 +163,7 @@ export const Conversation = ({ conversation, scrollKey }) => { {unread && }
-
+
{names} }} />
diff --git a/app/javascript/mastodon/features/directory/components/account_card.tsx b/app/javascript/mastodon/features/directory/components/account_card.tsx index 9d317efd43..2a0470bb72 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.tsx +++ b/app/javascript/mastodon/features/directory/components/account_card.tsx @@ -1,3 +1,4 @@ +import type { MouseEventHandler } from 'react'; import { useCallback } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; @@ -43,6 +44,39 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { const account = useAppSelector((s) => getAccount(s, accountId)); const dispatch = useAppDispatch(); + const handleMouseEnter = useCallback( + ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + emojis.forEach((emoji) => { + const original = emoji.getAttribute('data-original'); + if (original) emoji.src = original; + }); + }, + [], + ); + + const handleMouseLeave = useCallback( + ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + emojis.forEach((emoji) => { + const staticUrl = emoji.getAttribute('data-static'); + if (staticUrl) emoji.src = staticUrl; + }); + }, + [], + ); + const handleFollow = useCallback(() => { if (!account) return; @@ -151,7 +185,9 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { {account.get('note').length > 0 && (
)} diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx index 08d62b2c37..e143c9fc16 100644 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_html.tsx @@ -1,7 +1,5 @@ import type { ComponentPropsWithoutRef, ElementType } from 'react'; -import classNames from 'classnames'; - import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { useEmojify } from './hooks'; @@ -9,13 +7,12 @@ import type { CustomEmojiMapArg } from './types'; type EmojiHTMLProps = Omit< ComponentPropsWithoutRef, - 'dangerouslySetInnerHTML' | 'className' + 'dangerouslySetInnerHTML' > & { htmlString: string; extraEmojis?: CustomEmojiMapArg; as?: Element; shallow?: boolean; - className?: string; }; export const ModernEmojiHTML = ({ @@ -23,7 +20,6 @@ export const ModernEmojiHTML = ({ htmlString, as: Wrapper = 'div', // Rename for syntax highlighting shallow, - className = '', ...props }: EmojiHTMLProps) => { const emojifiedHtml = useEmojify({ @@ -37,11 +33,7 @@ export const ModernEmojiHTML = ({ } return ( - + ); }; @@ -51,13 +43,7 @@ export const EmojiHTML = ( if (isModernEmojiEnabled()) { return ; } - const { as: asElement, htmlString, extraEmojis, className, ...rest } = props; + const { as: asElement, htmlString, extraEmojis, ...rest } = props; const Wrapper = asElement ?? 'div'; - return ( - - ); + return ; }; diff --git a/app/javascript/mastodon/features/emoji/handlers.ts b/app/javascript/mastodon/features/emoji/handlers.ts deleted file mode 100644 index 3b02028f3c..0000000000 --- a/app/javascript/mastodon/features/emoji/handlers.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { autoPlayGif } from '@/mastodon/initial_state'; - -const PARENT_MAX_DEPTH = 10; - -export function handleAnimateGif(event: MouseEvent) { - // We already check this in ui/index.jsx, but just to be sure. - if (autoPlayGif) { - return; - } - - const { target, type } = event; - const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate. - - if (target instanceof HTMLImageElement) { - setAnimateGif(target, animate); - } else if (!(target instanceof HTMLElement) || target === document.body) { - return; - } - - let parent: HTMLElement | null = null; - let iter = 0; - - if (target.classList.contains('animate-parent')) { - parent = target; - } else { - // Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'. - let current: HTMLElement | null = target; - while (current) { - if (iter >= PARENT_MAX_DEPTH) { - return; // We can just exit right now. - } - current = current.parentElement; - if (current?.classList.contains('animate-parent')) { - parent = current; - break; - } - iter++; - } - } - - // Affect all animated children within the parent. - if (parent) { - const animatedChildren = - parent.querySelectorAll('img.custom-emoji'); - for (const child of animatedChildren) { - setAnimateGif(child, animate); - } - } -} - -function setAnimateGif(image: HTMLImageElement, animate: boolean) { - const { classList, dataset } = image; - if ( - !classList.contains('custom-emoji') || - !dataset.static || - !dataset.original - ) { - return; - } - image.src = animate ? dataset.original : dataset.static; -} diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.jsx b/app/javascript/mastodon/features/getting_started/components/announcements.jsx index 96bd995d2b..87d7e2a3be 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.jsx +++ b/app/javascript/mastodon/features/getting_started/components/announcements.jsx @@ -111,14 +111,42 @@ class ContentWithRouter extends ImmutablePureComponent { } }; + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } + }; + + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + }; + render () { const { announcement } = this.props; return (
); } @@ -210,21 +238,9 @@ class Reaction extends ImmutablePureComponent { } return ( - - - - - - - + + + ); } diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx b/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx index 01a4f0e1fd..a1f14f14f6 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx @@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; +import { isFeatureEnabled } from 'mastodon/utils/environment'; const messages = defineMessages({ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, @@ -62,10 +63,12 @@ class KeyboardShortcuts extends ImmutablePureComponent { b - - q - - + {isFeatureEnabled('outgoing_quotes') && ( + + q + + + )} enter, o diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx index a17425169b..f63d42f826 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx @@ -76,6 +76,32 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ [clickCoordinatesRef, statusId, account, history], ); + const handleMouseEnter = useCallback>( + ({ currentTarget }) => { + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + for (const emoji of emojis) { + const newSrc = emoji.getAttribute('data-original'); + if (newSrc) emoji.src = newSrc; + } + }, + [], + ); + + const handleMouseLeave = useCallback>( + ({ currentTarget }) => { + const emojis = + currentTarget.querySelectorAll('.custom-emoji'); + + for (const emoji of emojis) { + const newSrc = emoji.getAttribute('data-static'); + if (newSrc) emoji.src = newSrc; + } + }, + [], + ); + const handleContentWarningClick = useCallback(() => { dispatch(toggleStatusSpoilers(statusId)); }, [dispatch, statusId]); @@ -97,11 +123,13 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ return (
diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 6156cf1916..fa9d6497ae 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -19,6 +19,7 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/ import { IconButton } from '../../../components/icon_button'; import { Dropdown } from 'mastodon/components/dropdown_menu'; import { me } from '../../../initial_state'; +import { isFeatureEnabled } from '@/mastodon/utils/environment'; import { BoostButton } from '@/mastodon/components/status/boost_button'; const messages = defineMessages({ @@ -236,7 +237,7 @@ class ActionBar extends PureComponent { } menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - if (!['private', 'direct'].includes(status.get('visibility'))) { + if (isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) { menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange }); } menu.push(null); diff --git a/app/javascript/mastodon/features/status/components/refresh_controller.tsx b/app/javascript/mastodon/features/status/components/refresh_controller.tsx index 34faaf1d5d..9788b2849f 100644 --- a/app/javascript/mastodon/features/status/components/refresh_controller.tsx +++ b/app/javascript/mastodon/features/status/components/refresh_controller.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from 'react'; -import { useIntl, defineMessages } from 'react-intl'; +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { fetchContext, @@ -8,80 +8,31 @@ import { } from 'mastodon/actions/statuses'; import type { AsyncRefreshHeader } from 'mastodon/api'; import { apiGetAsyncRefresh } from 'mastodon/api/async_refreshes'; -import { Alert } from 'mastodon/components/alert'; -import { ExitAnimationWrapper } from 'mastodon/components/exit_animation_wrapper'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; -const AnimatedAlert: React.FC< - React.ComponentPropsWithoutRef & { withEntryDelay?: boolean } -> = ({ isActive = false, withEntryDelay, ...props }) => ( - - {(delayedIsActive) => } - -); - const messages = defineMessages({ - moreFound: { - id: 'status.context.more_replies_found', - defaultMessage: 'More replies found', - }, - show: { - id: 'status.context.show', - defaultMessage: 'Show', - }, - loadingInitial: { + loading: { id: 'status.context.loading', - defaultMessage: 'Loading', - }, - loadingMore: { - id: 'status.context.loading_more', - defaultMessage: 'Loading more replies', - }, - success: { - id: 'status.context.loading_success', - defaultMessage: 'All replies loaded', - }, - error: { - id: 'status.context.loading_error', - defaultMessage: "Couldn't load new replies", - }, - retry: { - id: 'status.context.retry', - defaultMessage: 'Retry', + defaultMessage: 'Checking for more replies', }, }); -type LoadingState = - | 'idle' - | 'more-available' - | 'loading-initial' - | 'loading-more' - | 'success' - | 'error'; - export const RefreshController: React.FC<{ statusId: string; }> = ({ statusId }) => { const refresh = useAppSelector( (state) => state.contexts.refreshing[statusId], ); - const currentReplyCount = useAppSelector( - (state) => state.contexts.replies[statusId]?.length ?? 0, + const autoRefresh = useAppSelector( + (state) => + !state.contexts.replies[statusId] || + state.contexts.replies[statusId].length === 0, ); - const autoRefresh = !currentReplyCount; const dispatch = useAppDispatch(); const intl = useIntl(); - - const [loadingState, setLoadingState] = useState( - refresh && autoRefresh ? 'loading-initial' : 'idle', - ); - - const [wasDismissed, setWasDismissed] = useState(false); - const dismissPrompt = useCallback(() => { - setWasDismissed(true); - setLoadingState('idle'); - }, []); + const [ready, setReady] = useState(false); + const [loading, setLoading] = useState(false); useEffect(() => { let timeoutId: ReturnType; @@ -94,104 +45,67 @@ export const RefreshController: React.FC<{ if (result.async_refresh.result_count > 0) { if (autoRefresh) { - void dispatch(fetchContext({ statusId })).then(() => { - setLoadingState('idle'); - }); - } else { - setLoadingState('more-available'); + void dispatch(fetchContext({ statusId })); + return ''; } - } else { - setLoadingState('idle'); + + setReady(true); } } else { scheduleRefresh(refresh); } + + return ''; }); }, refresh.retry * 1000); }; - if (refresh && !wasDismissed) { + if (refresh) { scheduleRefresh(refresh); - setLoadingState('loading-initial'); } return () => { clearTimeout(timeoutId); }; - }, [dispatch, statusId, refresh, autoRefresh, wasDismissed]); - - useEffect(() => { - // Hide success message after a short delay - if (loadingState === 'success') { - const timeoutId = setTimeout(() => { - setLoadingState('idle'); - }, 3000); - - return () => { - clearTimeout(timeoutId); - }; - } - return () => ''; - }, [loadingState]); + }, [dispatch, setReady, statusId, refresh, autoRefresh]); const handleClick = useCallback(() => { - setLoadingState('loading-more'); + setLoading(true); + setReady(false); dispatch(fetchContext({ statusId })) .then(() => { - setLoadingState('success'); + setLoading(false); return ''; }) .catch(() => { - setLoadingState('error'); + setLoading(false); }); - }, [dispatch, statusId]); + }, [dispatch, setReady, statusId]); - if (loadingState === 'loading-initial') { + if (ready && !loading) { return ( -
- -
+ ); } + if (!refresh && !loading) { + return null; + } + return ( -
- - - - +
+
); }; diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 404faf609e..8bab174f67 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -5,7 +5,6 @@ import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { withRouter } from 'react-router-dom'; -import { difference } from 'lodash'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -151,11 +150,6 @@ class Status extends ImmutablePureComponent { fullscreen: false, showMedia: defaultMediaVisibility(this.props.status), loadedStatusId: undefined, - /** - * Holds the ids of newly added replies, excluding the initial load. - * Used to highlight newly added replies in the UI - */ - newRepliesIds: [], }; UNSAFE_componentWillMount () { @@ -468,7 +462,6 @@ class Status extends ImmutablePureComponent { previousId={i > 0 ? list[i - 1] : undefined} nextId={list[i + 1] || (ancestors && statusId)} rootId={statusId} - shouldHighlightOnMount={this.state.newRepliesIds.includes(id)} /> )); } @@ -502,20 +495,11 @@ class Status extends ImmutablePureComponent { } componentDidUpdate (prevProps) { - const { status, ancestorsIds, descendantsIds } = this.props; + const { status, ancestorsIds } = this.props; if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) { this._scrollStatusIntoView(); } - - // Only highlight replies after the initial load - if (prevProps.descendantsIds.length) { - const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds); - - if (newRepliesIds.length) { - this.setState({newRepliesIds}); - } - } } componentWillUnmount () { @@ -648,8 +632,8 @@ class Status extends ImmutablePureComponent {
- {descendants} {remoteHint} + {descendants}
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index efec38caf4..0583bf99c5 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -22,12 +22,11 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; -import { handleAnimateGif } from '../emoji/handlers'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; +import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; @@ -380,11 +379,6 @@ class UI extends PureComponent { window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); - if (!autoPlayGif) { - window.addEventListener('mouseover', handleAnimateGif, { passive: true }); - window.addEventListener('mouseout', handleAnimateGif, { passive: true }); - } - document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); @@ -410,8 +404,6 @@ class UI extends PureComponent { window.removeEventListener('blur', this.handleWindowBlur); window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('resize', this.handleResize); - window.removeEventListener('mouseover', handleAnimateGif); - window.removeEventListener('mouseout', handleAnimateGif); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index d1aaffe2f2..f08c0babbd 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Zrušit boostnutí", "status.cannot_quote": "Nemáte oprávnění citovat tento příspěvek", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", - "status.contains_quote": "Obsahuje citaci", "status.context.load_new_replies": "K dispozici jsou nové odpovědi", "status.context.loading": "Hledání dalších odpovědí", "status.continued_thread": "Pokračuje ve vlákně", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Příspěvek odstraněn autorem", "status.quote_followers_only": "Pouze moji sledující mohou citovat tento příspěvek", "status.quote_manual_review": "Autor provede manuální kontrolu", - "status.quote_noun": "Citace", "status.quote_policy_change": "Změňte, kdo může citovat", "status.quote_post_author": "Citovali příspěvek od @{name}", "status.quote_private": "Soukromé příspěvky nelze citovat", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 6d8129e703..4c24eaa387 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Dadhybu", "status.cannot_quote": "Does dim caniatâd i chi ddyfynnu'r postiad hwn", "status.cannot_reblog": "Does dim modd hybu'r postiad hwn", - "status.contains_quote": "Yn cynnwys dyfyniad", "status.context.load_new_replies": "Mae atebion newydd ar gael", "status.context.loading": "Yn chwilio am fwy o atebion", "status.continued_thread": "Edefyn parhaus", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Postiad wedi'i ddileu gan yr awdur", "status.quote_followers_only": "Dim ond dilynwyr all ddyfynnu'r postiad hwn", "status.quote_manual_review": "Bydd yr awdur yn ei adolygu ei hyn", - "status.quote_noun": "Dyfynnu", "status.quote_policy_change": "Newid pwy all ddyfynnu", "status.quote_post_author": "Wedi dyfynnu postiad gan @{name}", "status.quote_private": "Does dim modd dyfynnu postiadau preifat", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 9521401bca..8fd1711358 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Fjern fremhævelse", "status.cannot_quote": "Du har ikke tilladelse til at citere dette indlæg", "status.cannot_reblog": "Dette indlæg kan ikke fremhæves", - "status.contains_quote": "Indeholder citat", "status.context.load_new_replies": "Nye svar tilgængelige", "status.context.loading": "Tjekker for flere svar", "status.continued_thread": "Fortsat tråd", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Indlæg fjernet af forfatter", "status.quote_followers_only": "Kun følgere kan citere dette indlæg", "status.quote_manual_review": "Forfatter vil manuelt gennemgå", - "status.quote_noun": "Citat", "status.quote_policy_change": "Ændr hvem der kan citere", "status.quote_post_author": "Citerede et indlæg fra @{name}", "status.quote_private": "Private indlæg kan ikke citeres", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 7ceb4509dc..0e5fcbd9ff 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Beitrag nicht mehr teilen", "status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", - "status.contains_quote": "Enthält Zitat", "status.context.load_new_replies": "Neue Antworten verfügbar", "status.context.loading": "Weitere Antworten werden abgerufen", "status.continued_thread": "Fortgeführter Thread", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Beitrag durch Autor*in entfernt", "status.quote_followers_only": "Nur Follower können diesen Beitrag zitieren", "status.quote_manual_review": "Zitierte*r überprüft manuell", - "status.quote_noun": "Zitat", "status.quote_policy_change": "Ändern, wer zitieren darf", "status.quote_post_author": "Zitierte einen Beitrag von @{name}", "status.quote_private": "Private Beiträge können nicht zitiert werden", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index feefaabeea..e2bec9981f 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -736,7 +736,7 @@ "privacy.private.long": "Μόνο οι ακόλουθοί σας", "privacy.private.short": "Ακόλουθοι", "privacy.public.long": "Όλοι εντός και εκτός του Mastodon", - "privacy.public.short": "Δημόσια", + "privacy.public.short": "Δημόσιο", "privacy.quote.anyone": "{visibility}, ο καθένας μπορεί να παραθέσει", "privacy.quote.disabled": "{visibility}, παραθέσεις απενεργοποιημένες", "privacy.quote.limited": "{visibility}, παραθέσεις περιορισμένες", @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Ακύρωση ενίσχυσης", "status.cannot_quote": "Δε σας επιτρέπετε να παραθέσετε αυτή την ανάρτηση", "status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί", - "status.contains_quote": "Περιέχει παράθεση", "status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες", "status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις", "status.continued_thread": "Συνεχιζόμενο νήματος", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Η ανάρτηση αφαιρέθηκε από τον συντάκτη", "status.quote_followers_only": "Μόνο οι ακόλουθοι μπορούν να παραθέσουν αυτή την ανάρτηση", "status.quote_manual_review": "Ο συντάκτης θα επανεξετάσει χειροκίνητα", - "status.quote_noun": "Παράθεση", "status.quote_policy_change": "Αλλάξτε ποιός μπορεί να κάνει παράθεση", "status.quote_post_author": "Παρατίθεται μια ανάρτηση από @{name}", "status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f949c30339..7721cc36d3 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -865,13 +865,8 @@ "status.cannot_quote": "You are not allowed to quote this post", "status.cannot_reblog": "This post cannot be boosted", "status.contains_quote": "Contains quote", - "status.context.loading": "Loading more replies", - "status.context.loading_error": "Couldn't load new replies", - "status.context.loading_more": "Loading more replies", - "status.context.loading_success": "All replies loaded", - "status.context.more_replies_found": "More replies found", - "status.context.retry": "Retry", - "status.context.show": "Show", + "status.context.load_new_replies": "New replies available", + "status.context.loading": "Checking for more replies", "status.continued_thread": "Continued thread", "status.copy": "Copy link to post", "status.delete": "Delete", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 3f582452d5..672cad81aa 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Quitar adhesión", "status.cannot_quote": "No te es permitido citar este mensaje", "status.cannot_reblog": "No se puede adherir a este mensaje", - "status.contains_quote": "Contiene cita", "status.context.load_new_replies": "Hay nuevas respuestas", "status.context.loading": "Buscando más respuestas", "status.continued_thread": "Continuación de hilo", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Mensaje eliminado por el autor", "status.quote_followers_only": "Solo los seguidores pueden citar este mensaje", "status.quote_manual_review": "El autor revisará manualmente", - "status.quote_noun": "Cita", "status.quote_policy_change": "Cambiá quién puede citar", "status.quote_post_author": "Se citó un mensaje de @{name}", "status.quote_private": "No se pueden citar los mensajes privados", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 6b48c21d36..ff1a75b4cc 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Deshacer impulso", "status.cannot_quote": "No está permitido citar esta publicación", "status.cannot_reblog": "Esta publicación no puede ser impulsada", - "status.contains_quote": "Contiene cita", "status.context.load_new_replies": "Nuevas respuestas disponibles", "status.context.loading": "Comprobando si hay más respuestas", "status.continued_thread": "Hilo continuado", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Publicación eliminada por el autor", "status.quote_followers_only": "Solo los seguidores pueden citar esta publicación", "status.quote_manual_review": "El autor la revisará manualmente", - "status.quote_noun": "Cita", "status.quote_policy_change": "Cambia quién puede citarte", "status.quote_post_author": "Ha citado una publicación de @{name}", "status.quote_private": "Las publicaciones privadas no pueden citarse", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index f84cb41511..e4f2bb0b44 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Deshacer impulso", "status.cannot_quote": "No tienes permiso para citar esta publicación", "status.cannot_reblog": "Esta publicación no se puede impulsar", - "status.contains_quote": "Contiene cita", "status.context.load_new_replies": "Hay nuevas respuestas", "status.context.loading": "Buscando más respuestas", "status.continued_thread": "Continuó el hilo", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Publicación eliminada por el autor", "status.quote_followers_only": "Solo los seguidores pueden citar esta publicación", "status.quote_manual_review": "El autor revisará manualmente", - "status.quote_noun": "Cita", "status.quote_policy_change": "Cambia quién puede citarte", "status.quote_post_author": "Ha citado una publicación de @{name}", "status.quote_private": "Las publicaciones privadas no pueden ser citadas", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 4aaeb51421..791736f131 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Lõpeta jagamine", "status.cannot_quote": "Sul pole õigust seda postitust tsiteerida", "status.cannot_reblog": "Seda postitust ei saa jagada", - "status.contains_quote": "Sisaldab tsitaati", "status.context.load_new_replies": "Leidub uusi vastuseid", "status.context.loading": "Kontrollin täiendavate vastuste olemasolu", "status.continued_thread": "Jätkatud lõim", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Autor on postituse eemaldanud", "status.quote_followers_only": "Vaid jälgijad saavad seda postitust tsiteerida", "status.quote_manual_review": "Autor vaatab selle üle", - "status.quote_noun": "Tsitaat", "status.quote_policy_change": "Muuda neid, kes võivad tsiteerida", "status.quote_post_author": "Tsiteeris kasutaja @{name} postitust", "status.quote_private": "Otsepostituste tsiteerimine pole võimalik", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 8bf69ab31d..2925044cc5 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Peru tehostus", "status.cannot_quote": "Sinulla ei ole oikeutta lainata tätä julkaisua", "status.cannot_reblog": "Tätä julkaisua ei voi tehostaa", - "status.contains_quote": "Sisältää lainauksen", "status.context.load_new_replies": "Uusia vastauksia saatavilla", "status.context.loading": "Tarkistetaan lisävastauksia", "status.continued_thread": "Jatkoi ketjua", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Tekijä on poistanut julkaisun", "status.quote_followers_only": "Vain seuraajat voivat lainata tätä julkaisua", "status.quote_manual_review": "Tekijä arvioi pyynnön manuaalisesti", - "status.quote_noun": "Lainaus", "status.quote_policy_change": "Vaihda, kuka voi lainata", "status.quote_post_author": "Lainaa käyttäjän @{name} julkaisua", "status.quote_private": "Yksityisiä julkaisuja ei voi lainata", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index a8db9baeca..dc4cbc694a 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Strika stimbran", "status.cannot_quote": "Tú hevur ikki loyvi at sitera hendan postin", "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin", - "status.contains_quote": "Inniheldur sitat", "status.context.load_new_replies": "Nýggj svar tøk", "status.context.loading": "Kanni um tað eru fleiri svar", "status.continued_thread": "Framhaldandi tráður", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Posturin burturbeindur av høvundinum", "status.quote_followers_only": "Bara fylgjarar kunnu sitera hendan postin", "status.quote_manual_review": "Høvundurin fer at eftirkanna manuelt", - "status.quote_noun": "Sitat", "status.quote_policy_change": "Broyt hvør kann sitera", "status.quote_post_author": "Siteraði ein post hjá @{name}", "status.quote_private": "Privatir postar kunnu ikki siterast", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 4ede71c487..f1641450df 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Dímhol", "status.cannot_quote": "Ní cheadaítear duit an post seo a lua", "status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", - "status.contains_quote": "Tá luachan ann", "status.context.load_new_replies": "Freagraí nua ar fáil", "status.context.loading": "Ag seiceáil le haghaidh tuilleadh freagraí", "status.continued_thread": "Snáithe ar lean", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Post bainte ag an údar", "status.quote_followers_only": "Ní féidir ach le leantóirí an post seo a lua", "status.quote_manual_review": "Déanfaidh an t-údar athbhreithniú de láimh", - "status.quote_noun": "Luachan", "status.quote_policy_change": "Athraigh cé a fhéadann luachan a thabhairt", "status.quote_post_author": "Luaigh mé post le @{name}", "status.quote_private": "Ní féidir poist phríobháideacha a lua", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 2d37337a0c..62b2a7389c 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Desfacer compartido", "status.cannot_quote": "Non tes permiso para citar esta publicación", "status.cannot_reblog": "Esta publicación non pode ser promovida", - "status.contains_quote": "Contén unha cita", "status.context.load_new_replies": "Non hai respostas dispoñibles", "status.context.loading": "Mirando se hai máis respostas", "status.continued_thread": "Continua co fío", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Publicación retirada pola autora", "status.quote_followers_only": "Só as seguidoras poden citar esta publicación", "status.quote_manual_review": "A autora revisará manualmente", - "status.quote_noun": "Cita", "status.quote_policy_change": "Cambia quen pode citarte", "status.quote_post_author": "Citou unha publicación de @{name}", "status.quote_private": "As publicacións privadas non se poden citar", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index bcba5e0b7c..e338305ab8 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "הסרת הדהוד", "status.cannot_quote": "אין לך הרשאה לצטט את ההודעה הזו", "status.cannot_reblog": "לא ניתן להדהד חצרוץ זה", - "status.contains_quote": "הודעה מכילה ציטוט", "status.context.load_new_replies": "הגיעו תגובות חדשות", "status.context.loading": "מחפש תגובות חדשות", "status.continued_thread": "שרשור מתמשך", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "ההודעה הוסרה על ידי המחבר.ת", "status.quote_followers_only": "רק עוקביך יוכלו לצטט את ההודעה", "status.quote_manual_review": "מחבר.ת ההודעה יחזרו אליך אחרי בדיקה", - "status.quote_noun": "ציטוט", "status.quote_policy_change": "הגדרת הרשאה לציטוט הודעותיך", "status.quote_post_author": "ההודעה צוטטה על ידי @{name}", "status.quote_private": "הודעות פרטיות לא ניתנות לציטוט", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 83fb3e60fe..42fad56b6f 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Megtolás visszavonása", "status.cannot_quote": "Nem idézheted ezt a bejegyzést", "status.cannot_reblog": "Ezt a bejegyzést nem lehet megtolni", - "status.contains_quote": "Idézést tartalmaz", "status.context.load_new_replies": "Új válaszok érhetőek el", "status.context.loading": "További válaszok keresése", "status.continued_thread": "Folytatott szál", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "A szerző eltávolítta a bejegyzést", "status.quote_followers_only": "Csak a követők idézhetik ezt a bejegyzést", "status.quote_manual_review": "A szerző kézileg fogja jóváhagyni", - "status.quote_noun": "Idézés", "status.quote_policy_change": "Módosítás, hogy kik idézhetnek", "status.quote_post_author": "Idézte @{name} bejegyzését", "status.quote_private": "A privát bejegyzések nem idézhetőek", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index ba42000ec7..8cd1bafa4a 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -642,21 +642,21 @@ "notifications.column_settings.alert": "デスクトップ通知", "notifications.column_settings.favourite": "お気に入り", "notifications.column_settings.filter_bar.advanced": "すべてのカテゴリを表示", - "notifications.column_settings.filter_bar.category": "クイックフィルターバー", + "notifications.column_settings.filter_bar.category": "クイックフィルターバー:", "notifications.column_settings.follow": "新しいフォロワー", - "notifications.column_settings.follow_request": "新しいフォローリクエスト", + "notifications.column_settings.follow_request": "新しいフォローリクエスト:", "notifications.column_settings.group": "グループ", "notifications.column_settings.mention": "返信", "notifications.column_settings.poll": "アンケート結果", "notifications.column_settings.push": "プッシュ通知", "notifications.column_settings.quote": "引用", - "notifications.column_settings.reblog": "ブースト", + "notifications.column_settings.reblog": "ブースト:", "notifications.column_settings.show": "カラムに表示", "notifications.column_settings.sound": "通知音を再生", "notifications.column_settings.status": "新しい投稿", - "notifications.column_settings.unread_notifications.category": "未読の通知", + "notifications.column_settings.unread_notifications.category": "未読の通知:", "notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示", - "notifications.column_settings.update": "編集", + "notifications.column_settings.update": "編集:", "notifications.filter.all": "すべて", "notifications.filter.boosts": "ブースト", "notifications.filter.favourites": "お気に入り", @@ -875,7 +875,6 @@ "status.quote.cancel": "引用をキャンセル", "status.quote_error.filtered": "あなたのフィルター設定によって非表示になっています", "status.quote_error.pending_approval": "承認待ちの投稿", - "status.quote_noun": "引用", "status.quotes": "{count, plural, other {引用}}", "status.read_more": "もっと見る", "status.reblog": "ブースト", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index f61f7469d5..c1a21ad6ba 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Niet langer boosten", "status.cannot_quote": "Je bent niet gemachtigd om dit bericht te citeren", "status.cannot_reblog": "Dit bericht kan niet geboost worden", - "status.contains_quote": "Bevat citaat", "status.context.load_new_replies": "Nieuwe reacties beschikbaar", "status.context.loading": "Op nieuwe reacties aan het controleren", "status.continued_thread": "Vervolg van gesprek", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Bericht verwijderd door auteur", "status.quote_followers_only": "Alleen volgers mogen dit bericht citeren", "status.quote_manual_review": "De auteur gaat het handmatig beoordelen", - "status.quote_noun": "Citaat", "status.quote_policy_change": "Wijzig wie jou mag citeren", "status.quote_post_author": "Citeerde een bericht van @{name}", "status.quote_private": "Citeren van berichten aan alleen volgers is niet mogelijk", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 9d61ed2053..d4f5993b85 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Yeniden paylaşımı geri al", "status.cannot_quote": "Bu gönderiyi alıntılamaya izniniz yok", "status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz", - "status.contains_quote": "Alıntı içeriyor", "status.context.load_new_replies": "Yeni yanıtlar mevcut", "status.context.loading": "Daha fazla yanıt için kontrol ediliyor", "status.continued_thread": "Devam eden akış", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Gönderi yazarı tarafından kaldırıldı", "status.quote_followers_only": "Sadece takipçiler bu gönderiyi alıntılayabilir", "status.quote_manual_review": "Yazar manuel olarak gözden geçirecek", - "status.quote_noun": "Alıntı", "status.quote_policy_change": "Kimin alıntı yapabileceğini değiştirin", "status.quote_post_author": "@{name} adlı kullanıcının bir gönderisini alıntıladı", "status.quote_private": "Özel gönderiler alıntılanamaz", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 99a5dc9051..5ac881547d 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "Bỏ đăng lại", "status.cannot_quote": "Bạn không được phép trích dẫn tút này", "status.cannot_reblog": "Không thể đăng lại tút này", - "status.contains_quote": "Chứa trích dẫn", "status.context.load_new_replies": "Có những trả lời mới", "status.context.loading": "Kiểm tra nhiều trả lời hơn", "status.continued_thread": "Tiếp tục chủ đề", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "Tút gốc đã bị tác giả gỡ", "status.quote_followers_only": "Chỉ người theo dõi tôi có thể trích dẫn tút này", "status.quote_manual_review": "Người đăng sẽ duyệt thủ công", - "status.quote_noun": "Trích dẫn", "status.quote_policy_change": "Thay đổi người có thể trích dẫn", "status.quote_post_author": "Trích dẫn từ tút của @{name}", "status.quote_private": "Không thể trích dẫn nhắn riêng", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index c69e72eced..1f1f895ff7 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "取消转嘟", "status.cannot_quote": "你无法引用此嘟文", "status.cannot_reblog": "不能转嘟这条嘟文", - "status.contains_quote": "包含引用", "status.context.load_new_replies": "有新回复", "status.context.loading": "正在检查更多回复", "status.continued_thread": "上接嘟文串", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "嘟文已被作者删除", "status.quote_followers_only": "只有关注者才能引用这篇嘟文", "status.quote_manual_review": "嘟文作者将人工审核", - "status.quote_noun": "引用", "status.quote_policy_change": "更改谁可以引用", "status.quote_post_author": "引用了 @{name} 的嘟文", "status.quote_private": "不能引用私人嘟文", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index e4f9dcea29..5b70f9ffde 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -864,7 +864,6 @@ "status.cancel_reblog_private": "取消轉嘟", "status.cannot_quote": "您不被允許引用此嘟文", "status.cannot_reblog": "這則嘟文無法被轉嘟", - "status.contains_quote": "包含引用嘟文", "status.context.load_new_replies": "有新回嘟", "status.context.loading": "正在檢查更多回嘟", "status.continued_thread": "接續討論串", @@ -904,7 +903,6 @@ "status.quote_error.revoked": "嘟文已被作者刪除", "status.quote_followers_only": "只有我的跟隨者能引用此嘟文", "status.quote_manual_review": "嘟文作者將人工審閱", - "status.quote_noun": "引用嘟文", "status.quote_policy_change": "變更可以引用的人", "status.quote_post_author": "已引用 @{name} 之嘟文", "status.quote_private": "無法引用私人嘟文", diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 2d544417e3..fc4448740f 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -12,7 +12,11 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures'; +export type Features = + | 'modern_emojis' + | 'outgoing_quotes' + | 'fasp' + | 'http_message_signatures'; export function isFeatureEnabled(feature: Features) { return initialState?.features.includes(feature) ?? false; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 781e8511f5..c68bc6d679 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -163,7 +163,7 @@ $content-width: 840px; flex: 1 1 auto; } - @media screen and (max-width: ($content-width + $sidebar-width)) { + @media screen and (max-width: $content-width + $sidebar-width) { .sidebar-wrapper--empty { display: none; } @@ -1081,17 +1081,6 @@ a.name-tag, } } - &__action-bar { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; - - &:not(.no-wrap) { - flex-wrap: wrap; - } - } - &__meta { padding: 0 15px; color: $dark-text-color; @@ -1108,8 +1097,10 @@ a.name-tag, } } - &__actions { - margin-inline-start: auto; + &__action-bar { + display: flex; + justify-content: space-between; + align-items: center; } &__permissions { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b390a8a8e5..acfc906dc6 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1597,16 +1597,6 @@ } } } - - .no-reduce-motion &--highlighted-entry::before { - content: ''; - position: absolute; - inset: 0; - background: rgb(from $ui-highlight-color r g b / 20%); - opacity: 0; - animation: fade 0.7s reverse both 0.3s; - pointer-events: none; - } } .status__relative-time { @@ -2970,6 +2960,7 @@ a.account__display-name { flex: 1 1 auto; flex-direction: row; justify-content: flex-start; + overflow-x: auto; position: relative; &.unscrollable { @@ -3145,29 +3136,6 @@ a.account__display-name { } } -.column__alert { - position: sticky; - bottom: 1rem; - z-index: 10; - box-sizing: border-box; - display: grid; - width: 100%; - max-width: 360px; - padding-inline: 10px; - margin-top: 1rem; - margin-inline: auto; - - @media (max-width: #{$mobile-menu-breakpoint - 1}) { - bottom: 4rem; - } - - & > * { - // Make all nested alerts occupy the same space - // rather than stack - grid-area: 1 / 1; - } -} - .ui { --mobile-bottom-nav-height: 55px; --last-content-item-border-width: 2px; @@ -3208,6 +3176,7 @@ a.account__display-name { .column, .drawer { flex: 1 1 100%; + overflow: hidden; } @media screen and (width > $mobile-breakpoint) { @@ -10419,21 +10388,6 @@ noscript { } } -.notification-bar__loading-indicator { - --spinner-size: 22px; - - position: relative; - height: var(--spinner-size); - width: var(--spinner-size); - margin-inline-start: 2px; - - svg { - color: $white; - height: var(--spinner-size); - width: var(--spinner-size); - } -} - .hashtag-header { border-bottom: 1px solid var(--background-border-color); padding: 15px; diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 6d386f45dc..7b49acd119 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -9,7 +9,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity quoted_status = status_from_uri(object_uri) return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable? - if StatusPolicy.new(@account, quoted_status).quote? + if Mastodon::Feature.outgoing_quotes_enabled? && StatusPolicy.new(@account, quoted_status).quote? accept_quote_request!(quoted_status) else reject_quote_request!(quoted_status) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index f99d15a6d9..aaa7b574ee 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -38,7 +38,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :quote, key: :quote_uri, if: :quote? attribute :quote_authorization, if: :quote_authorization? - attribute :interaction_policy + attribute :interaction_policy, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } def id raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only] diff --git a/app/serializers/rest/shallow_status_serializer.rb b/app/serializers/rest/shallow_status_serializer.rb index 0b951f6caa..d82ac32621 100644 --- a/app/serializers/rest/shallow_status_serializer.rb +++ b/app/serializers/rest/shallow_status_serializer.rb @@ -6,5 +6,5 @@ class REST::ShallowStatusSerializer < REST::StatusSerializer # It looks like redefining one `has_one` requires redefining all inherited ones has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer - has_one :quote_approval + has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index da4eb4b125..50b04e29f2 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -36,7 +36,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_one :quote, key: :quote, serializer: REST::QuoteSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer - has_one :quote_approval + has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } delegate :local?, to: :object diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 5cbf990c4d..7e3733391f 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -85,7 +85,7 @@ class PostStatusService < BaseService @sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present? @visibility = @options[:visibility] || @account.user&.setting_default_privacy @visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced? - @visibility = :private if @quoted_status&.private_visibility? && %i(public unlisted).include?(@visibility&.to_sym) + @visibility = :private if @quoted_status&.private_visibility? @scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = nil if scheduled_in_the_past? rescue ArgumentError diff --git a/app/views/admin/announcements/_announcement.html.haml b/app/views/admin/announcements/_announcement.html.haml index 5944b0b295..87ae97cf48 100644 --- a/app/views/admin/announcements/_announcement.html.haml +++ b/app/views/admin/announcements/_announcement.html.haml @@ -9,7 +9,7 @@ - else = l(announcement.created_at) - .announcements-list__item__actions + %div - if can?(:distribute, announcement) = table_link_to 'mail', t('admin.terms_of_service.notify_users'), admin_announcement_preview_path(announcement) - if can?(:update, announcement) diff --git a/app/views/admin/roles/_role.html.haml b/app/views/admin/roles/_role.html.haml index ddaca5d8a9..085bdbd156 100644 --- a/app/views/admin/roles/_role.html.haml +++ b/app/views/admin/roles/_role.html.haml @@ -26,5 +26,5 @@ = link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id) · %abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size) - .announcements-list__item__actions + %div = table_link_to 'edit', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role) diff --git a/app/views/admin/rules/_rule.html.haml b/app/views/admin/rules/_rule.html.haml index d79c1dfa6c..7d84534d59 100644 --- a/app/views/admin/rules/_rule.html.haml +++ b/app/views/admin/rules/_rule.html.haml @@ -3,7 +3,7 @@ #{rule_counter + 1}. = truncate(rule.text) - .announcements-list__item__action-bar.no-wrap + .announcements-list__item__action-bar .announcements-list__item__meta = rule.hint diff --git a/app/views/admin/warning_presets/_warning_preset.html.haml b/app/views/admin/warning_presets/_warning_preset.html.haml index 6488c3a554..2cc056420f 100644 --- a/app/views/admin/warning_presets/_warning_preset.html.haml +++ b/app/views/admin/warning_presets/_warning_preset.html.haml @@ -6,5 +6,5 @@ .announcements-list__item__meta = truncate(warning_preset.text) - .announcements-list__item__actions + %div = table_link_to 'delete', t('admin.warning_presets.delete'), admin_warning_preset_path(warning_preset), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, warning_preset) diff --git a/app/views/admin/webhooks/_webhook.html.haml b/app/views/admin/webhooks/_webhook.html.haml index 6159d97820..dca5abeb77 100644 --- a/app/views/admin/webhooks/_webhook.html.haml +++ b/app/views/admin/webhooks/_webhook.html.haml @@ -14,6 +14,6 @@ %abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size) - .announcements-list__item__actions + %div = table_link_to 'edit', t('admin.webhooks.edit'), edit_admin_webhook_path(webhook) if can?(:update, webhook) = table_link_to 'delete', t('admin.webhooks.delete'), admin_webhook_path(webhook), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, webhook) diff --git a/app/views/filters/_filter.html.haml b/app/views/filters/_filter.html.haml index 15326f3006..a544ac3a75 100644 --- a/app/views/filters/_filter.html.haml +++ b/app/views/filters/_filter.html.haml @@ -32,10 +32,10 @@ .permissions-list__item__text__type = t('filters.index.statuses_long', count: filter.statuses.size) - .filters-list__item__action-bar - .filters-list__item__meta + .announcements-list__item__action-bar + .announcements-list__item__meta = t('filters.index.contexts', contexts: filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', ')) - .filters-list__item__actions + %div = table_link_to 'edit', t('filters.edit.title'), edit_filter_path(filter) = table_link_to 'close', t('filters.index.delete'), filter_path(filter), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index b28302a93f..3745ed219f 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -28,7 +28,7 @@ = t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date)) - unless application.superapp? || current_account.unavailable? - .announcements-list__item__actions + %div = table_link_to 'close', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') } .announcements-list__item__permissions diff --git a/config/locales/el.yml b/config/locales/el.yml index a17e870075..3243e9b761 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -114,7 +114,7 @@ el: other: Αυτός ο λογαριασμός έχει %{count} παραπτώματα. promote: Προαγωγή protocol: Πρωτόκολλο - public: Δημόσιος + public: Δημόσιο push_subscription_expires: Η εγγραφή PuSH λήγει redownload: Ανανέωση άβαταρ redownloaded_msg: Επιτυχής ανανέωση προφίλ του/της %{username} από την πηγή @@ -1919,7 +1919,7 @@ el: visibilities: direct: Ιδιωτική επισήμανση private: Μόνο ακόλουθοι - public: Δημόσια + public: Δημόσιο public_long: Όλοι εντός και εκτός του Mastodon unlisted: Ήσυχα δημόσια unlisted_long: Κρυμμένη από τα αποτελέσματα αναζήτησης Mastodon, τις τάσεις και τις δημόσιες ροές diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 184ab506d6..28502bdd9c 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -572,7 +572,6 @@ ja: title: モデレーション moderation_notes: create: モデレーションノートを追加 - title: モデレーションメモ private_comment: コメント (非公開) public_comment: コメント (公開) purge: パージ @@ -1068,18 +1067,14 @@ ja: trending: トレンド username_blocks: add_new: ルールを作成 - block_registrations: 登録拒否 comparison: contains: 含む equals: 一致 - contains_html: "%{string}を含む" delete: 削除 edit: title: ユーザー名ルールの編集 - matches_exactly_html: "%{string}に一致" new: create: ルールを作成 - title: ユーザー名ルール warning_presets: add_new: 追加 delete: 削除 @@ -1684,7 +1679,6 @@ ja: self_vote: 自分のアンケートには解答できません too_few_options: は複数必要です too_many_options: は%{max}個までです - vote: 投票 preferences: other: その他 posting_defaults: デフォルトの投稿設定 diff --git a/config/puma.rb b/config/puma.rb index d34c14b425..16c481a2ae 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -27,7 +27,7 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' end end - before_worker_boot do + on_worker_boot do # Ruby process metrics (memory, GC, etc) PrometheusExporter::Instrumentation::Process.start(type: 'puma') @@ -44,7 +44,7 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' end end -before_worker_boot do +on_worker_boot do ActiveSupport.on_load(:active_record) do ActiveRecord::Base.establish_connection end diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 043f22b28e..a6bbfcd24d 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -45,7 +45,7 @@ module Mastodon def api_versions { - mastodon: 7, + mastodon: Mastodon::Feature.outgoing_quotes_enabled? ? 7 : 6, } end diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index aae4ce0338..64627cbdfb 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::Activity::QuoteRequest do +RSpec.describe ActivityPub::Activity::QuoteRequest, feature: :outgoing_quotes do let(:sender) { Fabricate(:account, domain: 'example.com') } let(:recipient) { Fabricate(:account) } let(:quoted_post) { Fabricate(:status, account: recipient) } diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 085866ef1d..f450997976 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -28,7 +28,7 @@ RSpec.describe StatusCacheHydrator do end end - context 'when handling a status with a quote policy' do + context 'when handling a status with a quote policy', feature: :outgoing_quotes do let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } before do diff --git a/spec/requests/api/v1/statuses/interaction_policies_spec.rb b/spec/requests/api/v1/statuses/interaction_policies_spec.rb index aa447de17f..cdc33e40d7 100644 --- a/spec/requests/api/v1/statuses/interaction_policies_spec.rb +++ b/spec/requests/api/v1/statuses/interaction_policies_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Interaction policies' do +RSpec.describe 'Interaction policies', feature: :outgoing_quotes do let(:user) { Fabricate(:user) } let(:scopes) { 'write:statuses' } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 249abc2440..eb3e8aed5b 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -158,7 +158,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'without a quote policy' do + context 'without a quote policy', feature: :outgoing_quotes do let(:user) do Fabricate(:user, settings: { default_quote_policy: 'followers' }) end @@ -180,7 +180,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'without a quote policy and the user defaults to nobody' do + context 'without a quote policy and the user defaults to nobody', feature: :outgoing_quotes do let(:user) do Fabricate(:user, settings: { default_quote_policy: 'nobody' }) end @@ -202,7 +202,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'with a quote policy' do + context 'with a quote policy', feature: :outgoing_quotes do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do { @@ -227,7 +227,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'with a self-quote post' do + context 'with a self-quote post', feature: :outgoing_quotes do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do { @@ -248,7 +248,7 @@ RSpec.describe '/api/v1/statuses' do end end - context 'with a self-quote post and a CW but no text' do + context 'with a self-quote post and a CW but no text', feature: :outgoing_quotes do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do { @@ -420,7 +420,7 @@ RSpec.describe '/api/v1/statuses' do context 'when updating only the quote policy' do let(:params) { { status: status.text, quote_approval_policy: 'public' } } - it 'updates the status', :aggregate_failures do + it 'updates the status', :aggregate_failures, feature: :outgoing_quotes do expect { subject } .to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 04179e9bf4..336f394337 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -58,7 +58,7 @@ RSpec.describe ActivityPub::NoteSerializer do end end - context 'with a quote policy' do + context 'with a quote policy', feature: :outgoing_quotes do let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } it 'has the expected shape' do diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 96289cdeee..c434d0cb6e 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -321,14 +321,6 @@ RSpec.describe PostStatusService do expect(status).to be_private_visibility end - it 'correctly preserves visibility for private mentions self-quoting private posts' do - account = Fabricate(:account) - quoted_status = Fabricate(:status, account: account, visibility: :private) - - status = subject.call(account, text: 'test', quoted_status: quoted_status, visibility: 'direct') - expect(status).to be_direct_visibility - end - it 'returns existing status when used twice with idempotency key' do account = Fabricate(:account) status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')