diff --git a/Gemfile b/Gemfile index 9a0e1d6094..a294fb7206 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ ruby '>= 3.2.0', '< 3.5.0' gem 'propshaft' gem 'puma', '~> 7.0' -gem 'rails', '~> 8.0' +gem 'rails', '~> 8.1.0' gem 'thor', '~> 1.2' gem 'dotenv' diff --git a/Gemfile.lock b/Gemfile.lock index 8feb1490d0..1d94c3ec05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,29 +10,31 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (8.0.3) - actionpack (= 8.0.3) - activesupport (= 8.0.3) + action_text-trix (2.1.16) + railties + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.3) - actionpack (= 8.0.3) - activejob (= 8.0.3) - activerecord (= 8.0.3) - activestorage (= 8.0.3) - activesupport (= 8.0.3) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) - actionmailer (8.0.3) - actionpack (= 8.0.3) - actionview (= 8.0.3) - activejob (= 8.0.3) - activesupport (= 8.0.3) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.3) - actionview (= 8.0.3) - activesupport (= 8.0.3) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -40,15 +42,16 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.3) - actionpack (= 8.0.3) - activerecord (= 8.0.3) - activestorage (= 8.0.3) - activesupport (= 8.0.3) + actiontext (8.1.2) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.3) - activesupport (= 8.0.3) + actionview (8.1.2) + activesupport (= 8.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -58,29 +61,29 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (8.0.3) - activesupport (= 8.0.3) + activejob (8.1.2) + activesupport (= 8.1.2) globalid (>= 0.3.6) - activemodel (8.0.3) - activesupport (= 8.0.3) - activerecord (8.0.3) - activemodel (= 8.0.3) - activesupport (= 8.0.3) + activemodel (8.1.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) timeout (>= 0.4.0) - activestorage (8.0.3) - actionpack (= 8.0.3) - activejob (= 8.0.3) - activerecord (= 8.0.3) - activesupport (= 8.0.3) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) marcel (~> 1.0) - activesupport (8.0.3) + activesupport (8.1.2) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) @@ -96,7 +99,7 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1213.0) + aws-partitions (1.1220.0) aws-sdk-core (3.242.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -105,7 +108,7 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.121.0) + aws-sdk-kms (1.122.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) aws-sdk-s3 (1.213.0) @@ -129,7 +132,7 @@ GEM binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blurhash (0.1.8) - bootsnap (1.22.0) + bootsnap (1.23.0) msgpack (~> 1.2) brakeman (8.0.2) racc @@ -227,7 +230,7 @@ GEM mail (~> 2.7) email_validator (2.2.4) activemodel - erb (6.0.1) + erb (6.0.2) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -291,7 +294,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.69.0) + haml_lint (0.71.0) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -447,17 +450,18 @@ GEM mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2026.0203) + mime-types-data (3.2026.0224) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (6.0.1) + minitest (6.0.2) + drb (~> 2.0) prism (~> 1.5) msgpack (1.8.0) multi_json (1.19.1) mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.6.2) + net-imap (0.6.3) date net-protocol net-ldap (0.20.0) @@ -590,7 +594,7 @@ GEM ox (2.14.23) bigdecimal (>= 3.0) parallel (1.27.0) - parser (3.3.10.1) + parser (3.3.10.2) ast (~> 2.4.1) racc parslet (2.0.0) @@ -657,33 +661,33 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.0.3) - actioncable (= 8.0.3) - actionmailbox (= 8.0.3) - actionmailer (= 8.0.3) - actionpack (= 8.0.3) - actiontext (= 8.0.3) - actionview (= 8.0.3) - activejob (= 8.0.3) - activemodel (= 8.0.3) - activerecord (= 8.0.3) - activestorage (= 8.0.3) - activesupport (= 8.0.3) + rails (8.1.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) bundler (>= 1.15.0) - railties (= 8.0.3) + railties (= 8.1.2) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.0.3) - actionpack (= 8.0.3) - activesupport (= 8.0.3) + railties (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -701,7 +705,7 @@ GEM readline (~> 0.0) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (7.1.0) + rdoc (7.2.0) erb psych (>= 4.0.0) tsort @@ -792,8 +796,9 @@ GEM lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (~> 3.5) - ruby-prof (1.7.2) + ruby-prof (2.0.2) base64 + ostruct ruby-progressbar (1.13.0) ruby-saml (1.18.1) nokogiri (>= 1.13.10) @@ -903,7 +908,7 @@ GEM vite_rails (3.0.20) railties (>= 5.1, < 9) vite_ruby (~> 3.0, >= 3.2.2) - vite_ruby (3.9.2) + vite_ruby (3.9.3) dry-cli (>= 0.7, < 2) logger (~> 1.6) mutex_m @@ -936,7 +941,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.4) + zeitwerk (2.7.5) PLATFORMS ruby @@ -1050,7 +1055,7 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors rack-test (~> 2.1) - rails (~> 8.0) + rails (~> 8.1.0) rails-i18n (~> 8.0) rdf-normalize (~> 0.5) redcarpet (~> 3.6) @@ -1100,4 +1105,4 @@ RUBY VERSION ruby 3.4.8 BUNDLED WITH - 4.0.6 + 4.0.7 diff --git a/app/controllers/activitypub/feature_authorizations_controller.rb b/app/controllers/activitypub/feature_authorizations_controller.rb new file mode 100644 index 0000000000..ef9f458bf7 --- /dev/null +++ b/app/controllers/activitypub/feature_authorizations_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class ActivityPub::FeatureAuthorizationsController < ActivityPub::BaseController + include Authorization + + vary_by -> { 'Signature' if authorized_fetch_mode? } + + before_action :require_account_signature!, if: :authorized_fetch_mode? + before_action :set_collection_item + + def show + expires_in 30.seconds, public: true if public_fetch_mode? + render json: @collection_item, serializer: ActivityPub::FeatureAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' + end + + private + + def pundit_user + signed_request_account + end + + def set_collection_item + @collection_item = @account.collection_items.accepted.find(params[:id]) + + authorize @collection_item.collection, :show? + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError + not_found + end +end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 5e1074b224..fdc8e53f53 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -54,7 +54,7 @@ module Admin end # Allow transparently upgrading a domain block - if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(@domain_block.domain.strip) + if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(@domain_block.domain) @domain_block = existing_domain_block @domain_block.assign_attributes(resource_params) end diff --git a/app/controllers/admin/instances/moderation_notes_controller.rb b/app/controllers/admin/instances/moderation_notes_controller.rb index 635c097349..dd6c32bda5 100644 --- a/app/controllers/admin/instances/moderation_notes_controller.rb +++ b/app/controllers/admin/instances/moderation_notes_controller.rb @@ -34,8 +34,11 @@ class Admin::Instances::ModerationNotesController < Admin::BaseController end def set_instance - domain = params[:instance_id]&.strip - @instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain)) + @instance = Instance.find_or_initialize_by(domain: normalized_domain) + end + + def normalized_domain + TagManager.instance.normalize_domain(params[:instance_id]) end def set_instance_note diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 6ab4acab99..033d250a2e 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -55,8 +55,11 @@ module Admin private def set_instance - domain = params[:id]&.strip - @instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain)) + @instance = Instance.find_or_initialize_by(domain: normalized_domain) + end + + def normalized_domain + TagManager.instance.normalize_domain(params[:id]) end def set_instances diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb index d9c8232702..27b7503e9f 100644 --- a/app/controllers/api/v1/peers/search_controller.rb +++ b/app/controllers/api/v1/peers/search_controller.rb @@ -47,10 +47,6 @@ class Api::V1::Peers::SearchController < Api::BaseController end def normalized_domain - TagManager.instance.normalize_domain(query_value) - end - - def query_value - params[:q].strip + TagManager.instance.normalize_domain(params[:q]) end end diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index 67a4d8ef49..36822d831b 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -39,6 +39,6 @@ class Api::V1::TagsController < Api::BaseController def set_or_create_tag return not_found unless Tag::HASHTAG_NAME_RE.match?(params[:id]) - @tag = Tag.find_normalized(params[:id]) || Tag.new(name: Tag.normalize(params[:id]), display_name: params[:id]) + @tag = Tag.find_normalized(params[:id]) || Tag.new(name: params[:id], display_name: params[:id]) end end diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap index 94d5402b20..de4ccd6594 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar_overlay-test.jsx.snap @@ -26,6 +26,7 @@ exports[` renders a overlay avatar 1`] = ` > alice @@ -44,6 +45,7 @@ exports[` renders a overlay avatar 1`] = ` > eve@blackhat.lair diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index 6e1c5dbfd4..b086ef4225 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -7,6 +7,8 @@ import { useHovering } from 'mastodon/hooks/useHovering'; import { autoPlayGif } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; +import { useAccount } from '../hooks/useAccount'; + interface Props { account: | Pick @@ -91,3 +93,10 @@ export const Avatar: React.FC = ({ return avatar; }; + +export const AvatarById: React.FC< + { accountId: string } & Omit +> = ({ accountId, ...otherProps }) => { + const account = useAccount(accountId); + return ; +}; diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx index 0bd33fea69..e7fc1252c1 100644 --- a/app/javascript/mastodon/components/avatar_overlay.tsx +++ b/app/javascript/mastodon/components/avatar_overlay.tsx @@ -10,6 +10,14 @@ interface Props { overlaySize?: number; } +const handleImgLoadError = (error: { currentTarget: HTMLElement }) => { + // + // When the img tag fails to load the image, set the img tag to display: none. This prevents the + // alt-text from overrunning the containing div. + // + error.currentTarget.style.display = 'none'; +}; + export const AvatarOverlay: React.FC = ({ account, friend, @@ -38,7 +46,13 @@ export const AvatarOverlay: React.FC = ({ className='account__avatar' style={{ width: `${baseSize}px`, height: `${baseSize}px` }} > - {accountSrc && {account?.get('acct')}} + {accountSrc && ( + {account?.get('acct')} + )}
@@ -46,7 +60,13 @@ export const AvatarOverlay: React.FC = ({ className='account__avatar' style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }} > - {friendSrc && {friend?.get('acct')}} + {friendSrc && ( + {friend?.get('acct')} + )}
diff --git a/app/javascript/mastodon/components/copy_icon_button.tsx b/app/javascript/mastodon/components/copy_icon_button.tsx index 29f5f34430..51cffe6292 100644 --- a/app/javascript/mastodon/components/copy_icon_button.tsx +++ b/app/javascript/mastodon/components/copy_icon_button.tsx @@ -19,8 +19,9 @@ const messages = defineMessages({ export const CopyIconButton: React.FC<{ title: string; value: string; - className: string; -}> = ({ title, value, className }) => { + className?: string; + 'aria-describedby'?: string; +}> = ({ title, value, className, 'aria-describedby': ariaDescribedBy }) => { const [copied, setCopied] = useState(false); const dispatch = useAppDispatch(); @@ -38,8 +39,9 @@ export const CopyIconButton: React.FC<{ className={classNames(className, copied ? 'copied' : 'copyable')} title={title} onClick={handleClick} - icon='' + icon='copy-icon' iconComponent={ContentCopyIcon} + aria-describedby={ariaDescribedBy} /> ); }; diff --git a/app/javascript/mastodon/components/form_fields/copy_link_field.module.scss b/app/javascript/mastodon/components/form_fields/copy_link_field.module.scss new file mode 100644 index 0000000000..06834e9d91 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/copy_link_field.module.scss @@ -0,0 +1,14 @@ +.wrapper { + position: relative; +} + +.input { + padding-inline-end: 45px; +} + +.copyButton { + position: absolute; + inset-inline-end: 0; + top: 0; + padding: 9px; +} diff --git a/app/javascript/mastodon/components/form_fields/copy_link_field.tsx b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx new file mode 100644 index 0000000000..ad93e3a065 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/copy_link_field.tsx @@ -0,0 +1,81 @@ +import { forwardRef, useCallback, useRef } from 'react'; + +import { useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import { CopyIconButton } from 'mastodon/components/copy_icon_button'; + +import classes from './copy_link_field.module.scss'; +import { FormFieldWrapper } from './form_field_wrapper'; +import type { CommonFieldWrapperProps } from './form_field_wrapper'; +import { TextInput } from './text_input_field'; +import type { TextInputProps } from './text_input_field'; + +interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps { + value: string; +} + +/** + * A read-only text field with a button for copying the field value + */ + +export const CopyLinkField = forwardRef( + ( + { id, label, hint, hasError, value, required, className, ...otherProps }, + ref, + ) => { + const intl = useIntl(); + const inputRef = useRef(); + const handleFocus = useCallback(() => { + inputRef.current?.select(); + }, []); + + const mergeRefs = useCallback( + (element: HTMLInputElement | null) => { + inputRef.current = element; + if (typeof ref === 'function') { + ref(element); + } else if (ref) { + ref.current = element; + } + }, + [ref], + ); + + return ( + + {(inputProps) => ( +
+ + +
+ )} +
+ ); + }, +); + +CopyLinkField.displayName = 'CopyLinkField'; diff --git a/app/javascript/mastodon/components/form_fields/index.ts b/app/javascript/mastodon/components/form_fields/index.ts index fca366106f..b44ceb63f8 100644 --- a/app/javascript/mastodon/components/form_fields/index.ts +++ b/app/javascript/mastodon/components/form_fields/index.ts @@ -1,3 +1,4 @@ +export { FormFieldWrapper } from './form_field_wrapper'; export { FormStack } from './form_stack'; export { Fieldset } from './fieldset'; export { TextInputField, TextInput } from './text_input_field'; @@ -8,6 +9,7 @@ export { Combobox, type ComboboxItemState, } from './combobox_field'; +export { CopyLinkField } from './copy_link_field'; export { RadioButtonField, RadioButton } from './radio_button_field'; export { ToggleField, Toggle } from './toggle_field'; export { SelectField, Select } from './select_field'; diff --git a/app/javascript/mastodon/components/modal_shell/index.tsx b/app/javascript/mastodon/components/modal_shell/index.tsx new file mode 100644 index 0000000000..8b6fdcc6ad --- /dev/null +++ b/app/javascript/mastodon/components/modal_shell/index.tsx @@ -0,0 +1,56 @@ +import classNames from 'classnames'; + +interface SimpleComponentProps { + className?: string; + children?: React.ReactNode; +} + +interface ModalShellComponent extends React.FC { + Body: React.FC; + Actions: React.FC; +} + +export const ModalShell: ModalShellComponent = ({ children, className }) => { + return ( +
+ {children} +
+ ); +}; + +const ModalShellBody: ModalShellComponent['Body'] = ({ + children, + className, +}) => { + return ( +
+
+ {children} +
+
+ ); +}; + +const ModalShellActions: ModalShellComponent['Actions'] = ({ + children, + className, +}) => { + return ( +
+
+ {children} +
+
+ ); +}; + +ModalShell.Body = ModalShellBody; +ModalShell.Actions = ModalShellActions; diff --git a/app/javascript/mastodon/features/account_edit/components/profile_display_modal.tsx b/app/javascript/mastodon/features/account_edit/components/profile_display_modal.tsx new file mode 100644 index 0000000000..edf98e977c --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/components/profile_display_modal.tsx @@ -0,0 +1,122 @@ +import type { ChangeEventHandler, FC } from 'react'; +import { useCallback } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import { Callout } from '@/mastodon/components/callout'; +import { ToggleField } from '@/mastodon/components/form_fields'; +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { patchProfile } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; + +import type { DialogModalProps } from '../../ui/components/dialog_modal'; +import { DialogModal } from '../../ui/components/dialog_modal'; +import { messages } from '../index'; +import classes from '../styles.module.scss'; + +export const ProfileDisplayModal: FC = ({ onClose }) => { + const intl = useIntl(); + + const { profile, isPending } = useAppSelector((state) => state.profileEdit); + const serverName = useAppSelector( + (state) => state.meta.get('domain') as string, + ); + + const dispatch = useAppDispatch(); + const handleToggleChange: ChangeEventHandler = useCallback( + (event) => { + const { name, checked } = event.target; + void dispatch(patchProfile({ [name]: checked })); + }, + [dispatch], + ); + + if (!profile) { + return ; + } + + return ( + +
+ + } + hint={ + + } + /> + + + } + hint={ + + } + /> + + + } + hint={ + + } + /> +
+ + + } + icon={false} + > + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index c8bc93f15f..da58088e89 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -1,13 +1,14 @@ import { useCallback, useEffect } from 'react'; import type { FC } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import type { ModalType } from '@/mastodon/actions/modal'; import { openModal } from '@/mastodon/actions/modal'; import { Avatar } from '@/mastodon/components/avatar'; +import { Button } from '@/mastodon/components/button'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; @@ -25,7 +26,7 @@ import { EditButton } from './components/edit_button'; import { AccountEditSection } from './components/section'; import classes from './styles.module.scss'; -const messages = defineMessages({ +export const messages = defineMessages({ columnTitle: { id: 'account_edit.column_title', defaultMessage: 'Edit Profile', @@ -104,6 +105,9 @@ export const AccountEdit: FC = () => { const handleBioEdit = useCallback(() => { handleOpenModal('ACCOUNT_EDIT_BIO'); }, [handleOpenModal]); + const handleProfileDisplayEdit = useCallback(() => { + handleOpenModal('ACCOUNT_EDIT_PROFILE_DISPLAY'); + }, [handleOpenModal]); const history = useHistory(); const handleFeaturedTagsEdit = useCallback(() => { @@ -193,6 +197,17 @@ export const AccountEdit: FC = () => { title={messages.profileTabTitle} description={messages.profileTabSubtitle} showDescription + buttons={ + + } /> diff --git a/app/javascript/mastodon/features/account_edit/styles.module.scss b/app/javascript/mastodon/features/account_edit/styles.module.scss index ee8603cc4f..29daddbe3f 100644 --- a/app/javascript/mastodon/features/account_edit/styles.module.scss +++ b/app/javascript/mastodon/features/account_edit/styles.module.scss @@ -90,6 +90,16 @@ textarea.inputText { } } +.toggleInputWrapper { + > div { + padding: 12px 0; + + &:not(:first-child) { + border-top: 1px solid var(--color-border-primary); + } + } +} + // Column component .column { diff --git a/app/javascript/mastodon/features/account_timeline/components/fields.tsx b/app/javascript/mastodon/features/account_timeline/components/fields.tsx index c2802c2c51..5bad90aaae 100644 --- a/app/javascript/mastodon/features/account_timeline/components/fields.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/fields.tsx @@ -118,14 +118,14 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
{fields.map((field, key) => ( - + ))}
); }; -const FieldRow: FC<{ +const FieldCard: FC<{ htmlHandlers: ReturnType; field: AccountField; }> = ({ htmlHandlers, field }) => { @@ -183,15 +183,14 @@ const FieldRow: FC<{ ref={wrapperRef} > {verified_at && ( - + > + + )} ); diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss index d4ea2d7a7d..51a7962c76 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -278,11 +278,17 @@ svg.badgeIcon { } .fieldVerifiedIcon { + display: block; + position: absolute; width: 16px; height: 16px; - position: absolute; top: 8px; right: 8px; + + > svg { + width: 100%; + height: 100%; + } } .fieldOverflowButton { diff --git a/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx b/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx index 33e2e22891..f7251f7b41 100644 --- a/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx +++ b/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx @@ -2,7 +2,9 @@ import type { FC } from 'react'; import { FormattedMessage } from 'react-intl'; +import { Button } from '@/mastodon/components/button'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { ModalShell } from '@/mastodon/components/modal_shell'; import type { AccountField } from '../common'; import { useFieldHtml } from '../hooks/useFieldHtml'; @@ -16,29 +18,25 @@ export const AccountFieldModal: FC<{ const handleLabelElement = useFieldHtml(field.nameHasEmojis); const handleValueElement = useFieldHtml(field.valueHasEmojis); return ( -
-
-
- - -
-
-
-
- -
-
-
+ + + + + + + + + ); }; diff --git a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss index a57a5e738a..2ef62a7d25 100644 --- a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss +++ b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss @@ -7,6 +7,7 @@ border: none; background: none; padding: 8px 0; + font-size: 15px; font-weight: 500; display: flex; align-items: center; @@ -41,6 +42,10 @@ align-items: center; font-size: 15px; } + + [data-color-scheme='dark'] & { + border: 1px solid var(--color-border-primary); + } } .tagsWrapper { diff --git a/app/javascript/mastodon/features/collections/detail/collection_list_item.module.scss b/app/javascript/mastodon/features/collections/detail/collection_list_item.module.scss index 9e771dbaa0..3c71e90f48 100644 --- a/app/javascript/mastodon/features/collections/detail/collection_list_item.module.scss +++ b/app/javascript/mastodon/features/collections/detail/collection_list_item.module.scss @@ -44,6 +44,7 @@ --gap: 0.75ch; display: flex; + flex-wrap: wrap; gap: var(--gap); & > li:not(:last-child)::after { diff --git a/app/javascript/mastodon/features/collections/detail/index.tsx b/app/javascript/mastodon/features/collections/detail/index.tsx index d5b14da859..d2317e716f 100644 --- a/app/javascript/mastodon/features/collections/detail/index.tsx +++ b/app/javascript/mastodon/features/collections/detail/index.tsx @@ -3,18 +3,21 @@ import { useCallback, useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Helmet } from 'react-helmet'; -import { useParams } from 'react-router'; +import { useLocation, useParams } from 'react-router'; +import { openModal } from '@/mastodon/actions/modal'; import { useRelationship } from '@/mastodon/hooks/useRelationship'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ShareIcon from '@/material-icons/400-24px/share.svg?react'; -import { showAlert } from 'mastodon/actions/alerts'; import type { ApiCollectionJSON } from 'mastodon/api_types/collections'; import { Account } from 'mastodon/components/account'; import { Avatar } from 'mastodon/components/avatar'; import { Column } from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; -import { LinkedDisplayName } from 'mastodon/components/display_name'; +import { + DisplayName, + LinkedDisplayName, +} from 'mastodon/components/display_name'; import { IconButton } from 'mastodon/components/icon_button'; import ScrollableList from 'mastodon/components/scrollable_list'; import { Tag } from 'mastodon/components/tags/tag'; @@ -46,32 +49,40 @@ const messages = defineMessages({ }, }); -const AuthorNote: React.FC<{ id: string }> = ({ id }) => { +export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({ + id, + // When previewMode is enabled, your own display name + // will not be replaced with "you" + previewMode = false, +}) => { const account = useAccount(id); const author = ( - + {previewMode ? ( + + ) : ( + + )} ); - if (id === me) { - return ( -

+ const displayAsYou = id === me && !previewMode; + + return ( +

+ {displayAsYou ? ( -

- ); - } - return ( -

- + ) : ( + + )}

); }; @@ -84,8 +95,23 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({ const dispatch = useAppDispatch(); const handleShare = useCallback(() => { - dispatch(showAlert({ message: 'Collection sharing not yet implemented' })); - }, [dispatch]); + dispatch( + openModal({ + modalType: 'SHARE_COLLECTION', + modalProps: { + collection, + }, + }), + ); + }, [collection, dispatch]); + + const location = useLocation<{ newCollection?: boolean }>(); + const wasJustCreated = location.state.newCollection; + useEffect(() => { + if (wasJustCreated) { + handleShare(); + } + }, [handleShare, wasJustCreated]); return (
diff --git a/app/javascript/mastodon/features/collections/detail/share_modal.module.scss b/app/javascript/mastodon/features/collections/detail/share_modal.module.scss new file mode 100644 index 0000000000..2344ea519e --- /dev/null +++ b/app/javascript/mastodon/features/collections/detail/share_modal.module.scss @@ -0,0 +1,75 @@ +.heading { + font-size: 28px; + line-height: 1.3; + margin-bottom: 16px; +} + +.preview { + display: flex; + flex-wrap: wrap-reverse; + align-items: start; + justify-content: space-between; + gap: 8px; + padding: 16px; + margin-bottom: 16px; + border-radius: 8px; + color: var(--color-text-primary); + background: linear-gradient( + 145deg, + var(--color-bg-brand-soft), + var(--color-bg-primary) + ); + border: 1px solid var(--color-bg-brand-base); +} + +.previewHeading { + font-size: 22px; + line-height: 1.3; + margin-bottom: 4px; +} + +.actions { + display: flex; + flex-direction: column; + justify-content: center; +} + +$bottomsheet-breakpoint: 630px; + +.shareButtonWrapper { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + width: 100%; + + & > button { + flex: 1; + min-width: 220px; + white-space: normal; + + @media (width > $bottomsheet-breakpoint) { + max-width: 50%; + } + } +} + +.closeButtonDesktop { + position: absolute; + top: 4px; + inset-inline-end: 4px; + padding: 8px; + + @media (width <= $bottomsheet-breakpoint) { + display: none; + } +} + +.closeButtonMobile { + margin-top: 16px; + margin-bottom: -18px; + + @media (width > $bottomsheet-breakpoint) { + display: none; + } +} diff --git a/app/javascript/mastodon/features/collections/detail/share_modal.tsx b/app/javascript/mastodon/features/collections/detail/share_modal.tsx new file mode 100644 index 0000000000..3bff066ee6 --- /dev/null +++ b/app/javascript/mastodon/features/collections/detail/share_modal.tsx @@ -0,0 +1,141 @@ +import { useCallback } from 'react'; + +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { useLocation } from 'react-router'; + +import { me } from '@/mastodon/initial_state'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { changeCompose, focusCompose } from 'mastodon/actions/compose'; +import type { ApiCollectionJSON } from 'mastodon/api_types/collections'; +import { AvatarById } from 'mastodon/components/avatar'; +import { AvatarGroup } from 'mastodon/components/avatar_group'; +import { Button } from 'mastodon/components/button'; +import { CopyLinkField } from 'mastodon/components/form_fields'; +import { IconButton } from 'mastodon/components/icon_button'; +import { ModalShell } from 'mastodon/components/modal_shell'; +import { useAppDispatch } from 'mastodon/store'; + +import { AuthorNote } from '.'; +import classes from './share_modal.module.scss'; + +const messages = defineMessages({ + shareTextOwn: { + id: 'collection.share_template_own', + defaultMessage: 'Check out my new collection: {link}', + }, + shareTextOther: { + id: 'collection.share_template_other', + defaultMessage: 'Check out this cool collection: {link}', + }, +}); + +export const CollectionShareModal: React.FC<{ + collection: ApiCollectionJSON; + onClose: () => void; +}> = ({ collection, onClose }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const location = useLocation<{ newCollection?: boolean }>(); + const isNew = !!location.state.newCollection; + const isOwnCollection = collection.account_id === me; + + const collectionLink = `${window.location.origin}/collections/${collection.id}`; + + const handleShareOnDevice = useCallback(() => { + void navigator.share({ + url: collectionLink, + }); + }, [collectionLink]); + + const handleShareViaPost = useCallback(() => { + const shareMessage = isOwnCollection + ? intl.formatMessage(messages.shareTextOwn, { + link: collectionLink, + }) + : intl.formatMessage(messages.shareTextOther, { + link: collectionLink, + }); + + onClose(); + dispatch(changeCompose(shareMessage)); + dispatch(focusCompose()); + }, [collectionLink, dispatch, intl, isOwnCollection, onClose]); + + return ( + + +

+ {isNew ? ( + + ) : ( + + )} +

+ + + +
+
+

{collection.name}

+ +
+ + {collection.items.slice(0, 5).map(({ account_id }) => { + if (!account_id) return; + return ( + + ); + })} + +
+ + +
+ + +
+ + {'share' in navigator && ( + + )} +
+ + +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/collections/detail/styles.module.scss b/app/javascript/mastodon/features/collections/detail/styles.module.scss index cb94f2894c..690ec29f71 100644 --- a/app/javascript/mastodon/features/collections/detail/styles.module.scss +++ b/app/javascript/mastodon/features/collections/detail/styles.module.scss @@ -48,6 +48,10 @@ color: var(--color-text-secondary); } +.previewAuthorNote { + font-size: 13px; +} + .metaData { margin-top: 16px; font-size: 15px; diff --git a/app/javascript/mastodon/features/collections/editor/details.tsx b/app/javascript/mastodon/features/collections/editor/details.tsx index e8d99df4dd..6234bca514 100644 --- a/app/javascript/mastodon/features/collections/editor/details.tsx +++ b/app/javascript/mastodon/features/collections/editor/details.tsx @@ -127,7 +127,9 @@ export const CollectionDetails: React.FC<{ history.replace( `/collections/${result.payload.collection.id}/edit/details`, ); - history.push(`/collections/${result.payload.collection.id}`); + history.push(`/collections/${result.payload.collection.id}`, { + newCollection: true, + }); } }); } diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx index 2ea413208e..385ec6a794 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'mastodon/components/button'; +import { ModalShell } from 'mastodon/components/modal_shell'; export interface BaseConfirmationModalProps { onClose: () => void; @@ -56,53 +57,49 @@ export const ConfirmationModal: React.FC< }, [onClose, onSecondary]); return ( -
-
-
-

{title}

- {message &&

{message}

} + + +

{title}

+ {message &&

{message}

} - {extraContent ?? children} -
-
+ {extraContent ?? children} + -
-
- - - {secondary && ( - <> -
- - + + - {/* eslint-disable jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */} - - {/* eslint-enable */} -
-
-
+ {secondary && ( + <> +
+ + + )} + + {/* eslint-disable jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */} + + {/* eslint-enable */} + + ); }; diff --git a/app/javascript/mastodon/features/ui/components/dialog_modal.tsx b/app/javascript/mastodon/features/ui/components/dialog_modal.tsx new file mode 100644 index 0000000000..8c850fa0d3 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/dialog_modal.tsx @@ -0,0 +1,94 @@ +import type { FC, ReactNode } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import { Button } from '@/mastodon/components/button'; +import { IconButton } from '@/mastodon/components/icon_button'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; + +export type { BaseConfirmationModalProps as DialogModalProps } from './confirmation_modals/confirmation_modal'; + +interface DialogModalProps { + className?: string; + title: ReactNode; + onClose: () => void; + description?: ReactNode; + formClassName?: string; + children?: ReactNode; + noCancelButton?: boolean; + onSave?: () => void; + saveLabel?: ReactNode; +} + +export const DialogModal: FC = ({ + className, + title, + onClose, + description, + formClassName, + children, + noCancelButton = false, + onSave, + saveLabel, +}) => { + const intl = useIntl(); + + const showButtons = !noCancelButton || onSave; + + return ( +
+
+ + +

{title}

+
+ +
+ {description && ( +
+ {description} +
+ )} +
+ {children} +
+
+ + {showButtons && ( +
+ {!noCancelButton && ( + + )} + {onSave && ( + + )} +
+ )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 7cacfab800..6086858e3d 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -11,6 +11,7 @@ import { DomainBlockModal, ReportModal, ReportCollectionModal, + ShareCollectionModal, EmbedModal, ListAdder, CompareHistoryModal, @@ -79,6 +80,7 @@ export const MODAL_COMPONENTS = { 'DOMAIN_BLOCK': DomainBlockModal, 'REPORT': ReportModal, 'REPORT_COLLECTION': ReportCollectionModal, + 'SHARE_COLLECTION': ShareCollectionModal, 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, 'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }), @@ -95,6 +97,7 @@ export const MODAL_COMPONENTS = { 'ACCOUNT_FIELD_OVERFLOW': () => import('@/mastodon/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })), 'ACCOUNT_EDIT_NAME': () => import('@/mastodon/features/account_edit/components/name_modal').then(module => ({ default: module.NameModal })), 'ACCOUNT_EDIT_BIO': () => import('@/mastodon/features/account_edit/components/bio_modal').then(module => ({ default: module.BioModal })), + 'ACCOUNT_EDIT_PROFILE_DISPLAY': () => import('@/mastodon/features/account_edit/components/profile_display_modal').then(module => ({ default: module.ProfileDisplayModal })), }; export default class ModalRoot extends PureComponent { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index d6c0f70c70..099be340d2 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -62,6 +62,12 @@ export function CollectionsEditor() { ); } +export function ShareCollectionModal() { + return import('../../collections/detail/share_modal').then( + module => ({default: module.CollectionShareModal}) + ); +} + export function Status () { return import('../../status'); } diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 5e20ae1b08..c3e879c7c2 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Знайсці іншы сервер", "closed_registrations_modal.preamble": "Mastodon дэцэнтралізаваны, так што дзе б вы ні стварылі ўліковы запіс, вы зможаце падпісвацца і камунікаваць з кім хочаце на гэтым серверы. Вы нават можаце стварыць свой!", "closed_registrations_modal.title": "Рэгістрацыя ў Mastodon", + "collection.share_modal.share_link_label": "Падзяліцца спасылкай", + "collection.share_modal.share_via_post": "Апублікаваць у Mastodon", + "collection.share_modal.share_via_system": "Падзяліцца ў…", + "collection.share_modal.title": "Падзяліцца калекцыяй", + "collection.share_modal.title_new": "Падзяліцеся сваёй калекцыяй!", + "collection.share_template_other": "Глядзі, якая класная калекцыя: {link}", + "collection.share_template_own": "Глядзі, у мяне новая калекцыя: {link}", "collections.account_count": "{count, plural,one {# уліковы запіс} few {# уліковыя запісы} other {# уліковых запісаў}}", "collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў, на якія Вы падпісаныя", "collections.accounts.empty_title": "Гэтая калекцыя пустая", @@ -448,6 +455,7 @@ "conversation.open": "Прагледзець размову", "conversation.with": "З {names}", "copy_icon_button.copied": "Скапіявана ў буфер абмену", + "copy_icon_button.copy_this_text": "Скапіяваць спасылку ў буфер абмену", "copypaste.copied": "Скапіравана", "copypaste.copy_to_clipboard": "Скапіяваць у буфер абмену", "directory.federated": "З вядомага федэральнага сусвету", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 401dfc2948..4fcb4cc5e2 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -271,6 +271,12 @@ "closed_registrations_modal.find_another_server": "Find en anden server", "closed_registrations_modal.preamble": "Mastodon er decentraliseret, så uanset hvor du opretter din konto, vil du være i stand til at følge og interagere med hvem som helst på denne server. Du kan endda selv være vært for den!", "closed_registrations_modal.title": "Oprettelse på Mastodon", + "collection.share_modal.share_link_label": "Invitationlink til deling", + "collection.share_modal.share_via_system": "Del med…", + "collection.share_modal.title": "Del samling", + "collection.share_modal.title_new": "Del din nye samling!", + "collection.share_template_other": "Tjek denne seje samling: {link}", + "collection.share_template_own": "Tjek min nye samling: {link}", "collections.account_count": "{count, plural, one {# konto} other {# konti}}", "collections.accounts.empty_description": "Tilføj op til {count} konti, du følger", "collections.accounts.empty_title": "Denne samling er tom", @@ -448,6 +454,7 @@ "conversation.open": "Vis samtale", "conversation.with": "Med {names}", "copy_icon_button.copied": "Kopieret til udklipsholderen", + "copy_icon_button.copy_this_text": "Kopiér link til udklipsholderen", "copypaste.copied": "Kopieret", "copypaste.copy_to_clipboard": "Kopiér til udklipsholder", "directory.federated": "Fra kendt fediverse", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index c4e62d65bc..ce49025b5a 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Anderen Server suchen", "closed_registrations_modal.preamble": "Mastodon ist dezentralisiert, das heißt, unabhängig davon, wo du dein Konto erstellst, kannst du jedem Profil auf diesem Server folgen und mit ihm interagieren. Du kannst sogar deinen eigenen Mastodon-Server hosten!", "closed_registrations_modal.title": "Bei Mastodon registrieren", + "collection.share_modal.share_link_label": "Link zum Teilen", + "collection.share_modal.share_via_post": "Auf Mastodon veröffentlichen", + "collection.share_modal.share_via_system": "Teilen …", + "collection.share_modal.title": "Sammlung teilen", + "collection.share_modal.title_new": "Teile deine neue Sammlung!", + "collection.share_template_other": "Seht euch diese coole Sammlung an: {link}", + "collection.share_template_own": "Seht euch meine neue Sammlung an: {link}", "collections.account_count": "{count, plural, one {# Konto} other {# Konten}}", "collections.accounts.empty_description": "Füge bis zu {count} Konten, denen du folgst, hinzu", "collections.accounts.empty_title": "Diese Sammlung ist leer", @@ -448,6 +455,7 @@ "conversation.open": "Unterhaltung anzeigen", "conversation.with": "Mit {names}", "copy_icon_button.copied": "In die Zwischenablage kopiert", + "copy_icon_button.copy_this_text": "Link in die Zwischenablage kopieren", "copypaste.copied": "Kopiert", "copypaste.copy_to_clipboard": "In die Zwischenablage kopieren", "directory.federated": "Aus bekanntem Fediverse", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 8a38e7c0ce..68de788149 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Βρες άλλον διακομιστή", "closed_registrations_modal.preamble": "Το Mastodon είναι αποκεντρωμένο, οπότε ανεξάρτητα από το πού θα δημιουργήσεις τον λογαριασμό σου, μπορείς να ακολουθήσεις και να αλληλεπιδράσεις με οποιονδήποτε σε αυτόν τον διακομιστή. Μπορείς ακόμη και να κάνεις τον δικό σου!", "closed_registrations_modal.title": "Εγγραφή στο Mastodon", + "collection.share_modal.share_link_label": "Σύνδεσμος κοινοποίησης", + "collection.share_modal.share_via_post": "Ανάρτηση στο Mastodon", + "collection.share_modal.share_via_system": "Κοινοποίηση σε…", + "collection.share_modal.title": "Κοινοποίηση συλλογής", + "collection.share_modal.title_new": "Μοιραστείτε τη νέα σας συλλογή!", + "collection.share_template_other": "Δείτε αυτή την ωραία συλλογή: {link}", + "collection.share_template_own": "Δείτε τη νέα μου συλλογή: {link}", "collections.account_count": "{count, plural, one {# λογαριασμός} other {# λογαριασμοί}}", "collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς που ακολουθείτε", "collections.accounts.empty_title": "Αυτή η συλλογή είναι κενή", @@ -448,6 +455,7 @@ "conversation.open": "Προβολή συνομιλίας", "conversation.with": "Με {names}", "copy_icon_button.copied": "Αντιγράφηκε στο πρόχειρο", + "copy_icon_button.copy_this_text": "Αντιγραφή συνδέσμου στο πρόχειρο", "copypaste.copied": "Αντιγράφηκε", "copypaste.copy_to_clipboard": "Αντιγραφή στο πρόχειρο", "directory.federated": "Από το γνωστό fediverse", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index d5b1e24ef4..cac4cf72e0 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Find another server", "closed_registrations_modal.preamble": "Mastodon is decentralised, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!", "closed_registrations_modal.title": "Signing up on Mastodon", + "collection.share_modal.share_link_label": "Invite share link", + "collection.share_modal.share_via_post": "Post on Mastodon", + "collection.share_modal.share_via_system": "Share to…", + "collection.share_modal.title": "Share collection", + "collection.share_modal.title_new": "Share your new collection!", + "collection.share_template_other": "Check out this cool collection: {link}", + "collection.share_template_own": "Check out my new collection: {link}", "collections.account_count": "{count, plural, one {# account} other {# accounts}}", "collections.accounts.empty_description": "Add up to {count} accounts you follow", "collections.accounts.empty_title": "This collection is empty", @@ -448,6 +455,7 @@ "conversation.open": "View conversation", "conversation.with": "With {names}", "copy_icon_button.copied": "Copied to clipboard", + "copy_icon_button.copy_this_text": "Copy link to clipboard", "copypaste.copied": "Copied", "copypaste.copy_to_clipboard": "Copy to clipboard", "directory.federated": "From known fediverse", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index e95ac60420..03b7a5d2c4 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -161,6 +161,15 @@ "account_edit.featured_hashtags.title": "Featured hashtags", "account_edit.name_modal.add_title": "Add display name", "account_edit.name_modal.edit_title": "Edit display name", + "account_edit.profile_tab.button_label": "Customize", + "account_edit.profile_tab.hint.description": "These settings customize what users see on {server} in the official apps, but they may not apply to users on other servers and 3rd party apps.", + "account_edit.profile_tab.hint.title": "Displays still vary", + "account_edit.profile_tab.show_featured.description": "‘Featured’ is an optional tab where you can showcase other accounts.", + "account_edit.profile_tab.show_featured.title": "Show ‘Featured’ tab", + "account_edit.profile_tab.show_media.description": "‘Media’ is an optional tab that shows your posts containing images or videos.", + "account_edit.profile_tab.show_media.title": "Show ‘Media’ tab", + "account_edit.profile_tab.show_media_replies.description": "When enabled, Media tab shows both your posts and replies to other people’s posts.", + "account_edit.profile_tab.show_media_replies.title": "Include replies on ‘Media’ tab", "account_edit.profile_tab.subtitle": "Customize the tabs on your profile and what they display.", "account_edit.profile_tab.title": "Profile tab settings", "account_edit.save": "Save", @@ -271,6 +280,13 @@ "closed_registrations_modal.find_another_server": "Find another server", "closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!", "closed_registrations_modal.title": "Signing up on Mastodon", + "collection.share_modal.share_link_label": "Invite share link", + "collection.share_modal.share_via_post": "Post on Mastodon", + "collection.share_modal.share_via_system": "Share to…", + "collection.share_modal.title": "Share collection", + "collection.share_modal.title_new": "Share your new collection!", + "collection.share_template_other": "Check out this cool collection: {link}", + "collection.share_template_own": "Check out my new collection: {link}", "collections.account_count": "{count, plural, one {# account} other {# accounts}}", "collections.accounts.empty_description": "Add up to {count} accounts you follow", "collections.accounts.empty_title": "This collection is empty", @@ -448,6 +464,7 @@ "conversation.open": "View conversation", "conversation.with": "With {names}", "copy_icon_button.copied": "Copied to clipboard", + "copy_icon_button.copy_this_text": "Copy link to clipboard", "copypaste.copied": "Copied", "copypaste.copy_to_clipboard": "Copy to clipboard", "directory.federated": "From known fediverse", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 53676366cc..3e81941bc8 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Buscar otro servidor", "closed_registrations_modal.preamble": "Mastodon es descentralizado, por lo que no importa dónde creés tu cuenta, podrás seguir e interactuar con cualquier persona en este servidor. ¡Incluso podés montar tu propio servidor!", "closed_registrations_modal.title": "Registrarse en Mastodon", + "collection.share_modal.share_link_label": "Enlace para compartir", + "collection.share_modal.share_via_post": "Enviar a Mastodon", + "collection.share_modal.share_via_system": "Compartir en…", + "collection.share_modal.title": "Compartir colección", + "collection.share_modal.title_new": "¡Compartí tu nueva colección!", + "collection.share_template_other": "¡Mirá qué copada está esta colección! {link}", + "collection.share_template_own": "Mirá mi nueva colección: {link}", "collections.account_count": "{count, plural, one {# hora} other {# horas}}", "collections.accounts.empty_description": "Agregá hasta {count} cuentas que seguís", "collections.accounts.empty_title": "Esta colección está vacía", @@ -448,6 +455,7 @@ "conversation.open": "Ver conversación", "conversation.with": "Con {names}", "copy_icon_button.copied": "Copiado en el portapapeles", + "copy_icon_button.copy_this_text": "Copiar enlace al portapapeles", "copypaste.copied": "Copiado", "copypaste.copy_to_clipboard": "Copiar al portapapeles", "directory.federated": "Desde fediverso conocido", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 9983265adf..51c9882261 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Buscar otro servidor", "closed_registrations_modal.preamble": "Mastodon es descentralizado, por lo que no importa dónde crees tu cuenta, podrás seguir e interactuar con cualquier persona en este servidor. ¡Incluso puedes alojarlo tú mismo!", "closed_registrations_modal.title": "Registrarse en Mastodon", + "collection.share_modal.share_link_label": "Enlace para compartir", + "collection.share_modal.share_via_post": "Publicar en Mastodon", + "collection.share_modal.share_via_system": "Compartir con…", + "collection.share_modal.title": "Compartir la colección", + "collection.share_modal.title_new": "¡Comparte tu nueva colección!", + "collection.share_template_other": "Echa un vistazo a esta increíble colección: {link}", + "collection.share_template_own": "Echa un vistazo a mi nueva colección: {link}", "collections.account_count": "{count, plural,one {# cuenta} other {# cuentas}}", "collections.accounts.empty_description": "Añade hasta {count} cuentas que sigues", "collections.accounts.empty_title": "Esta colección está vacía", @@ -448,6 +455,7 @@ "conversation.open": "Ver conversación", "conversation.with": "Con {names}", "copy_icon_button.copied": "Copiado al portapapeles", + "copy_icon_button.copy_this_text": "Copiar enlace al portapapeles", "copypaste.copied": "Copiado", "copypaste.copy_to_clipboard": "Copiar al portapapeles", "directory.federated": "Desde el fediverso conocido", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 22102bae45..62abafc4bc 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Buscar otro servidor", "closed_registrations_modal.preamble": "Mastodon es descentralizado, por lo que no importa dónde crees tu cuenta, podrás seguir e interactuar con cualquier persona en este servidor. ¡Incluso puedes alojarlo tú mismo!", "closed_registrations_modal.title": "Registrarse en Mastodon", + "collection.share_modal.share_link_label": "Enlace para compartir", + "collection.share_modal.share_via_post": "Publicar en Mastodon", + "collection.share_modal.share_via_system": "Compartir con…", + "collection.share_modal.title": "Compartir la colección", + "collection.share_modal.title_new": "¡Comparte tu nueva colección!", + "collection.share_template_other": "Echa un vistazo a esta fantástica colección: {link}", + "collection.share_template_own": "Echa un vistazo a mi nueva colección: {link}", "collections.account_count": "{count, plural, one {# cuenta} other {# cuentas}}", "collections.accounts.empty_description": "Añade hasta {count} cuentas que sigas", "collections.accounts.empty_title": "Esta colección está vacía", @@ -307,7 +314,7 @@ "collections.no_collections_yet": "Aún no hay colecciones.", "collections.old_last_post_note": "Última publicación hace más de una semana", "collections.remove_account": "Borrar esta cuenta", - "collections.report_collection": "Reportar esta colección", + "collections.report_collection": "Informar de esta colección", "collections.search_accounts_label": "Buscar cuentas para añadir…", "collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas", "collections.sensitive": "Sensible", @@ -448,6 +455,7 @@ "conversation.open": "Ver conversación", "conversation.with": "Con {names}", "copy_icon_button.copied": "Copiado al portapapeles", + "copy_icon_button.copy_this_text": "Copiar enlace al portapapeles", "copypaste.copied": "Copiado", "copypaste.copy_to_clipboard": "Copiar al portapapeles", "directory.federated": "Desde el fediverso conocido", @@ -977,7 +985,7 @@ "report.category.title_account": "perfil", "report.category.title_status": "publicación", "report.close": "Hecho", - "report.collection_comment": "¿Por qué quieres reportar esta colección?", + "report.collection_comment": "¿Por qué quieres informar de esta colección?", "report.comment.title": "¿Hay algo más que creas que deberíamos saber?", "report.forward": "Reenviar a {target}", "report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?", @@ -999,7 +1007,7 @@ "report.rules.title": "¿Qué normas se están violando?", "report.statuses.subtitle": "Selecciona todos los que correspondan", "report.statuses.title": "¿Hay alguna publicación que respalde este informe?", - "report.submission_error": "No se pudo enviar el reporte", + "report.submission_error": "No se pudo enviar el informe", "report.submission_error_details": "Comprueba tu conexión de red e inténtalo más tarde.", "report.submit": "Enviar", "report.target": "Reportando {target}", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 9467d2802a..417947a25b 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Finn ein annan ambætara", "closed_registrations_modal.preamble": "Mastodon er desentraliserað, so óansæð hvar tú stovnar tína kontu, so ber til hjá tær at fylgja og virka saman við einum og hvørjum á hesum ambætaranum. Tað ber enntá til at hýsa tí sjálvi!", "closed_registrations_modal.title": "At stovna kontu á Mastodon", + "collection.share_modal.share_link_label": "Innbjóðingarleinki at deila", + "collection.share_modal.share_via_post": "Posta á Mastodon", + "collection.share_modal.share_via_system": "Deil til…", + "collection.share_modal.title": "Deil savn", + "collection.share_modal.title_new": "Deil títt nýggja savn!", + "collection.share_template_other": "Hygg at hesum kula savninum: {link}", + "collection.share_template_own": "Hygg at mínum nýggja savni: {link}", "collections.account_count": "{count, plural, one {# konta} other {# kontur}}", "collections.accounts.empty_description": "Legg afturat upp til {count} kontur, sum tú fylgir", "collections.accounts.empty_title": "Hetta savnið er tómt", @@ -448,6 +455,7 @@ "conversation.open": "Vís samrøðu", "conversation.with": "Við {names}", "copy_icon_button.copied": "Avritað til setiborðið", + "copy_icon_button.copy_this_text": "Avrita leinki til setiborðið", "copypaste.copied": "Avritað", "copypaste.copy_to_clipboard": "Avrita til setiborðið", "directory.federated": "Frá tí kenda fediversinum", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 4d3421be05..54036c953f 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -48,6 +48,7 @@ "account.featured.hashtags": "Hashtags", "account.featured_tags.last_status_at": "Dernière publication {date}", "account.featured_tags.last_status_never": "Aucune publication", + "account.field_overflow": "Voir tout", "account.filters.all": "Toutes les activités", "account.filters.boosts_toggle": "Afficher les partages", "account.filters.posts_boosts": "Messages et partages", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index dcbfb5af34..18daca7893 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -48,6 +48,7 @@ "account.featured.hashtags": "Hashtags", "account.featured_tags.last_status_at": "Dernier message le {date}", "account.featured_tags.last_status_never": "Aucun message", + "account.field_overflow": "Voir tout", "account.filters.all": "Toutes les activités", "account.filters.boosts_toggle": "Afficher les partages", "account.filters.posts_boosts": "Messages et partages", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 5bc25e66cc..5ae8d75fce 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Faigh freastalaí eile", "closed_registrations_modal.preamble": "Ós rud é go bhfuil Mastodon díláraithe, is cuma cá háit a chruthaíonn tú do chuntas, beidh tú in ann idirghníomhú le haon duine ar an bhfreastalaí seo agus iad a leanúint. Is féidir fiú é a féin-óstáil!", "closed_registrations_modal.title": "Cláraigh le Mastodon", + "collection.share_modal.share_link_label": "Nasc roinnte cuireadh", + "collection.share_modal.share_via_post": "Postáil ar Mastodon", + "collection.share_modal.share_via_system": "Comhroinn le…", + "collection.share_modal.title": "Comhroinn bailiúchán", + "collection.share_modal.title_new": "Roinn do bhailiúchán nua!", + "collection.share_template_other": "Féach ar an mbailiúchán fionnuar seo: {link}", + "collection.share_template_own": "Féach ar mo bhailiúchán nua: {link}", "collections.account_count": "{count, plural, one {# cuntas} two {# cuntais} few {# cuntais} many {# cuntais} other {# cuntais}}", "collections.accounts.empty_description": "Cuir suas le {count} cuntas leis a leanann tú", "collections.accounts.empty_title": "Tá an bailiúchán seo folamh", @@ -448,6 +455,7 @@ "conversation.open": "Féach ar comhrá", "conversation.with": "Le {names}", "copy_icon_button.copied": "Cóipeáladh chuig an ngearrthaisce", + "copy_icon_button.copy_this_text": "Cóipeáil nasc chuig an ghearrthaisce", "copypaste.copied": "Cóipeáilte", "copypaste.copy_to_clipboard": "Cóipeáil chuig an ngearrthaisce", "directory.federated": "Ó chomhchruinne aitheanta", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 3bbde7a51b..ee31fcac5d 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Finna annan netþjón", "closed_registrations_modal.preamble": "Mastodon er ekki miðstýrt, svo það skiptir ekki máli hvar þú býrð til aðgang; þú munt get fylgt eftir og haft samskipti við hvern sem er á þessum þjóni. Þú getur jafnvel hýst þinn eigin Mastodon þjón!", "closed_registrations_modal.title": "Að nýskrá sig á Mastodon", + "collection.share_modal.share_link_label": "Tengill til að deila", + "collection.share_modal.share_via_post": "Birta á Mastodon", + "collection.share_modal.share_via_system": "Deila með…", + "collection.share_modal.title": "Deila safni", + "collection.share_modal.title_new": "Deildu nýja safninu þínu!", + "collection.share_template_other": "Kíktu á þetta áhugaverða safn: {link}", + "collection.share_template_own": "Kíktu á nýja safnið mitt: {link}", "collections.account_count": "{count, plural, one {# aðgangur} other {# aðgangar}}", "collections.accounts.empty_description": "Bættu við allt að {count} aðgöngum sem þú fylgist með", "collections.accounts.empty_title": "Þetta safn er tómt", @@ -448,6 +455,7 @@ "conversation.open": "Skoða samtal", "conversation.with": "Við {names}", "copy_icon_button.copied": "Afritað á klippispjald", + "copy_icon_button.copy_this_text": "Afrita tengil á klippispjald", "copypaste.copied": "Afritað", "copypaste.copy_to_clipboard": "Afrita á klippispjald", "directory.federated": "Frá samtengdum vefþjónum", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 8802c730b8..9746153961 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -44,9 +44,11 @@ "account.familiar_followers_two": "Seguito da {name1} e {name2}", "account.featured": "In primo piano", "account.featured.accounts": "Profili", + "account.featured.collections": "Collezioni", "account.featured.hashtags": "Hashtag", "account.featured_tags.last_status_at": "Ultimo post il {date}", "account.featured_tags.last_status_never": "Nessun post", + "account.field_overflow": "Mostra il contenuto completo", "account.filters.all": "Tutte le attività", "account.filters.boosts_toggle": "Mostra le condivisioni", "account.filters.posts_boosts": "Post e condivisioni", @@ -269,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Trova un altro server", "closed_registrations_modal.preamble": "Mastodon è decentralizzato, quindi, non importa dove crei il tuo profilo, potrai seguire e interagire con chiunque su questo server. Anche se sei tu stesso a ospitarlo!", "closed_registrations_modal.title": "Registrazione su Mastodon", + "collection.share_modal.share_link_label": "Condividi il collegamento d'invito", + "collection.share_modal.share_via_post": "Pubblica su Mastodon", + "collection.share_modal.share_via_system": "Condividi con…", + "collection.share_modal.title": "Condividi la collezione", + "collection.share_modal.title_new": "Condividi la tua nuova collezione!", + "collection.share_template_other": "Dai un'occhiata a questa fantastica collezione: {link}", + "collection.share_template_own": "Dai un'occhiata alla mia collezione: {link}", "collections.account_count": "{count, plural, one {# account} other {# account}}", "collections.accounts.empty_description": "Aggiungi fino a {count} account che segui", "collections.accounts.empty_title": "Questa collezione è vuota", @@ -446,6 +455,7 @@ "conversation.open": "Visualizza conversazione", "conversation.with": "Con {names}", "copy_icon_button.copied": "Copiato negli appunti", + "copy_icon_button.copy_this_text": "Copia il link negli appunti", "copypaste.copied": "Copiato", "copypaste.copy_to_clipboard": "Copia negli Appunti", "directory.federated": "Da un fediverse noto", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 084cff2c85..3bdec56300 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -48,6 +48,7 @@ "account.featured.hashtags": "Hashtags", "account.featured_tags.last_status_at": "Laatste bericht op {date}", "account.featured_tags.last_status_never": "Geen berichten", + "account.field_overflow": "Volledige inhoud tonen", "account.filters.all": "Alle activiteit", "account.filters.boosts_toggle": "Boosts tonen", "account.filters.posts_boosts": "Berichten en boosts", @@ -270,6 +271,12 @@ "closed_registrations_modal.find_another_server": "Een andere server zoeken", "closed_registrations_modal.preamble": "Mastodon is gedecentraliseerd. Op welke server je ook een account hebt, je kunt overal vandaan mensen op deze server volgen en er mee interactie hebben. Je kunt zelfs zelf een Mastodon-server hosten!", "closed_registrations_modal.title": "Registreren op Mastodon", + "collection.share_modal.share_via_post": "Bericht op Mastodon", + "collection.share_modal.share_via_system": "Delen met…", + "collection.share_modal.title": "Verzameling delen", + "collection.share_modal.title_new": "Deel je nieuwe verzameling!", + "collection.share_template_other": "Bekijk deze coole verzameling: {link}", + "collection.share_template_own": "Bekijk mijn nieuwe verzameling: {link}", "collections.account_count": "{count, plural, one {# account} other {# accounts}}", "collections.accounts.empty_description": "Voeg tot {count} accounts toe die je volgt", "collections.accounts.empty_title": "Deze verzameling is leeg", @@ -447,6 +454,7 @@ "conversation.open": "Gesprek tonen", "conversation.with": "Met {names}", "copy_icon_button.copied": "Gekopieerd naar klembord", + "copy_icon_button.copy_this_text": "Link kopiëren naar klembord", "copypaste.copied": "Gekopieerd", "copypaste.copy_to_clipboard": "Naar klembord kopiëren", "directory.federated": "Fediverse (wat bekend is)", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 74147e6c59..56f3149fdb 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -48,6 +48,7 @@ "account.featured.hashtags": "Etiquetas", "account.featured_tags.last_status_at": "Última publicação em {date}", "account.featured_tags.last_status_never": "Sem publicações", + "account.field_overflow": "Mostrar todo o conteúdo", "account.filters.all": "Toda a atividade", "account.filters.boosts_toggle": "Mostrar partilhas", "account.filters.posts_boosts": "Publicações e partilhas", @@ -162,8 +163,10 @@ "account_edit.name_modal.edit_title": "Editar o nome a mostrar", "account_edit.save": "Guardar", "account_edit_tags.column_title": "Editar etiquetas em destaque", + "account_edit_tags.help_text": "As etiquetas destacadas ajudam os utilizadores a descobrir e interagir com o seu perfil. Aparecem como filtros na vista de atividade da sua página de perfil.", "account_edit_tags.search_placeholder": "Insira uma etiqueta…", "account_edit_tags.suggestions": "Sugestões:", + "account_edit_tags.tag_status_count": "{count, plural, one {# publicação} other {# publicações}}", "account_note.placeholder": "Clicar para adicionar nota", "admin.dashboard.daily_retention": "Taxa de retenção de utilizadores por dia após a inscrição", "admin.dashboard.monthly_retention": "Taxa de retenção de utilizadores por mês após a inscrição", @@ -267,6 +270,7 @@ "closed_registrations_modal.preamble": "O Mastodon é descentralizado, por isso não importa onde a tua conta é criada, pois continuarás a poder acompanhar e interagir com qualquer um neste servidor. Podes até alojar o teu próprio servidor!", "closed_registrations_modal.title": "Criar uma conta no Mastodon", "collections.account_count": "{count, plural, one {# conta} other {# contas}}", + "collections.accounts.empty_description": "Adicione até {count} contas que segue", "collections.accounts.empty_title": "Esta coleção está vazia", "collections.collection_description": "Descrição", "collections.collection_name": "Nome", @@ -287,15 +291,25 @@ "collections.detail.share": "Partilhar esta coleção", "collections.edit_details": "Editar detalhes", "collections.error_loading_collections": "Ocorreu um erro ao tentar carregar as suas coleções.", + "collections.hints.accounts_counter": "{count} / {max} contas", "collections.hints.add_more_accounts": "Adicione pelo menos {count, plural, one {# conta} other {# contas}} para continuar", + "collections.hints.can_not_remove_more_accounts": "As coleções devem conter pelo menos {count, plural, one {# conta} other {# contas}}. Não é possível remover mais contas.", "collections.last_updated_at": "Última atualização: {date}", "collections.manage_accounts": "Gerir contas", "collections.mark_as_sensitive": "Marcar como sensível", "collections.mark_as_sensitive_hint": "Oculta a descrição e as contas da coleção por trás de um aviso de conteúdo. O nome da coleção ainda estará visível.", + "collections.name_length_hint": "Limite de 40 carateres", "collections.new_collection": "Nova coleção", "collections.no_collections_yet": "Ainda não existem coleções.", + "collections.old_last_post_note": "Última publicação há mais de uma semana", + "collections.remove_account": "Remover esta conta", + "collections.report_collection": "Denunciar esta coleção", + "collections.search_accounts_label": "Procurar contas para adicionar…", + "collections.search_accounts_max_reached": "Já adicionou o máximo de contas", + "collections.sensitive": "Sensível", "collections.topic_hint": "Adicione uma etiqueta para ajudar outros a entender o tópico principal desta coleção.", "collections.view_collection": "Ver coleções", + "collections.view_other_collections_by_user": "Ver outras coleções deste utilizador", "collections.visibility_public": "Pública", "collections.visibility_public_hint": "Visível nos resultados de pesquisa e outras áreas onde aparecem recomendações.", "collections.visibility_title": "Visibilidade", @@ -384,6 +398,9 @@ "confirmations.discard_draft.post.title": "Descartar o rascunho da publicação?", "confirmations.discard_edit_media.confirm": "Descartar", "confirmations.discard_edit_media.message": "Tens alterações por guardar na descrição da multimédia ou pré-visualização do conteúdo. Descartar mesmo assim?", + "confirmations.follow_to_collection.confirm": "Seguir e adicionar à coleção", + "confirmations.follow_to_collection.message": "Precisa de seguir {name} para o/a adicionar a uma coleção.", + "confirmations.follow_to_collection.title": "Seguir conta?", "confirmations.follow_to_list.confirm": "Seguir e adicionar à lista", "confirmations.follow_to_list.message": "Tens de seguir {name} para o adicionares a uma lista.", "confirmations.follow_to_list.title": "Seguir utilizador?", @@ -956,6 +973,7 @@ "report.category.title_account": "perfil", "report.category.title_status": "publicação", "report.close": "Concluído", + "report.collection_comment": "Porque quer denunciar esta coleção?", "report.comment.title": "Há mais alguma coisa que devamos saber?", "report.forward": "Reencaminhar para {target}", "report.forward_hint": "A conta pertence a outro servidor. Enviar uma cópia anónima da denúncia para esse servidor também?", @@ -977,6 +995,8 @@ "report.rules.title": "Que regras estão a ser violadas?", "report.statuses.subtitle": "Seleciona tudo o que se aplicar", "report.statuses.title": "Existe alguma publicação que suporte esta denúncia?", + "report.submission_error": "A denúncia não pôde ser enviada", + "report.submission_error_details": "Por favor, verifique a sua ligação à rede e tente novamente mais tarde.", "report.submit": "Enviar", "report.target": "A denunciar {target}", "report.thanks.take_action": "Aqui estão as suas opções para controlar o que vê no Mastodon:", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 7e3c421cce..4fd73e3cea 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -269,6 +269,12 @@ "closed_registrations_modal.find_another_server": "Gjeni shërbyes tjetër", "closed_registrations_modal.preamble": "Mastodon-i është i decentralizuar, ndaj pavarësisht se ku krijoni llogarinë tuaj, do të jeni në gjendje të ndiqni dhe ndërveproni me këdo në këtë shërbyes. Mundeni madje edhe ta strehoni ju vetë!", "closed_registrations_modal.title": "Po regjistroheni në Mastodon", + "collection.share_modal.share_via_post": "Postoje në Mastodon", + "collection.share_modal.share_via_system": "Ndajeni me të tjerë në…", + "collection.share_modal.title": "Ndani koleksionin me të tjerë", + "collection.share_modal.title_new": "Ndani me të tjerë koleksionin tuaj të ri!", + "collection.share_template_other": "Shihni këtë koleksion të hijshëm: {link}", + "collection.share_template_own": "Shihni koleksionin tim të ri: {link}", "collections.account_count": "{count, plural, one {# llogari} other {# llogari}}", "collections.accounts.empty_title": "Ky koleksion është i zbrazët", "collections.collection_description": "Përshkrim", @@ -445,6 +451,7 @@ "conversation.open": "Shfaq bisedën", "conversation.with": "Me {names}", "copy_icon_button.copied": "U kopjua në të papastër", + "copy_icon_button.copy_this_text": "Kopjoje lidhjen në të papastër", "copypaste.copied": "U kopjua", "copypaste.copy_to_clipboard": "Kopjoje në të papastër", "directory.federated": "Nga fedivers i njohur", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index ec7906da57..270f130cad 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -44,9 +44,11 @@ "account.familiar_followers_two": "{name1} ve {name2} tarafından takip ediliyor", "account.featured": "Öne çıkan", "account.featured.accounts": "Profiller", + "account.featured.collections": "Koleksiyonlar", "account.featured.hashtags": "Etiketler", "account.featured_tags.last_status_at": "Son gönderinin tarihi {date}", "account.featured_tags.last_status_never": "Gönderi yok", + "account.field_overflow": "Tüm içeriği göster", "account.filters.all": "Tüm aktiviteler", "account.filters.boosts_toggle": "Yeniden paylaşımları göster", "account.filters.posts_boosts": "Gönderiler ve yeniden paylaşımlar", @@ -269,6 +271,13 @@ "closed_registrations_modal.find_another_server": "Başka sunucu bul", "closed_registrations_modal.preamble": "Mastodon merkeziyetsizdir, bu yüzden hesabınızı nerede oluşturursanız oluşturun, bu sunucudaki herhangi birini takip edebilecek veya onunla etkileşebileceksiniz. Hatta kendi sunucunuzu bile barındırabilirsiniz!", "closed_registrations_modal.title": "Mastodon'a kayıt olmak", + "collection.share_modal.share_link_label": "Davet bağlantısı", + "collection.share_modal.share_via_post": "Mastodon'da paylaş", + "collection.share_modal.share_via_system": "Paylaş…", + "collection.share_modal.title": "Koleksiyonu paylaş", + "collection.share_modal.title_new": "Yeni koleksiyonunuzu paylaşın!", + "collection.share_template_other": "Bu harika koleksiyona göz atın: {link}", + "collection.share_template_own": "Yeni koleksiyonuma göz atın: {link}", "collections.account_count": "{count, plural, one {# hesap} other {# hesap}}", "collections.accounts.empty_description": "Takip ettiğiniz hesapların sayısını {count} kadar artırın", "collections.accounts.empty_title": "Bu koleksiyon boş", @@ -446,6 +455,7 @@ "conversation.open": "Sohbeti görüntüle", "conversation.with": "{names} ile", "copy_icon_button.copied": "Panoya kopyalandı", + "copy_icon_button.copy_this_text": "Bağlantıyı panoya kopyala", "copypaste.copied": "Kopyalandı", "copypaste.copy_to_clipboard": "Panoya kopyala", "directory.federated": "Bilinen fediverse'lerden", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 3d00966ea4..6295ca9b50 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -48,6 +48,7 @@ "account.featured.hashtags": "话题", "account.featured_tags.last_status_at": "上次发言于 {date}", "account.featured_tags.last_status_never": "暂无嘟文", + "account.field_overflow": "显示完整内容", "account.filters.all": "所有活动", "account.filters.boosts_toggle": "显示转嘟", "account.filters.posts_boosts": "嘟文与转嘟", @@ -270,6 +271,13 @@ "closed_registrations_modal.find_another_server": "查找其他服务器", "closed_registrations_modal.preamble": "Mastodon 是去中心化的,所以无论在哪个实例创建账号,都可以关注本服务器上的账号并与之交流。 或者你还可以自己搭建实例!", "closed_registrations_modal.title": "注册 Mastodon 账号", + "collection.share_modal.share_link_label": "分享链接", + "collection.share_modal.share_via_post": "在 Mastodon 上发布", + "collection.share_modal.share_via_system": "分享到…", + "collection.share_modal.title": "分享收藏列表", + "collection.share_modal.title_new": "分享你的新收藏列表!", + "collection.share_template_other": "发现了个收藏列表,来看看:{link}", + "collection.share_template_own": "我的新收藏列表,来看看:{link}", "collections.account_count": "{count, plural, other {# 个账号}}", "collections.accounts.empty_description": "添加你关注的账号,最多 {count} 个", "collections.accounts.empty_title": "收藏列表为空", @@ -447,6 +455,7 @@ "conversation.open": "查看对话", "conversation.with": "与 {names}", "copy_icon_button.copied": "已复制到剪贴板", + "copy_icon_button.copy_this_text": "复制链接到剪贴板", "copypaste.copied": "已复制", "copypaste.copy_to_clipboard": "复制到剪贴板", "directory.federated": "来自已知联邦宇宙", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 186dbb0550..31443295bb 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -271,6 +271,13 @@ "closed_registrations_modal.find_another_server": "尋找另一個伺服器", "closed_registrations_modal.preamble": "Mastodon 是去中心化的,所以無論您於哪個伺服器新增帳號,都可以與此伺服器上的任何人跟隨及互動。您甚至能自行架設自己的伺服器!", "closed_registrations_modal.title": "註冊 Mastodon", + "collection.share_modal.share_link_label": "分享連結", + "collection.share_modal.share_via_post": "於 Mastodon 上發嘟文", + "collection.share_modal.share_via_system": "分享至...", + "collection.share_modal.title": "分享收藏名單", + "collection.share_modal.title_new": "分享您的新收藏名單!", + "collection.share_template_other": "來看看這個酷酷的收藏名單:{link}", + "collection.share_template_own": "來看看我的新收藏名單:{link}", "collections.account_count": "{count, plural, other {# 個帳號}}", "collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號", "collections.accounts.empty_title": "此收藏名單是空的", @@ -448,6 +455,7 @@ "conversation.open": "檢視對話", "conversation.with": "與 {names}", "copy_icon_button.copied": "已複製到剪貼簿", + "copy_icon_button.copy_this_text": "複製連結至剪貼簿", "copypaste.copied": "已複製", "copypaste.copy_to_clipboard": "複製到剪貼簿", "directory.federated": "來自已知聯邦宇宙", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5a906c93a2..1e29fcb2b3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -6408,15 +6408,15 @@ a.status-card { line-height: 20px; color: var(--color-text-secondary); - h1 { + :where(h1) { font-size: 16px; line-height: 24px; color: var(--color-text-primary); font-weight: 500; + } - &:not(:only-child) { - margin-bottom: 8px; - } + :where(h1:not(:only-child)) { + margin-bottom: 8px; } strong { diff --git a/app/lib/account_statuses_filter.rb b/app/lib/account_statuses_filter.rb index 7c2d7e0cb6..e784bc4abd 100644 --- a/app/lib/account_statuses_filter.rb +++ b/app/lib/account_statuses_filter.rb @@ -37,7 +37,7 @@ class AccountStatusesFilter if anonymous? account.statuses.not_local_only.distributable_visibility elsif author? - account.statuses.all # NOTE: #merge! does not work without the #all + exclude_direct? ? account.statuses.where(visibility: %i(public unlisted private)) : account.statuses.all # NOTE: #merge! does not work without the #all elsif blocked? Status.none else @@ -46,9 +46,15 @@ class AccountStatusesFilter end def filtered_scope - scope = account.statuses.left_outer_joins(:mentions) + scope = account.statuses + + if exclude_direct? + scope = scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)) + else + scope = account.statuses.left_outer_joins(:mentions) + scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id])) + end - scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id])) scope.merge!(filtered_reblogs_scope) if reblogs_may_occur? scope @@ -123,6 +129,10 @@ class AccountStatusesFilter truthy_param?(:only_media) end + def exclude_direct? + truthy_param?(:exclude_direct) + end + def exclude_replies? truthy_param?(:exclude_replies) end diff --git a/app/lib/admin/metrics/measure/base_measure.rb b/app/lib/admin/metrics/measure/base_measure.rb index eabbe0890b..88a7cb09a0 100644 --- a/app/lib/admin/metrics/measure/base_measure.rb +++ b/app/lib/admin/metrics/measure/base_measure.rb @@ -94,7 +94,7 @@ class Admin::Metrics::Measure::BaseMeasure end def length_of_period - @length_of_period ||= @end_at - @start_at + @length_of_period ||= @end_at.to_date - @start_at.to_date end def params diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index 5a6284cc5b..4b2f96da63 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -17,9 +17,9 @@ class TagManager def normalize_domain(domain) return if domain.nil? - uri = Addressable::URI.new - uri.host = domain.strip.delete_suffix('/') - uri.normalized_host + Addressable::URI.new.tap do |uri| + uri.host = domain.strip.delete_suffix('/') + end.normalized_host end def local_url?(url) diff --git a/app/models/collection.rb b/app/models/collection.rb index c018dd9fa4..3b8ee82a3c 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -46,6 +46,7 @@ class Collection < ApplicationRecord scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) } scope :with_tag, -> { includes(:tag) } scope :discoverable, -> { where(discoverable: true) } + scope :local, -> { where(local: true) } def remote? !local? diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb index 78b5f6a6e2..c5c9ebc16e 100644 --- a/app/models/collection_item.rb +++ b/app/models/collection_item.rb @@ -40,6 +40,7 @@ class CollectionItem < ApplicationRecord scope :ordered, -> { order(position: :asc) } scope :with_accounts, -> { includes(account: [:account_stat, :user]) } scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) } + scope :local, -> { joins(:collection).merge(Collection.local) } def local_item_with_remote_account? local? && account&.remote? diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb index 76f91c5b64..6571a40c54 100644 --- a/app/models/concerns/domain_normalizable.rb +++ b/app/models/concerns/domain_normalizable.rb @@ -22,7 +22,7 @@ module DomainNormalizable private def normalize_domain - self.domain = TagManager.instance.normalize_domain(domain&.strip) + self.domain = TagManager.instance.normalize_domain(domain) rescue Addressable::URI::InvalidURIError errors.add(:domain, :invalid) end diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb index 9a91ab3bed..a0938d6c0a 100644 --- a/app/models/featured_tag.rb +++ b/app/models/featured_tag.rb @@ -26,7 +26,7 @@ class FeaturedTag < ApplicationRecord normalizes :name, with: ->(name) { name.strip.delete_prefix('#') } - scope :by_name, ->(name) { joins(:tag).where(tag: { name: HashtagNormalizer.new.normalize(name) }) } + scope :by_name, ->(name) { joins(:tag).where(tag: { name: }) } before_validation :set_tag diff --git a/app/models/tag.rb b/app/models/tag.rb index b87fbc4246..97edda879e 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -65,8 +65,9 @@ class Tag < ApplicationRecord .where(statuses: { id: account.statuses.select(:id).limit(RECENT_STATUS_LIMIT) }) .group(:id).order(Arel.star.count.desc) } - scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index + scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(normalize_value_for(:name, term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index + normalizes :name, with: ->(value) { HashtagNormalizer.new.normalize(value) } normalizes :display_name, with: ->(value) { value.gsub(HASHTAG_INVALID_CHARS_RE, '') } update_index('tags', :self) @@ -111,13 +112,13 @@ class Tag < ApplicationRecord class << self def find_or_create_by_names(name_or_names) - names = Array(name_or_names).map { |str| [normalize(str), str] }.uniq(&:first) + names = Array(name_or_names).map { |str| [normalize_value_for(:name, str), str] }.uniq(&:first) - names.map do |(normalized_name, display_name)| + names.map do |name, display_name| tag = begin - matching_name(normalized_name).first || create!(name: normalized_name, display_name:) + matching_name(name).first || create!(name:, display_name:) rescue ActiveRecord::RecordNotUnique - find_normalized(normalized_name) + find_normalized(name) end yield tag if block_given? @@ -148,7 +149,7 @@ class Tag < ApplicationRecord end def matching_name(name_or_names) - names = Array(name_or_names).map { |name| arel_table.lower(normalize(name)) } + names = Array(name_or_names).map { |name| arel_table.lower(normalize_value_for(:name, name)) } if names.size == 1 where(arel_table[:name].lower.eq(names.first)) @@ -156,10 +157,6 @@ class Tag < ApplicationRecord where(arel_table[:name].lower.in(names)) end end - - def normalize(str) - HashtagNormalizer.new.normalize(str) - end end private @@ -173,6 +170,6 @@ class Tag < ApplicationRecord end def display_name_matches_name? - HashtagNormalizer.new.normalize(display_name).casecmp(name).zero? + self.class.normalize_value_for(:name, display_name).casecmp(name).zero? end end diff --git a/app/serializers/activitypub/feature_authorization_serializer.rb b/app/serializers/activitypub/feature_authorization_serializer.rb new file mode 100644 index 0000000000..4181025802 --- /dev/null +++ b/app/serializers/activitypub/feature_authorization_serializer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ActivityPub::FeatureAuthorizationSerializer < ActivityPub::Serializer + include RoutingHelper + + attributes :id, :type, :interacting_object, :interaction_target + + def id + ap_account_feature_authorization_url(object.account_id, object) + end + + def type + 'FeatureAuthorization' + end + + def interaction_target + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def interacting_object + ActivityPub::TagManager.instance.uri_for(object.collection) + end +end diff --git a/app/serializers/activitypub/featured_item_serializer.rb b/app/serializers/activitypub/featured_item_serializer.rb index a524d6c25f..56c0b4390f 100644 --- a/app/serializers/activitypub/featured_item_serializer.rb +++ b/app/serializers/activitypub/featured_item_serializer.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer - attributes :id, :type, :featured_object, :featured_object_type + include RoutingHelper + + attributes :id, :type, :featured_object, :featured_object_type, + :feature_authorization def id ActivityPub::TagManager.instance.uri_for(object) @@ -18,4 +21,12 @@ class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer def featured_object_type object.account.actor_type || 'Person' end + + def feature_authorization + if object.account.local? + ap_account_feature_authorization_url(object.account_id, object) + else + object.approval_uri + end + end end diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 433cd9cb8c..a666450af0 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -77,7 +77,7 @@ class ReportService < BaseService end def forward_to_domains - @forward_to_domains ||= (@options[:forward_to_domains] || [@target_account.domain]).filter_map { |domain| TagManager.instance.normalize_domain(domain&.strip) }.uniq + @forward_to_domains ||= (@options[:forward_to_domains] || [@target_account.domain]).filter_map { |domain| TagManager.instance.normalize_domain(domain) }.uniq end def reported_status_ids diff --git a/app/services/tag_search_service.rb b/app/services/tag_search_service.rb index 6a4af5c9a0..1557c07bf0 100644 --- a/app/services/tag_search_service.rb +++ b/app/services/tag_search_service.rb @@ -39,7 +39,7 @@ class TagSearchService < BaseService def ensure_exact_match(results) return results unless @offset.nil? || @offset.zero? - normalized_query = Tag.normalize(@query) + normalized_query = Tag.normalize_value_for(:name, @query) exact_match = results.find { |tag| tag.name.downcase == normalized_query } exact_match ||= Tag.find_normalized(normalized_query) unless exact_match.nil? diff --git a/config/application.rb b/config/application.rb index 4e58bd9f6c..09fe38065e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -58,7 +58,7 @@ Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true' module Mastodon class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 8.0 + config.load_defaults 8.1 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. diff --git a/config/environments/development.rb b/config/environments/development.rb index 4f3e076e7c..1d9ba5a8fc 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -28,7 +28,7 @@ Rails.application.configure do config.cache_store = :redis_cache_store, REDIS_CONFIGURATION.cache config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.to_i}", + 'cache-control' => "public, max-age=#{2.days.to_i}", } else config.action_controller.perform_caching = false @@ -67,9 +67,18 @@ Rails.application.configure do # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index c31e03d33d..dc32ff2983 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -11,8 +11,10 @@ Rails.application.configure do # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). config.eager_load = true - # Full error reports are disabled and caching is turned on. + # Full error reports are disabled. config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. config.action_controller.perform_caching = true # Do not fallback to assets pipeline if a precompiled asset is missed. @@ -55,9 +57,8 @@ Rails.application.configure do # Use a different cache store in production. config.cache_store = :redis_cache_store, REDIS_CONFIGURATION.cache - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. - config.action_mailer.perform_caching = false + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = '/health' # Don't log any deprecations. config.active_support.report_deprecations = false @@ -105,6 +106,9 @@ Rails.application.configure do 'Referrer-Policy' => 'same-origin', } + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [:id] + # Enable DNS rebinding protection and other `Host` header attacks. # config.hosts = [ # "example.com", # Allow requests from example.com diff --git a/config/initializers/open_redirects.rb b/config/initializers/open_redirects.rb index 1c5a66e07d..821fde784d 100644 --- a/config/initializers/open_redirects.rb +++ b/config/initializers/open_redirects.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true -# TODO: Starting with Rails 7.0, the framework default is true for this setting. -# This location in devise redirects and we can't hook in or override: -# https://github.com/heartcombo/devise/blob/v4.9.3/app/controllers/devise/confirmations_controller.rb#L28 -# When solution is found, this setting can go back to default. -Rails.application.config.action_controller.raise_on_open_redirects = false +# In the Devise confirmations#show action, a redirect_to is called: +# https://github.com/heartcombo/devise/blob/v5.0.0/app/controllers/devise/confirmations_controller.rb#L28 +# +# We override the `after_confirmation_path_for` method in a way which sometimes +# returns raw URLs to external hosts, as part of the auth workflow. +# Discussion: https://github.com/mastodon/mastodon/pull/36505#discussion_r2782876831 + +Rails.application.reloader.to_prepare do + ActionController::Base.action_on_open_redirect = :log +end diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml index 25bd8f7fae..bc616b22ab 100644 --- a/config/locales/activerecord.fr.yml +++ b/config/locales/activerecord.fr.yml @@ -3,7 +3,7 @@ fr: activerecord: attributes: poll: - expires_at: Date d'expiration + expires_at: Date d'échéance options: Choix user: agreement: Contrat de service diff --git a/config/locales/it.yml b/config/locales/it.yml index a9e489f1a5..09dfb6a349 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -267,6 +267,7 @@ it: demote_user_html: "%{name} ha retrocesso l'utente %{target}" destroy_announcement_html: "%{name} ha eliminato l'annuncio %{target}" destroy_canonical_email_block_html: "%{name} ha sbloccato l'e-mail con l'hash %{target}" + destroy_collection_html: "%{name} ha rimosso la collezione di %{target}" destroy_custom_emoji_html: "%{name} ha eliminato l'emoji %{target}" destroy_domain_allow_html: "%{name} non ha consentito la federazione con il dominio %{target}" destroy_domain_block_html: "%{name} ha sbloccato il dominio %{target}" @@ -306,6 +307,7 @@ it: unsilence_account_html: "%{name} ha riattivato l'account di %{target}" unsuspend_account_html: "%{name} ha annullato la sospensione dell'account di %{target}" update_announcement_html: "%{name} ha aggiornato l'annuncio %{target}" + update_collection_html: "%{name} ha aggiornato la collezione di %{target}" update_custom_emoji_html: "%{name} ha aggiornato emoji %{target}" update_domain_block_html: "%{name} ha aggiornato il blocco dominio per %{target}" update_ip_block_html: "%{name} ha cambiato la regola per l'IP %{target}" diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index 6d7f710f0f..b85b5fbd18 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -267,6 +267,7 @@ pt-PT: demote_user_html: "%{name} despromoveu o utilizador %{target}" destroy_announcement_html: "%{name} eliminou a mensagem de manutenção %{target}" destroy_canonical_email_block_html: "%{name} desbloqueou o e-mail com a hash %{target}" + destroy_collection_html: "%{name} removeu a coleção de %{target}" destroy_custom_emoji_html: "%{name} eliminou o emoji %{target}" destroy_domain_allow_html: "%{name} bloqueou a federação com o domínio %{target}" destroy_domain_block_html: "%{name} desbloqueou o domínio %{target}" @@ -306,6 +307,7 @@ pt-PT: unsilence_account_html: "%{name} deixou de limitar a conta de %{target}" unsuspend_account_html: "%{name} desativou a suspensão de %{target}" update_announcement_html: "%{name} atualizou a mensagem de manutenção %{target}" + update_collection_html: "%{name} atualizou a coleção de %{target}" update_custom_emoji_html: "%{name} atualizou o emoji %{target}" update_domain_block_html: "%{name} atualizou o bloqueio de domínio para %{target}" update_ip_block_html: "%{name} alterou regra para o IP %{target}" @@ -686,6 +688,7 @@ pt-PT: cancel: Cancelar category: Categoria category_description_html: A razão pela qual esta conta e/ou conteúdo foi denunciado será citada na comunicação com a conta denunciada + collections: Coleções (%{count}) comment: none: Nenhum comment_description_html: 'Para fornecer mais informações, %{name} escreveu:' @@ -721,6 +724,7 @@ pt-PT: resolved_msg: Denúncia resolvida com sucesso! skip_to_actions: Passar para as ações status: Estado + statuses: Publicações (%{count}) statuses_description_html: O conteúdo ofensivo será citado na comunicação com a conta denunciada summary: action_preambles: diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 44ecabdc0f..4744417181 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -267,6 +267,7 @@ tr: demote_user_html: "%{name}, %{target} kullanıcısını düşürdü" destroy_announcement_html: "%{name}, %{target} duyurusunu sildi" destroy_canonical_email_block_html: "%{name}, %{target} karmasıyla e-posta engelini kaldırdı" + destroy_collection_html: "%{name}, %{target} kullanıcısının koleksiyonunu kaldırdı" destroy_custom_emoji_html: "%{name}, %{target} ifadesini sildi" destroy_domain_allow_html: "%{name}, %{target} alan adıyla birlik iznini kaldırdı" destroy_domain_block_html: "%{name}, %{target} alan adı engelini kaldırdı" @@ -306,6 +307,7 @@ tr: unsilence_account_html: "%{name}, %{target} kullanıcısının hesabının sessizliğini kaldırdı" unsuspend_account_html: "%{name}, %{target} kullanıcısının hesabının askı durumunu kaldırdı" update_announcement_html: "%{name}, %{target} duyurusunu güncelledi" + update_collection_html: "%{name}, %{target} kullanıcısının koleksiyonunu güncelledi" update_custom_emoji_html: "%{name}, %{target} emojisini güncelledi" update_domain_block_html: "%{name}, %{target} alan adının engelini güncelledi" update_ip_block_html: "%{name}, %{target} IP adresi için kuralı güncelledi" diff --git a/config/routes.rb b/config/routes.rb index b516a48866..57ca5923c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,7 +12,7 @@ class RedirectWithVary < ActionDispatch::Routing::PathRedirect end def redirect_with_vary(path) - RedirectWithVary.new(301, path) + RedirectWithVary.new(301, path, caller(1..1).first) end Rails.application.routes.draw do @@ -125,6 +125,7 @@ Rails.application.routes.draw do scope path: 'ap', as: 'ap' do resources :accounts, path: 'users', only: [:show], param: :id, concerns: :account_resources do resources :collection_items, only: [:show] + resources :feature_authorizations, only: [:show], module: :activitypub resources :featured_collections, only: [:index], module: :activitypub resources :statuses, only: [:show] do diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 5231e44799..17588c3d3f 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -45,7 +45,7 @@ module Mastodon def api_versions { - mastodon: 7, + mastodon: 8, glitch: 1, } end diff --git a/spec/lib/account_statuses_filter_spec.rb b/spec/lib/account_statuses_filter_spec.rb index 351d3dae57..ecb75d55d0 100644 --- a/spec/lib/account_statuses_filter_spec.rb +++ b/spec/lib/account_statuses_filter_spec.rb @@ -64,6 +64,9 @@ RSpec.describe AccountStatusesFilter do expect(results_for(exclude_reblogs: true)) .to all(satisfy { |status| !status.reblog? }) + + expect(results_for(exclude_direct: true)) + .to all(satisfy { |status| !status.direct_visibility? }) end def results_for(params) @@ -77,6 +80,18 @@ RSpec.describe AccountStatusesFilter do let(:current_account) { nil } let(:direct_status) { nil } + context 'when rejecting direct messages' do + let(:params) { { exclude_direct: true } } + + it 'returns only public statuses, public replies, and public reblogs' do + expect(results_unique_visibilities).to match_array %w(unlisted public) + + expect(results_in_reply_to_ids).to_not be_empty + + expect(results_reblog_of_ids).to_not be_empty + end + end + it 'returns only public statuses, public replies, and public reblogs' do expect(results_unique_visibilities).to match_array %w(unlisted public) @@ -95,6 +110,14 @@ RSpec.describe AccountStatusesFilter do account.block!(current_account) end + context 'when rejecting direct messages' do + let(:params) { { exclude_direct: true } } + + it 'returns nothing' do + expect(subject.to_a).to be_empty + end + end + it 'returns nothing' do expect(subject.to_a).to be_empty end @@ -121,6 +144,18 @@ RSpec.describe AccountStatusesFilter do current_account.follow!(account) end + context 'when rejecting direct messages' do + let(:params) { { exclude_direct: true } } + + it 'returns private statuses, replies, and reblogs' do + expect(results_unique_visibilities).to match_array %w(private unlisted public) + + expect(results_in_reply_to_ids).to_not be_empty + + expect(results_reblog_of_ids).to_not be_empty + end + end + it 'returns private statuses, replies, and reblogs' do expect(results_unique_visibilities).to match_array %w(private unlisted public) @@ -135,6 +170,8 @@ RSpec.describe AccountStatusesFilter do it 'returns the direct status' do expect(results_ids).to include(direct_status.id) end + + it_behaves_like 'filter params' end it_behaves_like 'filter params' @@ -143,6 +180,18 @@ RSpec.describe AccountStatusesFilter do context 'when accessed by a non-follower' do let(:current_account) { Fabricate(:account) } + context 'when rejecting direct messages' do + let(:params) { { exclude_direct: true } } + + it 'returns private statuses, replies, and reblogs' do + expect(results_unique_visibilities).to match_array %w(unlisted public) + + expect(results_in_reply_to_ids).to_not be_empty + + expect(results_reblog_of_ids).to_not be_empty + end + end + it 'returns only public statuses, replies, and reblogs' do expect(results_unique_visibilities).to match_array %w(unlisted public) @@ -157,6 +206,8 @@ RSpec.describe AccountStatusesFilter do it 'returns the private status' do expect(results_ids).to include(private_status.id) end + + it_behaves_like 'filter params' end context 'when blocking a reblogged account' do diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb index 1197776b02..b0a994aaf1 100644 --- a/spec/models/featured_tag_spec.rb +++ b/spec/models/featured_tag_spec.rb @@ -93,24 +93,22 @@ RSpec.describe FeaturedTag do end describe '#display_name' do - subject { Fabricate.build :featured_tag, name: name, tag: tag } + subject { featured_tag.display_name } - context 'with a name value present' do - let(:name) { 'Test' } + let(:featured_tag) { Fabricate.build :featured_tag, name: name, tag: tag } + + context 'with a name value present on the featured tag' do + let(:name) { 'FeaturedTagName' } let(:tag) { nil } - it 'uses name value' do - expect(subject.display_name).to eq('Test') - end + it { is_expected.to eq('FeaturedTagName') } end - context 'with a missing name value but a present tag' do + context 'with a missing name value but a present linked tag' do let(:name) { nil } - let(:tag) { Fabricate.build :tag, name: 'Tester' } + let(:tag) { Fabricate.build :tag, display_name: 'LinkedTagDisplayName' } - it 'uses name value' do - expect(subject.display_name).to eq('Tester') - end + it { is_expected.to eq('LinkedTagDisplayName') } end end diff --git a/spec/models/form/redirect_spec.rb b/spec/models/form/redirect_spec.rb new file mode 100644 index 0000000000..4427a0bb86 --- /dev/null +++ b/spec/models/form/redirect_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Form::Redirect do + describe 'Validations' do + it { is_expected.to validate_presence_of(:acct) } + + describe 'target account validation' do + subject { described_class.new(account:) } + + context 'when target_account is missing' do + let(:account) { Fabricate.build :account } + + it { is_expected.to_not allow_value(nil).for(:target_account).against(:acct).with_message(I18n.t('migrations.errors.not_found')) } + end + + context 'when account already moved' do + let(:account) { Fabricate.build :account, moved_to_account_id: target_account.id } + let(:target_account) { Fabricate :account } + + it { is_expected.to_not allow_value(target_account).for(:target_account).against(:acct).with_message(I18n.t('migrations.errors.already_moved')) } + end + + context 'when moving to self' do + let(:account) { Fabricate :account } + + it { is_expected.to_not allow_value(account).for(:target_account).against(:acct).with_message(I18n.t('migrations.errors.move_to_self')) } + end + end + end +end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index a6406a69a7..9fe723b3ba 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -40,26 +40,40 @@ RSpec.describe Tag do I18n.t('tags.does_not_match_previous_name') end - it 'invalid with #' do - expect(described_class.new(name: '#hello_world')).to_not be_valid + describe 'when skipping normalizations' do + subject { described_class.new } + + before { subject.attributes[:name] = name } + + context 'with a # in string' do + let(:name) { '#hello_world' } + + it { is_expected.to_not be_valid } + end + + context 'with a . in string' do + let(:name) { '.abcdef123' } + + it { is_expected.to_not be_valid } + end + + context 'with a space in string' do + let(:name) { 'hello world' } + + it { is_expected.to_not be_valid } + end end - it 'invalid with .' do - expect(described_class.new(name: '.abcdef123')).to_not be_valid - end - - it 'invalid with spaces' do - expect(described_class.new(name: 'hello world')).to_not be_valid - end - - it 'valid with aesthetic' do - expect(described_class.new(name: 'aesthetic')).to be_valid - end + it { is_expected.to allow_value('aesthetic').for(:name) } end describe 'Normalizations' do it { is_expected.to normalize(:display_name).from('#HelloWorld').to('HelloWorld') } it { is_expected.to normalize(:display_name).from('Hello❤️World').to('HelloWorld') } + + it { is_expected.to normalize(:name).from('#hello_world').to('hello_world') } + it { is_expected.to normalize(:name).from('hello world').to('helloworld') } + it { is_expected.to normalize(:name).from('.abcdef123').to('abcdef123') } end describe 'HASHTAG_RE' do @@ -210,7 +224,7 @@ RSpec.describe Tag do upcase_string = 'abcABCabcABCやゆよ' downcase_string = 'abcabcabcabcやゆよ' - tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) + tag = Fabricate(:tag, name: downcase_string) expect(described_class.find_normalized(upcase_string)).to eq tag end end @@ -239,7 +253,7 @@ RSpec.describe Tag do upcase_string = 'abcABCabcABCやゆよ' downcase_string = 'abcabcabcabcやゆよ' - tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) + tag = Fabricate(:tag, name: downcase_string) expect(described_class.matches_name(upcase_string)).to eq [tag] end @@ -254,7 +268,7 @@ RSpec.describe Tag do upcase_string = 'abcABCabcABCやゆよ' downcase_string = 'abcabcabcabcやゆよ' - tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) + tag = Fabricate(:tag, name: downcase_string) expect(described_class.matching_name(upcase_string)).to eq [tag] end end diff --git a/spec/requests/activitypub/feature_authorizations_spec.rb b/spec/requests/activitypub/feature_authorizations_spec.rb new file mode 100644 index 0000000000..ee4cc0579a --- /dev/null +++ b/spec/requests/activitypub/feature_authorizations_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'ActivityPub FeatureAuthorization endpoint' do + describe 'GET /ap/accounts/:account_id/feature_authorizations/:collection_item_id' do + let(:account) { Fabricate(:account) } + let(:collection) { Fabricate(:collection) } + let(:collection_item) { Fabricate(:collection_item, collection:, account:, state:) } + + context 'with an accepted collection item' do + let(:state) { :accepted } + + it 'returns http success and activity json' do + get ap_account_feature_authorization_path(account.id, collection_item) + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + + expect(response.parsed_body) + .to include(type: 'FeatureAuthorization') + end + end + + shared_examples 'not found' do + it 'returns http not found' do + get ap_account_feature_authorization_path(collection.account_id, collection_item) + + expect(response) + .to have_http_status(404) + end + end + + context 'with a revoked collection item' do + let(:state) { :revoked } + + it_behaves_like 'not found' + end + + context 'with a collection item featuring a remote account' do + let(:account) { Fabricate(:remote_account) } + let(:state) { :accepted } + + it_behaves_like 'not found' + end + end +end diff --git a/spec/serializers/activitypub/feature_authorization_serializer_spec.rb b/spec/serializers/activitypub/feature_authorization_serializer_spec.rb new file mode 100644 index 0000000000..30fd0e4640 --- /dev/null +++ b/spec/serializers/activitypub/feature_authorization_serializer_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FeatureAuthorizationSerializer do + include RoutingHelper + + subject { serialized_record_json(collection_item, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:collection_item) { Fabricate(:collection_item) } + let(:tag_manager) { ActivityPub::TagManager.instance } + + it 'returns the expected json structure' do + expect(subject) + .to include( + 'type' => 'FeatureAuthorization', + 'id' => ap_account_feature_authorization_url(collection_item.account_id, collection_item), + 'interactionTarget' => tag_manager.uri_for(collection_item.account), + 'interactingObject' => tag_manager.uri_for(collection_item.collection) + ) + end + end +end diff --git a/spec/serializers/activitypub/featured_collection_serializer_spec.rb b/spec/serializers/activitypub/featured_collection_serializer_spec.rb index 24dd065480..c0ae43abb9 100644 --- a/spec/serializers/activitypub/featured_collection_serializer_spec.rb +++ b/spec/serializers/activitypub/featured_collection_serializer_spec.rb @@ -3,6 +3,8 @@ require 'rails_helper' RSpec.describe ActivityPub::FeaturedCollectionSerializer do + include RoutingHelper + subject { serialized_record_json(collection, described_class, adapter: ActivityPub::Adapter) } let(:collection) do @@ -35,12 +37,14 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do 'type' => 'FeaturedItem', 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.first.account), 'featuredObjectType' => 'Person', + 'featureAuthorization' => ap_account_feature_authorization_url(collection_items.first.account_id, collection_items.first), }, { 'id' => ActivityPub::TagManager.instance.uri_for(collection_items.last), 'type' => 'FeaturedItem', 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.last.account), 'featuredObjectType' => 'Person', + 'featureAuthorization' => ap_account_feature_authorization_url(collection_items.last.account_id, collection_items.last), }, ], 'published' => match_api_datetime_format, diff --git a/spec/serializers/activitypub/featured_item_serializer_spec.rb b/spec/serializers/activitypub/featured_item_serializer_spec.rb index f17faf2410..7aca086192 100644 --- a/spec/serializers/activitypub/featured_item_serializer_spec.rb +++ b/spec/serializers/activitypub/featured_item_serializer_spec.rb @@ -3,16 +3,37 @@ require 'rails_helper' RSpec.describe ActivityPub::FeaturedItemSerializer do + include RoutingHelper + subject { serialized_record_json(collection_item, described_class, adapter: ActivityPub::Adapter) } let(:collection_item) { Fabricate(:collection_item) } - it 'serializes to the expected structure' do - expect(subject).to include({ - 'type' => 'FeaturedItem', - 'id' => ActivityPub::TagManager.instance.uri_for(collection_item), - 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_item.account), - 'featuredObjectType' => 'Person', - }) + context 'when a local account is featured' do + it 'serializes to the expected structure' do + expect(subject).to include({ + 'type' => 'FeaturedItem', + 'id' => ActivityPub::TagManager.instance.uri_for(collection_item), + 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_item.account), + 'featuredObjectType' => 'Person', + 'featureAuthorization' => ap_account_feature_authorization_url(collection_item.account_id, collection_item), + }) + end + end + + context 'when a remote account is featured' do + let(:collection) { Fabricate(:collection) } + let(:account) { Fabricate(:remote_account) } + let(:collection_item) { Fabricate(:collection_item, collection:, account:, approval_uri: 'https://example.com/auth/1') } + + it 'serializes to the expected structure' do + expect(subject).to include({ + 'type' => 'FeaturedItem', + 'id' => ActivityPub::TagManager.instance.uri_for(collection_item), + 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_item.account), + 'featuredObjectType' => 'Person', + 'featureAuthorization' => 'https://example.com/auth/1', + }) + end end end diff --git a/yarn.lock b/yarn.lock index 9167054fdb..e4bff3d9b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3925,7 +3925,7 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^2.0.0": +"@storybook/icons@npm:^2.0.1": version: 2.0.1 resolution: "@storybook/icons@npm:2.0.1" peerDependencies: @@ -12646,7 +12646,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.2, semver@npm:^7.7.1, semver@npm:^7.7.3": +"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.7.1, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -13101,11 +13101,11 @@ __metadata: linkType: hard "storybook@npm:^10.0.5": - version: 10.1.10 - resolution: "storybook@npm:10.1.10" + version: 10.2.13 + resolution: "storybook@npm:10.2.13" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^2.0.0" + "@storybook/icons": "npm:^2.0.1" "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/user-event": "npm:^14.6.1" "@vitest/expect": "npm:3.2.4" @@ -13113,7 +13113,7 @@ __metadata: esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0" open: "npm:^10.2.0" recast: "npm:^0.23.5" - semver: "npm:^7.6.2" + semver: "npm:^7.7.3" use-sync-external-store: "npm:^1.5.0" ws: "npm:^8.18.0" peerDependencies: @@ -13123,7 +13123,7 @@ __metadata: optional: true bin: storybook: ./dist/bin/dispatcher.js - checksum: 10c0/beff5472ee86a995cbde2789b2aabd941f823e31ca6957bb4434cb8ee3d3703cf1248e44f4b4d402416a52bfee94677e74f233cc906487901e831e8ab610defa + checksum: 10c0/5ca338b707c3e7e94c16ecdcb00ca3c450157dceec758c15c416649e346e628a0e034d2265656650fc4fee4680631de7cc588e1a244e42cbb41af9416281a998 languageName: node linkType: hard