diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e975479e..cfbc450d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. +## [4.5.6] - 2026-02-03 + +### Security + +- Fix ActivityPub collection caching logic for pinned posts and featured tags not checking blocked accounts ([GHSA-ccpr-m53r-mfwr](https://github.com/mastodon/mastodon/security/advisories/GHSA-ccpr-m53r-mfwr)) + +### Changed + +- Shorten caching of quote posts pending approval (#37570 and #37592 by @ClearlyClaire) + +### Fixed + +- Fix relationship cache not being cleared when handling account migrations (#37664 by @ClearlyClaire) +- Fix quote cancel button not appearing after edit then delete-and-redraft (#37066 by @PGrayCS) +- Fix followers with profile subscription (bell icon) being notified of post edits (#37646 by @ClearlyClaire) +- Fix error when encountering invalid tag in updated object (#37635 by @ClearlyClaire) +- Fix cross-server conversation tracking (#37559 by @ClearlyClaire) +- Fix recycled connections not being immediately closed (#37335 and #37674 by @ClearlyClaire and @shleeable) + ## [4.5.5] - 2026-01-20 ### Security diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 553a43dad2..6647d09997 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -6,17 +6,31 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController vary_by -> { 'Signature' if authorized_fetch_mode? } before_action :require_account_signature!, if: :authorized_fetch_mode? + before_action :check_authorization before_action :set_items before_action :set_size before_action :set_type def show expires_in 3.minutes, public: public_fetch_mode? - render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + + if @unauthorized + render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + else + render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter + end end private + def check_authorization + # Because in public fetch mode we cache the response, there would be no + # benefit from performing the check below, since a blocked account or domain + # would likely be served the cache from the reverse proxy anyway + + @unauthorized = authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) + end + def set_items case params[:id] when 'featured' @@ -59,11 +73,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController end def for_signed_account - # Because in public fetch mode we cache the response, there would be no - # benefit from performing the check below, since a blocked account or domain - # would likely be served the cache from the reverse proxy anyway - - if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) + if @unauthorized [] else yield diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index b4157a502e..5960c3dc2a 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -153,7 +153,8 @@ export function fetchAccountFail(id, error) { */ export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { - const alreadyFollowing = getState().getIn(['relationships', id, 'following']); + const relationship = getState().getIn(['relationships', id]); + const alreadyFollowing = relationship?.following || relationship?.requested; const locked = getState().getIn(['accounts', id, 'locked'], false); dispatch(followAccountRequest({ id, locked })); diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx index fc20ff53fa..6ed138c301 100644 --- a/app/javascript/mastodon/components/dropdown_menu.tsx +++ b/app/javascript/mastodon/components/dropdown_menu.tsx @@ -71,10 +71,15 @@ export const DropdownMenuItemContent: React.FC<{ item: MenuItem }> = ({ return null; } - const { text, description, icon } = item; + const { text, description, icon, iconId } = item; return ( <> - {icon && } + {icon && ( + + )} {text} {Boolean(description) && ( diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index 97aaecd1aa..97c1fbcae4 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -5,6 +5,7 @@ import { useIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; import { useIdentity } from '@/mastodon/identity_context'; +import { isClientFeatureEnabled } from '@/mastodon/utils/environment'; import { fetchRelationships, followAccount, @@ -94,7 +95,17 @@ export const FollowButton: React.FC<{ if (accountId === me) { return; - } else if (relationship.muting) { + } else if (relationship.blocking) { + dispatch( + openModal({ + modalType: 'CONFIRM_UNBLOCK', + modalProps: { account }, + }), + ); + } else if ( + relationship.muting && + !isClientFeatureEnabled('profile_redesign') + ) { dispatch(unmuteAccount(accountId)); } else if (account && relationship.following) { dispatch( @@ -107,13 +118,6 @@ export const FollowButton: React.FC<{ modalProps: { account }, }), ); - } else if (relationship.blocking) { - dispatch( - openModal({ - modalType: 'CONFIRM_UNBLOCK', - modalProps: { account }, - }), - ); } else { dispatch(followAccount(accountId)); } @@ -136,7 +140,10 @@ export const FollowButton: React.FC<{ label = intl.formatMessage(messages.editProfile); } else if (!relationship) { label = ; - } else if (relationship.muting) { + } else if ( + relationship.muting && + !isClientFeatureEnabled('profile_redesign') + ) { label = intl.formatMessage(messages.unmute); } else if (relationship.following) { label = intl.formatMessage(messages.unfollow); @@ -173,7 +180,7 @@ export const FollowButton: React.FC<{ (!(relationship?.following || relationship?.requested) && (account?.suspended || !!account?.moved)) } - secondary={following} + secondary={following || relationship?.blocking} compact={compact} className={classNames(className, { 'button--destructive': following })} > diff --git a/app/javascript/mastodon/components/form_fields/form_stack.module.scss b/app/javascript/mastodon/components/form_fields/form_stack.module.scss new file mode 100644 index 0000000000..083e36c320 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/form_stack.module.scss @@ -0,0 +1,7 @@ +.stack { + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 25px; + padding: 16px; +} diff --git a/app/javascript/mastodon/components/form_fields/form_stack.tsx b/app/javascript/mastodon/components/form_fields/form_stack.tsx new file mode 100644 index 0000000000..707545898e --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/form_stack.tsx @@ -0,0 +1,23 @@ +import classNames from 'classnames'; + +import { polymorphicForwardRef } from '@/types/polymorphic'; + +import classes from './form_stack.module.scss'; + +/** + * A simple wrapper for providing consistent spacing to a group of form fields. + */ + +export const FormStack = polymorphicForwardRef<'div'>( + ({ as: Element = 'div', children, className, ...otherProps }, ref) => ( + + {children} + + ), +); + +FormStack.displayName = 'FormStack'; diff --git a/app/javascript/mastodon/components/form_fields/index.ts b/app/javascript/mastodon/components/form_fields/index.ts index 8dd693d51e..f87626cb65 100644 --- a/app/javascript/mastodon/components/form_fields/index.ts +++ b/app/javascript/mastodon/components/form_fields/index.ts @@ -1,5 +1,6 @@ -export { TextInputField } from './text_input_field'; -export { TextAreaField } from './text_area_field'; +export { FormStack } from './form_stack'; +export { TextInputField, TextInput } from './text_input_field'; +export { TextAreaField, TextArea } from './text_area_field'; export { CheckboxField, Checkbox } from './checkbox_field'; export { ToggleField, Toggle } from './toggle_field'; -export { SelectField } from './select_field'; +export { SelectField, Select } from './select_field'; diff --git a/app/javascript/mastodon/components/form_fields/select.module.scss b/app/javascript/mastodon/components/form_fields/select.module.scss new file mode 100644 index 0000000000..e68e248fec --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/select.module.scss @@ -0,0 +1,66 @@ +.wrapper { + position: relative; + width: 100%; + + /* Dropdown indicator icon */ + &::after { + --icon-size: 11px; + + content: ''; + position: absolute; + top: 0; + bottom: 0; + inset-inline-end: 9px; + width: var(--icon-size); + background-color: var(--color-text-tertiary); + pointer-events: none; + mask-image: url("data:image/svg+xml;utf8,"); + mask-position: right center; + mask-size: var(--icon-size); + mask-repeat: no-repeat; + } + + &:has(.select:focus-visible)::after { + background-color: var(--color-text-secondary); + } + + &:has(.select:disabled)::after { + background-color: var(--color-text-disabled); + } +} + +.select { + appearance: none; + box-sizing: border-box; + display: block; + width: 100%; + height: 41px; + padding-inline-start: 10px; + padding-inline-end: 30px; + font-family: inherit; + font-size: 14px; + color: var(--color-text-primary); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border-primary); + border-radius: 4px; + outline: 0; + + @media screen and (width <= 600px) { + font-size: 16px; + } + + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: -1px; + } + + &:disabled { + color: var(--color-text-disabled); + border-color: transparent; + cursor: not-allowed; + } + + [data-has-error='true'] & { + border-color: var(--color-text-error); + } +} diff --git a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx index 762436fe28..469238dd44 100644 --- a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { SelectField } from './select_field'; +import { SelectField, Select } from './select_field'; const meta = { title: 'Components/Form Fields/SelectField', @@ -8,24 +8,19 @@ const meta = { args: { label: 'Fruit preference', hint: 'Select your favourite fruit or not. Up to you.', - }, - render(args) { - // Component styles require a wrapper class at the moment - return ( - - - Apple - Banana - Kiwi - Lemon - Mango - Orange - Pomelo - Strawberries - Something else - - - ); + children: ( + <> + Apple + Banana + Kiwi + Lemon + Mango + Orange + Pomelo + Strawberries + Something else + > + ), }, } satisfies Meta; @@ -59,3 +54,16 @@ export const WithError: Story = { hasError: true, }, }; + +export const Plain: Story = { + render(args) { + return ; + }, +}; + +export const Disabled: Story = { + ...Plain, + args: { + disabled: true, + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/select_field.tsx b/app/javascript/mastodon/components/form_fields/select_field.tsx index e612a215b5..59854b578e 100644 --- a/app/javascript/mastodon/components/form_fields/select_field.tsx +++ b/app/javascript/mastodon/components/form_fields/select_field.tsx @@ -1,8 +1,11 @@ import type { ComponentPropsWithoutRef } from 'react'; import { forwardRef } from 'react'; +import classNames from 'classnames'; + import { FormFieldWrapper } from './form_field_wrapper'; import type { CommonFieldWrapperProps } from './form_field_wrapper'; +import classes from './select.module.scss'; interface Props extends ComponentPropsWithoutRef<'select'>, CommonFieldWrapperProps {} @@ -25,14 +28,27 @@ export const SelectField = forwardRef( inputId={id} > {(inputProps) => ( - - - {children} - - + + {children} + )} ), ); SelectField.displayName = 'SelectField'; + +export const Select = forwardRef< + HTMLSelectElement, + ComponentPropsWithoutRef<'select'> +>(({ className, size, ...otherProps }, ref) => ( + + + +)); + +Select.displayName = 'Select'; diff --git a/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx b/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx index f4b8440916..448af8a28e 100644 --- a/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { TextAreaField } from './text_area_field'; +import { TextAreaField, TextArea } from './text_area_field'; const meta = { title: 'Components/Form Fields/TextAreaField', @@ -9,14 +9,6 @@ const meta = { label: 'Label', hint: 'This is a description of this form field', }, - render(args) { - // Component styles require a wrapper class at the moment - return ( - - - - ); - }, } satisfies Meta; export default meta; @@ -49,3 +41,17 @@ export const WithError: Story = { hasError: true, }, }; + +export const Plain: Story = { + render(args) { + return ; + }, +}; + +export const Disabled: Story = { + ...Plain, + args: { + disabled: true, + defaultValue: "This value can't be changed", + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/text_area_field.tsx b/app/javascript/mastodon/components/form_fields/text_area_field.tsx index fd514a88e2..bbde89574f 100644 --- a/app/javascript/mastodon/components/form_fields/text_area_field.tsx +++ b/app/javascript/mastodon/components/form_fields/text_area_field.tsx @@ -1,8 +1,11 @@ import type { ComponentPropsWithoutRef } from 'react'; import { forwardRef } from 'react'; +import classNames from 'classnames'; + import { FormFieldWrapper } from './form_field_wrapper'; import type { CommonFieldWrapperProps } from './form_field_wrapper'; +import classes from './text_input.module.scss'; interface Props extends ComponentPropsWithoutRef<'textarea'>, CommonFieldWrapperProps {} @@ -23,9 +26,22 @@ export const TextAreaField = forwardRef( hasError={hasError} inputId={id} > - {(inputProps) => } + {(inputProps) => } ), ); TextAreaField.displayName = 'TextAreaField'; + +export const TextArea = forwardRef< + HTMLTextAreaElement, + ComponentPropsWithoutRef<'textarea'> +>(({ className, ...otherProps }, ref) => ( + +)); + +TextArea.displayName = 'TextArea'; diff --git a/app/javascript/mastodon/components/form_fields/text_input.module.scss b/app/javascript/mastodon/components/form_fields/text_input.module.scss new file mode 100644 index 0000000000..2299068c5a --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/text_input.module.scss @@ -0,0 +1,42 @@ +.input { + box-sizing: border-box; + display: block; + resize: vertical; + width: 100%; + padding: 10px 16px; + font-family: inherit; + font-size: 14px; + line-height: 20px; + color: var(--color-text-primary); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border-primary); + border-radius: 4px; + outline: var(--outline-focus-default); + outline-color: transparent; + outline-offset: -1px; + transition: outline-color 0.15s ease-out; + + @media screen and (width <= 600px) { + font-size: 16px; + } + + &:focus { + outline-color: var(--color-text-brand); + } + + &:focus:user-invalid, + &:required:user-invalid, + [data-has-error='true'] & { + outline-color: var(--color-text-error); + } + + &:required:user-valid { + outline-color: var(--color-text-success); + } + + &:disabled { + color: var(--color-text-disabled); + border-color: transparent; + cursor: not-allowed; + } +} diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx index ec00ef5fd3..2cf8613f68 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { TextInputField } from './text_input_field'; +import { TextInputField, TextInput } from './text_input_field'; const meta = { title: 'Components/Form Fields/TextInputField', @@ -9,14 +9,6 @@ const meta = { label: 'Label', hint: 'This is a description of this form field', }, - render(args) { - // Component styles require a wrapper class at the moment - return ( - - - - ); - }, } satisfies Meta; export default meta; @@ -49,3 +41,17 @@ export const WithError: Story = { hasError: true, }, }; + +export const Plain: Story = { + render(args) { + return ; + }, +}; + +export const Disabled: Story = { + ...Plain, + args: { + disabled: true, + defaultValue: "This value can't be changed", + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.tsx index 3b2d941173..37cf150147 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.tsx @@ -1,8 +1,11 @@ import type { ComponentPropsWithoutRef } from 'react'; import { forwardRef } from 'react'; +import classNames from 'classnames'; + import { FormFieldWrapper } from './form_field_wrapper'; import type { CommonFieldWrapperProps } from './form_field_wrapper'; +import classes from './text_input.module.scss'; interface Props extends ComponentPropsWithoutRef<'input'>, CommonFieldWrapperProps {} @@ -15,10 +18,7 @@ interface Props */ export const TextInputField = forwardRef( - ( - { id, label, hint, hasError, required, type = 'text', ...otherProps }, - ref, - ) => ( + ({ id, label, hint, hasError, required, ...otherProps }, ref) => ( ( hasError={hasError} inputId={id} > - {(inputProps) => ( - - )} + {(inputProps) => } ), ); TextInputField.displayName = 'TextInputField'; + +export const TextInput = forwardRef< + HTMLInputElement, + ComponentPropsWithoutRef<'input'> +>(({ type = 'text', className, ...otherProps }, ref) => ( + +)); + +TextInput.displayName = 'TextInput'; diff --git a/app/javascript/mastodon/components/mini_card/index.tsx b/app/javascript/mastodon/components/mini_card/index.tsx index a619bb214a..9ddb964d71 100644 --- a/app/javascript/mastodon/components/mini_card/index.tsx +++ b/app/javascript/mastodon/components/mini_card/index.tsx @@ -1,33 +1,57 @@ -import type { FC, ReactNode } from 'react'; +import { forwardRef } from 'react'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import classNames from 'classnames'; +import type { OmitUnion } from '@/mastodon/utils/types'; + +import { Icon } from '../icon'; +import type { IconProp } from '../icon'; + import classes from './styles.module.css'; -export interface MiniCardProps { - label: ReactNode; - value: ReactNode; - className?: string; - hidden?: boolean; -} - -export const MiniCard: FC = ({ - label, - value, - className, - hidden, -}) => { - if (!label) { - return null; +export type MiniCardProps = OmitUnion< + ComponentPropsWithoutRef<'div'>, + { + label: ReactNode; + value: ReactNode; + icon?: IconProp; + iconId?: string; + iconClassName?: string; } +>; - return ( - - {label} - {value} - - ); -}; +export const MiniCard = forwardRef( + ( + { label, value, className, hidden, icon, iconId, iconClassName, ...props }, + ref, + ) => { + if (!label) { + return null; + } + + return ( + + {icon && ( + + )} + {label} + {value} + + ); + }, +); +MiniCard.displayName = 'MiniCard'; diff --git a/app/javascript/mastodon/components/mini_card/list.tsx b/app/javascript/mastodon/components/mini_card/list.tsx index 9b5c859cf4..c98f57c863 100644 --- a/app/javascript/mastodon/components/mini_card/list.tsx +++ b/app/javascript/mastodon/components/mini_card/list.tsx @@ -1,69 +1,37 @@ -import type { FC, Key, MouseEventHandler } from 'react'; - -import { FormattedMessage } from 'react-intl'; +import { forwardRef } from 'react'; +import type { ComponentPropsWithoutRef, Key } from 'react'; import classNames from 'classnames'; -import { useOverflow } from '@/mastodon/hooks/useOverflow'; +import type { OmitUnion } from '@/mastodon/utils/types'; import { MiniCard } from '.'; -import type { MiniCardProps } from '.'; +import type { MiniCardProps as BaseCardProps } from '.'; import classes from './styles.module.css'; +export type MiniCardProps = BaseCardProps & { + key?: Key; +}; + interface MiniCardListProps { - cards?: (Pick & { - key?: Key; - })[]; - className?: string; - onOverflowClick?: MouseEventHandler; + cards?: MiniCardProps[]; } -export const MiniCardList: FC = ({ - cards = [], - className, - onOverflowClick, -}) => { - const { - wrapperRef, - listRef, - hiddenCount, - hasOverflow, - hiddenIndex, - maxWidth, - } = useOverflow(); - +export const MiniCardList = forwardRef< + HTMLDListElement, + OmitUnion, MiniCardListProps> +>(({ cards = [], className, children, ...props }, ref) => { if (!cards.length) { return null; } return ( - - - {cards.map((card, index) => ( - = hiddenIndex} - className={card.className} - /> - ))} - - {cards.length > 1 && ( - - - - - - )} - + + {cards.map((card, index) => ( + + ))} + {children} + ); -}; +}); +MiniCardList.displayName = 'MiniCardList'; diff --git a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx index 60534f05f6..e099820d29 100644 --- a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx +++ b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx @@ -1,30 +1,12 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { action } from 'storybook/actions'; + +import LinkIcon from '@/material-icons/400-24px/link_2.svg?react'; import { MiniCardList } from './list'; const meta = { title: 'Components/MiniCard', component: MiniCardList, - args: { - onOverflowClick: action('Overflow clicked'), - }, - render(args) { - return ( - - - - ); - }, } satisfies Meta; export default meta; @@ -38,10 +20,12 @@ export const Default: Story = { { label: 'Website', value: bowie-the-db.meow, + icon: LinkIcon, }, { label: 'Free playlists', value: soundcloud.com, + icon: LinkIcon, }, { label: 'Location', value: 'Purris, France' }, ], @@ -54,11 +38,13 @@ export const LongValue: Story = { { label: 'Username', value: 'bowie-the-dj', + style: { maxWidth: '250px' }, }, { label: 'Bio', value: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + style: { maxWidth: '250px' }, }, ], }, diff --git a/app/javascript/mastodon/components/mini_card/styles.module.css b/app/javascript/mastodon/components/mini_card/styles.module.css index 642c08c5fa..07de32eef0 100644 --- a/app/javascript/mastodon/components/mini_card/styles.module.css +++ b/app/javascript/mastodon/components/mini_card/styles.module.css @@ -1,52 +1,49 @@ -.wrapper { - display: flex; - flex-wrap: nowrap; - justify-content: flex-start; - gap: 4px; -} - .list { display: flex; gap: 4px; - overflow: hidden; - position: relative; } -.card, -.more { +.card { border: 1px solid var(--color-border-primary); padding: 8px; border-radius: 8px; flex-shrink: 0; -} - -.more { + display: grid; + grid-template-rows: repeat(2, 1fr); + column-gap: 8px; + font-size: 13px; + line-height: 1rem; color: var(--color-text-secondary); - font-weight: 600; - appearance: none; - background: none; - aspect-ratio: 1; - height: 100%; - transition: all 300ms linear; } -.more:hover { - background-color: var(--color-bg-brand-softer); - color: var(--color-text-primary); +.cardWithIcon { + grid-template-columns: 16px 1fr; } -.hidden { - display: none; -} - -.label { - color: var(--color-text-secondary); - margin-bottom: 2px; +.icon { + grid-row: span 2; + grid-column: 1; + width: 100%; + height: auto; + align-self: center; + fill: currentColor; } .value { color: var(--color-text-primary); - font-weight: 600; + font-weight: 500; + + a { + color: var(--color-text-brand); + text-decoration: none; + transition: color 0.2s ease-in-out; + outline: none; + + &:hover, + &:focus { + color: var(--color-text-brand-soft); + } + } } .label, @@ -54,4 +51,8 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + + .cardWithIcon & { + grid-column: 2; + } } diff --git a/app/javascript/mastodon/features/about/components/rules.tsx b/app/javascript/mastodon/features/about/components/rules.tsx index 1b1e28a7ff..52339329fe 100644 --- a/app/javascript/mastodon/features/about/components/rules.tsx +++ b/app/javascript/mastodon/features/about/components/rules.tsx @@ -8,6 +8,7 @@ import { createSelector } from '@reduxjs/toolkit'; import type { List as ImmutableList } from 'immutable'; import type { SelectItem } from '@/mastodon/components/dropdown_selector'; +import { Select } from '@/mastodon/components/form_fields'; import type { RootState } from '@/mastodon/store'; import { useAppSelector } from '@/mastodon/store'; @@ -104,19 +105,17 @@ export const RulesSection: FC = ({ isLoading = false }) => { defaultMessage='Language' /> - - - {localeOptions.map((option) => ( - - {option.text} - - ))} - - + + {localeOptions.map((option) => ( + + {option.text} + + ))} + )} diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 8f7451636d..f08db79f72 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -1,4 +1,5 @@ -import { useCallback } from 'react'; +import type { RefCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; @@ -77,6 +78,40 @@ export const AccountHeader: React.FC<{ [dispatch, account], ); + const [isFooterIntersecting, setIsIntersecting] = useState(false); + const handleIntersect: IntersectionObserverCallback = useCallback( + (entries) => { + const entry = entries.at(0); + if (!entry) { + return; + } + + setIsIntersecting(entry.isIntersecting); + }, + [], + ); + const [observer] = useState( + () => + new IntersectionObserver(handleIntersect, { + rootMargin: '0px 0px -55px 0px', // Height of bottom nav bar. + }), + ); + + const handleObserverRef: RefCallback = useCallback( + (node) => { + if (node) { + observer.observe(node); + } + }, + [observer], + ); + + useEffect(() => { + return () => { + observer.disconnect(); + }; + }, [observer]); + if (!account) { return null; } @@ -114,7 +149,12 @@ export const AccountHeader: React.FC<{ )} - + - {isRedesignEnabled() && } + {isRedesignEnabled() && ( + + )} @@ -153,11 +199,13 @@ export const AccountHeader: React.FC<{ )} - + {!isRedesignEnabled() && ( + + )} {!suspendedOrHidden && ( @@ -181,10 +229,22 @@ export const AccountHeader: React.FC<{ )} + + {isRedesignEnabled() && ( + + )} {!hideTabs && !hidden && } + {titleFromAccount(account)} diff --git a/app/javascript/mastodon/features/account_timeline/components/badges.tsx b/app/javascript/mastodon/features/account_timeline/components/badges.tsx index 3b4b1ca2c6..d48dc669f5 100644 --- a/app/javascript/mastodon/features/account_timeline/components/badges.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/badges.tsx @@ -108,48 +108,6 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { className={classNames(className, classes.badgeMuted)} />, ); - } else if ( - relationship.followed_by && - (relationship.following || relationship.requested) - ) { - badges.push( - - } - className={className} - />, - ); - } else if (relationship.followed_by) { - badges.push( - - } - className={className} - />, - ); - } else if (relationship.requested_by) { - badges.push( - - } - className={className} - />, - ); } } diff --git a/app/javascript/mastodon/features/account_timeline/components/fields.tsx b/app/javascript/mastodon/features/account_timeline/components/fields.tsx index d407c5010f..0661c403b3 100644 --- a/app/javascript/mastodon/features/account_timeline/components/fields.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/fields.tsx @@ -1,21 +1,23 @@ -import { useCallback, useMemo } from 'react'; import type { FC } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import classNames from 'classnames'; import IconVerified from '@/images/icons/icon_verified.svg?react'; -import { openModal } from '@/mastodon/actions/modal'; import { AccountFields } from '@/mastodon/components/account_fields'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; -import { Icon } from '@/mastodon/components/icon'; -import { MiniCardList } from '@/mastodon/components/mini_card/list'; +import { IconButton } from '@/mastodon/components/icon_button'; +import { MiniCard } from '@/mastodon/components/mini_card'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useOverflowScroll } from '@/mastodon/hooks/useOverflow'; import type { Account } from '@/mastodon/models/account'; -import { useAppDispatch } from '@/mastodon/store'; +import { isValidUrl } from '@/mastodon/utils/checks'; +import IconLeftArrow from '@/material-icons/400-24px/chevron_left.svg?react'; +import IconRightArrow from '@/material-icons/400-24px/chevron_right.svg?react'; +import IconLink from '@/material-icons/400-24px/link_2.svg?react'; import { isRedesignEnabled } from '../common'; @@ -57,61 +59,94 @@ export const AccountHeaderFields: FC<{ accountId: string }> = ({ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { const htmlHandlers = useElementHandledLink(); - const cards = useMemo( - () => - account.fields - .toArray() - .map(({ value_emojified, name_emojified, verified_at }) => ({ - label: ( - <> - - {!!verified_at && ( - - )} - > - ), - value: ( - - ), - className: classNames( - classes.fieldCard, - !!verified_at && classes.fieldCardVerified, - ), - })), - [account.emojis, account.fields, htmlHandlers], - ); + const intl = useIntl(); - const dispatch = useAppDispatch(); - const handleOverflowClick = useCallback(() => { - dispatch( - openModal({ - modalType: 'ACCOUNT_FIELDS', - modalProps: { accountId: account.id }, - }), - ); - }, [account.id, dispatch]); + const { + bodyRef, + canScrollLeft, + canScrollRight, + handleLeftNav, + handleRightNav, + handleScroll, + } = useOverflowScroll(); return ( - + + {canScrollLeft && ( + + )} + + {account.fields.map( + ( + { name, name_emojified, value_emojified, value_plain, verified_at }, + key, + ) => ( + + } + value={ + + } + icon={fieldIcon(verified_at, value_plain)} + className={classNames( + classes.fieldCard, + verified_at && classes.fieldCardVerified, + )} + /> + ), + )} + + {canScrollRight && ( + + )} + ); }; + +function fieldIcon(verified_at: string | null, value_plain: string | null) { + if (verified_at) { + return IconVerified; + } else if (value_plain && isValidUrl(value_plain)) { + return IconLink; + } + return undefined; +} diff --git a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx deleted file mode 100644 index 5c29e77c11..0000000000 --- a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import type { FC } from 'react'; - -import { FormattedMessage, useIntl } from 'react-intl'; - -import IconVerified from '@/images/icons/icon_verified.svg?react'; -import { DisplayName } from '@/mastodon/components/display_name'; -import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; -import { EmojiHTML } from '@/mastodon/components/emoji/html'; -import { Icon } from '@/mastodon/components/icon'; -import { IconButton } from '@/mastodon/components/icon_button'; -import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; -import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; -import { useAccount } from '@/mastodon/hooks/useAccount'; -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; - -import classes from './redesign.module.scss'; - -export const AccountFieldsModal: FC<{ - accountId: string; - onClose: () => void; -}> = ({ accountId, onClose }) => { - const intl = useIntl(); - const account = useAccount(accountId); - const htmlHandlers = useElementHandledLink(); - - if (!account) { - return ( - - - - ); - } - - return ( - - - - - , - }} - /> - - - - - - {account.fields.map((field, index) => ( - - - - - {!!field.verified_at && ( - - )} - - - ))} - - - - - ); -}; diff --git a/app/javascript/mastodon/features/account_timeline/components/menu.tsx b/app/javascript/mastodon/features/account_timeline/components/menu.tsx index 80fd055cca..a926a651f7 100644 --- a/app/javascript/mastodon/features/account_timeline/components/menu.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/menu.tsx @@ -12,6 +12,7 @@ import { unpinAccount, } from '@/mastodon/actions/accounts'; import { removeAccountFromFollowers } from '@/mastodon/actions/accounts_typed'; +import { showAlert } from '@/mastodon/actions/alerts'; import { directCompose, mentionCompose } from '@/mastodon/actions/compose'; import { initDomainBlockModal, @@ -23,16 +24,78 @@ import { initReport } from '@/mastodon/actions/reports'; import { Dropdown } from '@/mastodon/components/dropdown_menu'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useIdentity } from '@/mastodon/identity_context'; +import type { Account } from '@/mastodon/models/account'; import type { MenuItem } from '@/mastodon/models/dropdown_menu'; +import type { Relationship } from '@/mastodon/models/relationship'; import { PERMISSION_MANAGE_FEDERATION, PERMISSION_MANAGE_USERS, } from '@/mastodon/permissions'; +import type { AppDispatch } from '@/mastodon/store'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import BlockIcon from '@/material-icons/400-24px/block.svg?react'; +import EditIcon from '@/material-icons/400-24px/edit_square.svg?react'; +import LinkIcon from '@/material-icons/400-24px/link_2.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; +import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'; +import ReportIcon from '@/material-icons/400-24px/report.svg?react'; +import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import { isRedesignEnabled } from '../common'; +export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { + const intl = useIntl(); + const { signedIn, permissions } = useIdentity(); + + const account = useAccount(accountId); + const relationship = useAppSelector((state) => + state.relationships.get(accountId), + ); + + const dispatch = useAppDispatch(); + const menuItems = useMemo(() => { + if (!account) { + return []; + } + + if (isRedesignEnabled()) { + return redesignMenuItems({ + account, + signedIn, + permissions, + intl, + relationship, + dispatch, + }); + } + return currentMenuItems({ + account, + signedIn, + permissions, + intl, + relationship, + dispatch, + }); + }, [account, signedIn, permissions, intl, relationship, dispatch]); + return ( + + ); +}; + +interface MenuItemsParams { + account: Account; + signedIn: boolean; + permissions: number; + intl: ReturnType; + relationship?: Relationship; + dispatch: AppDispatch; +} + const messages = defineMessages({ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, @@ -109,80 +172,78 @@ const messages = defineMessages({ }, }); -export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { - const intl = useIntl(); - const { signedIn, permissions } = useIdentity(); +function currentMenuItems({ + account, + signedIn, + permissions, + intl, + relationship, + dispatch, +}: MenuItemsParams): MenuItem[] { + const items: MenuItem[] = []; + const isRemote = account.acct !== account.username; - const account = useAccount(accountId); - const relationship = useAppSelector((state) => - state.relationships.get(accountId), - ); - - const dispatch = useAppDispatch(); - const menuItems = useMemo(() => { - const arr: MenuItem[] = []; - - if (!account) { - return arr; - } - - const isRemote = account.acct !== account.username; - - if (signedIn && !account.suspended) { - arr.push({ + if (signedIn && !account.suspended) { + items.push( + { text: intl.formatMessage(messages.mention, { name: account.username, }), action: () => { dispatch(mentionCompose(account)); }, - }); - arr.push({ + }, + { text: intl.formatMessage(messages.direct, { name: account.username, }), action: () => { dispatch(directCompose(account)); }, - }); - arr.push(null); - } + }, + null, + ); + } - if (isRemote) { - arr.push({ + if (isRemote) { + items.push( + { text: intl.formatMessage(messages.openOriginalPage), href: account.url, - }); - arr.push(null); - } + }, + null, + ); + } - if (!signedIn) { - return arr; - } + if (!signedIn) { + return items; + } - if (relationship?.following) { - if (!relationship.muting) { - if (relationship.showing_reblogs) { - arr.push({ - text: intl.formatMessage(messages.hideReblogs, { - name: account.username, - }), - action: () => { - dispatch(followAccount(account.id, { reblogs: false })); - }, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.showReblogs, { - name: account.username, - }), - action: () => { - dispatch(followAccount(account.id, { reblogs: true })); - }, - }); - } + if (relationship?.following) { + // Timeline options + if (!relationship.muting) { + if (relationship.showing_reblogs) { + items.push({ + text: intl.formatMessage(messages.hideReblogs, { + name: account.username, + }), + action: () => { + dispatch(followAccount(account.id, { reblogs: false })); + }, + }); + } else { + items.push({ + text: intl.formatMessage(messages.showReblogs, { + name: account.username, + }), + action: () => { + dispatch(followAccount(account.id, { reblogs: true })); + }, + }); + } - arr.push({ + items.push( + { text: intl.formatMessage(messages.languages), action: () => { dispatch( @@ -194,13 +255,295 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { }), ); }, - }); - arr.push(null); - } + }, + null, + ); } - if (isRedesignEnabled()) { - arr.push({ + items.push( + { + text: intl.formatMessage( + relationship.endorsed ? messages.unendorse : messages.endorse, + ), + action: () => { + if (relationship.endorsed) { + dispatch(unpinAccount(account.id)); + } else { + dispatch(pinAccount(account.id)); + } + }, + }, + { + text: intl.formatMessage(messages.add_or_remove_from_list), + action: () => { + dispatch( + openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: account.id, + }, + }), + ); + }, + }, + null, + ); + } + + if (relationship?.followed_by) { + const handleRemoveFromFollowers = () => { + dispatch( + openModal({ + modalType: 'CONFIRM', + modalProps: { + title: intl.formatMessage(messages.confirmRemoveFromFollowersTitle), + message: intl.formatMessage( + messages.confirmRemoveFromFollowersMessage, + { name: {account.acct} }, + ), + confirm: intl.formatMessage( + messages.confirmRemoveFromFollowersButton, + ), + onConfirm: () => { + void dispatch( + removeAccountFromFollowers({ accountId: account.id }), + ); + }, + }, + }), + ); + }; + + items.push({ + text: intl.formatMessage(messages.removeFromFollowers, { + name: account.username, + }), + action: handleRemoveFromFollowers, + dangerous: true, + }); + } + + if (relationship?.muting) { + items.push({ + text: intl.formatMessage(messages.unmute, { + name: account.username, + }), + action: () => { + dispatch(unmuteAccount(account.id)); + }, + }); + } else { + items.push({ + text: intl.formatMessage(messages.mute, { + name: account.username, + }), + action: () => { + dispatch(initMuteModal(account)); + }, + dangerous: true, + }); + } + + if (relationship?.blocking) { + items.push({ + text: intl.formatMessage(messages.unblock, { + name: account.username, + }), + action: () => { + dispatch(unblockAccount(account.id)); + }, + }); + } else { + items.push({ + text: intl.formatMessage(messages.block, { + name: account.username, + }), + action: () => { + dispatch(blockAccount(account.id)); + }, + dangerous: true, + }); + } + + if (!account.suspended) { + items.push({ + text: intl.formatMessage(messages.report, { + name: account.username, + }), + action: () => { + dispatch(initReport(account)); + }, + dangerous: true, + }); + } + + const remoteDomain = isRemote ? account.acct.split('@')[1] : null; + if (remoteDomain) { + items.push(null); + + if (relationship?.domain_blocking) { + items.push({ + text: intl.formatMessage(messages.unblockDomain, { + domain: remoteDomain, + }), + action: () => { + dispatch(unblockDomain(remoteDomain)); + }, + }); + } else { + items.push({ + text: intl.formatMessage(messages.blockDomain, { + domain: remoteDomain, + }), + action: () => { + dispatch(initDomainBlockModal(account)); + }, + dangerous: true, + }); + } + } + + if ( + (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || + (isRemote && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION) + ) { + items.push(null); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + items.push({ + text: intl.formatMessage(messages.admin_account, { + name: account.username, + }), + href: `/admin/accounts/${account.id}`, + }); + } + if ( + isRemote && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION + ) { + items.push({ + text: intl.formatMessage(messages.admin_domain, { + domain: remoteDomain, + }), + href: `/admin/instances/${remoteDomain}`, + }); + } + } + + return items; +} + +const redesignMessages = defineMessages({ + share: { id: 'account.menu.share', defaultMessage: 'Share…' }, + copy: { id: 'account.menu.copy', defaultMessage: 'Copy link' }, + copied: { + id: 'account.menu.copied', + defaultMessage: 'Copied account link to clipboard', + }, + mention: { id: 'account.menu.mention', defaultMessage: 'Mention' }, + direct: { + id: 'account.menu.direct', + defaultMessage: 'Privately mention', + }, + mute: { id: 'account.menu.mute', defaultMessage: 'Mute account' }, + unmute: { + id: 'account.menu.unmute', + defaultMessage: 'Unmute account', + }, + block: { id: 'account.menu.block', defaultMessage: 'Block account' }, + unblock: { + id: 'account.menu.unblock', + defaultMessage: 'Unblock account', + }, + domainBlock: { + id: 'account.menu.block_domain', + defaultMessage: 'Block {domain}', + }, + domainUnblock: { + id: 'account.menu.unblock_domain', + defaultMessage: 'Unblock {domain}', + }, + report: { id: 'account.menu.report', defaultMessage: 'Report account' }, + hideReblogs: { + id: 'account.menu.hide_reblogs', + defaultMessage: 'Hide boosts in timeline', + }, + showReblogs: { + id: 'account.menu.show_reblogs', + defaultMessage: 'Show boosts in timeline', + }, + addToList: { + id: 'account.menu.add_to_list', + defaultMessage: 'Add to list…', + }, + openOriginalPage: { + id: 'account.menu.open_original_page', + defaultMessage: 'View on {domain}', + }, + removeFollower: { + id: 'account.menu.remove_follower', + defaultMessage: 'Remove follower', + }, +}); + +function redesignMenuItems({ + account, + signedIn, + permissions, + intl, + relationship, + dispatch, +}: MenuItemsParams): MenuItem[] { + const items: MenuItem[] = []; + const isRemote = account.acct !== account.username; + const remoteDomain = isRemote ? account.acct.split('@')[1] : null; + + // Share and copy link options + if (account.url) { + if ('share' in navigator) { + items.push({ + text: intl.formatMessage(redesignMessages.share), + action: () => { + void navigator.share({ + url: account.url, + }); + }, + icon: ShareIcon, + }); + } + items.push( + { + text: intl.formatMessage(redesignMessages.copy), + action: () => { + void navigator.clipboard.writeText(account.url); + dispatch(showAlert({ message: redesignMessages.copied })); + }, + icon: LinkIcon, + }, + null, + ); + } + + // Mention and direct message options + if (signedIn && !account.suspended) { + items.push( + { + text: intl.formatMessage(redesignMessages.mention), + action: () => { + dispatch(mentionCompose(account)); + }, + }, + + { + text: intl.formatMessage(redesignMessages.direct), + action: () => { + dispatch(directCompose(account)); + }, + }, + null, + { text: intl.formatMessage( relationship?.note ? messages.editNote : messages.addNote, ), @@ -214,27 +557,34 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { }), ); }, - }); - if (!relationship?.following) { - arr.push(null); - } - } + icon: EditIcon, + }, + null, + ); + } - if (relationship?.following) { - arr.push({ - text: intl.formatMessage( - relationship.endorsed ? messages.unendorse : messages.endorse, - ), - action: () => { - if (relationship.endorsed) { - dispatch(unpinAccount(account.id)); - } else { - dispatch(pinAccount(account.id)); - } - }, - }); - arr.push({ - text: intl.formatMessage(messages.add_or_remove_from_list), + // Open on remote page. + if (isRemote) { + items.push( + { + text: intl.formatMessage(redesignMessages.openOriginalPage, { + domain: remoteDomain, + }), + href: account.url, + }, + null, + ); + } + + if (!signedIn) { + return items; + } + + // List and featuring options + if (relationship?.following) { + items.push( + { + text: intl.formatMessage(redesignMessages.addToList), action: () => { dispatch( openModal({ @@ -245,12 +595,76 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { }), ); }, - }); - arr.push(null); + }, + { + text: intl.formatMessage( + relationship.endorsed ? messages.unendorse : messages.endorse, + ), + action: () => { + if (relationship.endorsed) { + dispatch(unpinAccount(account.id)); + } else { + dispatch(pinAccount(account.id)); + } + }, + }, + null, + ); + + // Timeline options + if (!relationship.muting) { + items.push( + { + text: intl.formatMessage( + relationship.showing_reblogs + ? redesignMessages.hideReblogs + : redesignMessages.showReblogs, + ), + action: () => { + dispatch( + followAccount(account.id, { + reblogs: !relationship.showing_reblogs, + }), + ); + }, + }, + { + text: intl.formatMessage(messages.languages), + action: () => { + dispatch( + openModal({ + modalType: 'SUBSCRIBED_LANGUAGES', + modalProps: { + accountId: account.id, + }, + }), + ); + }, + }, + ); } - if (relationship?.followed_by) { - const handleRemoveFromFollowers = () => { + items.push( + { + text: intl.formatMessage( + relationship.muting ? redesignMessages.unmute : redesignMessages.mute, + ), + action: () => { + if (relationship.muting) { + dispatch(unmuteAccount(account.id)); + } else { + dispatch(initMuteModal(account)); + } + }, + }, + null, + ); + } + + if (relationship?.followed_by) { + items.push({ + text: intl.formatMessage(redesignMessages.removeFollower), + action: () => { dispatch( openModal({ modalType: 'CONFIRM', @@ -273,134 +687,91 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { }, }), ); - }; + }, + dangerous: true, + icon: PersonRemoveIcon, + }); + } - arr.push({ - text: intl.formatMessage(messages.removeFromFollowers, { - name: account.username, - }), - action: handleRemoveFromFollowers, - dangerous: true, - }); - } - - if (relationship?.muting) { - arr.push({ - text: intl.formatMessage(messages.unmute, { - name: account.username, - }), - action: () => { - dispatch(unmuteAccount(account.id)); - }, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.mute, { - name: account.username, - }), - action: () => { - dispatch(initMuteModal(account)); - }, - dangerous: true, - }); - } - - if (relationship?.blocking) { - arr.push({ - text: intl.formatMessage(messages.unblock, { - name: account.username, - }), - action: () => { - dispatch(unblockAccount(account.id)); - }, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.block, { - name: account.username, - }), - action: () => { - dispatch(blockAccount(account.id)); - }, - dangerous: true, - }); - } - - if (!account.suspended) { - arr.push({ - text: intl.formatMessage(messages.report, { - name: account.username, - }), - action: () => { - dispatch(initReport(account)); - }, - dangerous: true, - }); - } - - const remoteDomain = isRemote ? account.acct.split('@')[1] : null; - if (remoteDomain) { - arr.push(null); - - if (relationship?.domain_blocking) { - arr.push({ - text: intl.formatMessage(messages.unblockDomain, { - domain: remoteDomain, - }), - action: () => { - dispatch(unblockDomain(remoteDomain)); - }, - }); + items.push({ + text: intl.formatMessage( + relationship?.blocking + ? redesignMessages.unblock + : redesignMessages.block, + ), + action: () => { + if (relationship?.blocking) { + dispatch(unblockAccount(account.id)); } else { - arr.push({ - text: intl.formatMessage(messages.blockDomain, { - domain: remoteDomain, - }), - action: () => { - dispatch(initDomainBlockModal(account)); - }, - dangerous: true, - }); + dispatch(blockAccount(account.id)); } - } + }, + dangerous: true, + icon: BlockIcon, + }); + if (!account.suspended) { + items.push({ + text: intl.formatMessage(redesignMessages.report), + action: () => { + dispatch(initReport(account)); + }, + dangerous: true, + icon: ReportIcon, + }); + } + + if (remoteDomain) { + items.push({ + text: intl.formatMessage( + relationship?.domain_blocking + ? redesignMessages.domainUnblock + : redesignMessages.domainBlock, + { + domain: remoteDomain, + }, + ), + action: () => { + if (relationship?.domain_blocking) { + dispatch(unblockDomain(remoteDomain)); + } else { + dispatch(initDomainBlockModal(account)); + } + }, + dangerous: true, + icon: BlockIcon, + iconId: 'domain-block', + }); + } + + if ( + (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || + (isRemote && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION) + ) { + items.push(null); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + items.push({ + text: intl.formatMessage(messages.admin_account, { + name: account.username, + }), + href: `/admin/accounts/${account.id}`, + }); + } if ( - (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || - (isRemote && - (permissions & PERMISSION_MANAGE_FEDERATION) === - PERMISSION_MANAGE_FEDERATION) + remoteDomain && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION ) { - arr.push(null); - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { - arr.push({ - text: intl.formatMessage(messages.admin_account, { - name: account.username, - }), - href: `/admin/accounts/${account.id}`, - }); - } - if ( - isRemote && - (permissions & PERMISSION_MANAGE_FEDERATION) === - PERMISSION_MANAGE_FEDERATION - ) { - arr.push({ - text: intl.formatMessage(messages.admin_domain, { - domain: remoteDomain, - }), - href: `/admin/instances/${remoteDomain}`, - }); - } + items.push({ + text: intl.formatMessage(messages.admin_domain, { + domain: remoteDomain, + }), + href: `/admin/instances/${remoteDomain}`, + }); } + } - return arr; - }, [account, signedIn, permissions, intl, relationship, dispatch]); - return ( - - ); -}; + return items; +} 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 80195a7a82..944f6f7a7a 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -1,3 +1,7 @@ +.barWrapper { + border-bottom: none; +} + .nameWrapper { display: flex; gap: 16px; @@ -45,6 +49,43 @@ } } +$button-breakpoint: 420px; +$button-fallback-breakpoint: #{$button-breakpoint} + 55px; + +.buttonsDesktop { + @container (width < #{$button-breakpoint}) { + display: none; + } + + @supports (not (container-type: inline-size)) { + @media (max-width: #{$button-fallback-breakpoint}) { + display: none; + } + } +} + +.buttonsMobile { + position: sticky; + bottom: 55px; // Height of bottom nav bar. + + @container (width >= #{$button-breakpoint}) { + display: none; + } + + @supports (not (container-type: inline-size)) { + @media (min-width: (#{$button-fallback-breakpoint} + 1px)) { + display: none; + } + } +} + +.buttonsMobileIsStuck { + padding: 12px 16px; + background-color: var(--color-bg-primary); + border-top: 1px solid var(--color-border-primary); + margin: 0 -20px; +} + .badge { background-color: var(--color-bg-secondary); border: none; @@ -84,36 +125,112 @@ svg.badgeIcon { } } -.fieldList { +.fieldWrapper { margin-top: 16px; + width: 100%; + position: relative; +} + +.fieldWrapper::before, +.fieldWrapper::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 40px; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s ease-in-out; + z-index: 1; +} + +.fieldWrapper::before { + left: 0; + background: linear-gradient( + to left, + transparent 0%, + var(--color-bg-primary) 100% + ); +} + +.fieldWrapper::after { + right: 0; + background: linear-gradient( + to right, + transparent 0%, + var(--color-bg-primary) 100% + ); +} + +.fieldWrapperLeft::before { + opacity: 1; +} + +.fieldWrapperRight::after { + opacity: 1; +} + +.fieldList { + display: flex; + flex-wrap: nowrap; + gap: 4px; + scroll-snap-type: x mandatory; + scroll-padding-left: 40px; + scroll-padding-right: 40px; + scroll-behavior: smooth; + overflow-x: scroll; + scrollbar-width: none; + overflow-y: visible; } .fieldCard { - position: relative; + scroll-snap-align: start; - a { - color: var(--color-text-brand); - text-decoration: none; + &:focus-visible, + &:focus-within { + outline: var(--outline-focus-default); + outline-offset: -2px; + } + + :is(dt, dd) { + max-width: 200px; } } .fieldCardVerified { background-color: var(--color-bg-brand-softer); - - dt { - padding-right: 1rem; - } - - .fieldIconVerified { - position: absolute; - top: 4px; - right: 4px; - } } -.fieldIconVerified { - width: 1rem; - height: 1rem; +.fieldArrowButton { + position: absolute; + top: 50%; + transform: translateY(-50%); + background-color: var(--color-bg-elevated); + box-shadow: 0 1px 4px 0 var(--color-shadow-primary); + border-radius: 9999px; + transition: + color 0.2s ease-in-out, + background-color 0.2s ease-in-out; + outline-offset: 2px; + z-index: 2; + + &:first-child { + left: 4px; + } + + &:last-child { + right: 4px; + } + + &:hover, + &:focus, + &:focus-visible { + background-color: color-mix( + in oklab, + var(--color-bg-brand-base) var(--overlay-strength-brand), + var(--color-bg-elevated) + ); + } } .fieldNumbersWrapper { diff --git a/app/javascript/mastodon/features/account_timeline/v2/featured_tags.tsx b/app/javascript/mastodon/features/account_timeline/v2/featured_tags.tsx index bdcff2c7e9..48ce9cfaa5 100644 --- a/app/javascript/mastodon/features/account_timeline/v2/featured_tags.tsx +++ b/app/javascript/mastodon/features/account_timeline/v2/featured_tags.tsx @@ -9,7 +9,7 @@ import { useParams } from 'react-router'; import { fetchFeaturedTags } from '@/mastodon/actions/featured_tags'; import { useAppHistory } from '@/mastodon/components/router'; import { Tag } from '@/mastodon/components/tags/tag'; -import { useOverflow } from '@/mastodon/hooks/useOverflow'; +import { useOverflowButton } from '@/mastodon/hooks/useOverflow'; import { selectAccountFeaturedTags } from '@/mastodon/selectors/accounts'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; @@ -30,7 +30,7 @@ export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => { // Get list of tags with overflow handling. const [showOverflow, setShowOverflow] = useState(false); const { hiddenCount, wrapperRef, listRef, hiddenIndex, maxWidth } = - useOverflow(); + useOverflowButton(); // Handle whether to show all tags. const handleOverflowClick: MouseEventHandler = useCallback(() => { diff --git a/app/javascript/mastodon/features/collections/editor.tsx b/app/javascript/mastodon/features/collections/editor.tsx index 7896bfe7b6..af301e121c 100644 --- a/app/javascript/mastodon/features/collections/editor.tsx +++ b/app/javascript/mastodon/features/collections/editor.tsx @@ -16,7 +16,11 @@ import type { import { Button } from 'mastodon/components/button'; import { Column } from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; -import { CheckboxField, TextAreaField } from 'mastodon/components/form_fields'; +import { + CheckboxField, + FormStack, + TextAreaField, +} from 'mastodon/components/form_fields'; import { TextInputField } from 'mastodon/components/form_fields/text_input_field'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { @@ -129,88 +133,80 @@ const CollectionSettings: React.FC<{ ); return ( - - - - } - hint={ - - } - value={name} - onChange={handleNameChange} - maxLength={40} - /> - + + + } + hint={ + + } + value={name} + onChange={handleNameChange} + maxLength={40} + /> - - - } - hint={ - - } - value={description} - onChange={handleDescriptionChange} - maxLength={100} - /> - + + } + hint={ + + } + value={description} + onChange={handleDescriptionChange} + maxLength={100} + /> - - - } - hint={ - - } - value={topic} - onChange={handleTopicChange} - maxLength={40} - /> - + + } + hint={ + + } + value={topic} + onChange={handleTopicChange} + maxLength={40} + /> - - - } - hint={ - - } - checked={sensitive} - onChange={handleSensitiveChange} - /> - + + } + hint={ + + } + checked={sensitive} + onChange={handleSensitiveChange} + /> @@ -221,7 +217,7 @@ const CollectionSettings: React.FC<{ )} - + ); }; diff --git a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx index d297d7cee5..45b867ad9d 100644 --- a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx +++ b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.tsx @@ -1,9 +1,10 @@ -import { useEffect, useCallback, useRef, useState, useId } from 'react'; +import { useEffect, useCallback, useId } from 'react'; import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; import { Link } from 'react-router-dom'; +import { useOverflowScroll } from '@/mastodon/hooks/useOverflow'; import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; @@ -178,74 +179,24 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call state.settings.getIn(['dismissed_banners', DISMISSIBLE_ID]) as boolean, ); - const bodyRef = useRef(null); - const [canScrollLeft, setCanScrollLeft] = useState(false); - const [canScrollRight, setCanScrollRight] = useState(true); useEffect(() => { void dispatch(fetchSuggestions()); }, [dispatch]); - useEffect(() => { - if (!bodyRef.current) { - return; - } - - if (getComputedStyle(bodyRef.current).direction === 'rtl') { - setCanScrollLeft( - bodyRef.current.clientWidth - bodyRef.current.scrollLeft < - bodyRef.current.scrollWidth, - ); - setCanScrollRight(bodyRef.current.scrollLeft < 0); - } else { - setCanScrollLeft(bodyRef.current.scrollLeft > 0); - setCanScrollRight( - bodyRef.current.scrollLeft + bodyRef.current.clientWidth < - bodyRef.current.scrollWidth, - ); - } - }, [setCanScrollRight, setCanScrollLeft, suggestions]); - - const handleLeftNav = useCallback(() => { - if (!bodyRef.current) { - return; - } - - bodyRef.current.scrollLeft -= 200; - }, []); - - const handleRightNav = useCallback(() => { - if (!bodyRef.current) { - return; - } - - bodyRef.current.scrollLeft += 200; - }, []); - - const handleScroll = useCallback(() => { - if (!bodyRef.current) { - return; - } - - if (getComputedStyle(bodyRef.current).direction === 'rtl') { - setCanScrollLeft( - bodyRef.current.clientWidth - bodyRef.current.scrollLeft < - bodyRef.current.scrollWidth, - ); - setCanScrollRight(bodyRef.current.scrollLeft < 0); - } else { - setCanScrollLeft(bodyRef.current.scrollLeft > 0); - setCanScrollRight( - bodyRef.current.scrollLeft + bodyRef.current.clientWidth < - bodyRef.current.scrollWidth, - ); - } - }, [setCanScrollRight, setCanScrollLeft]); - const handleDismiss = useCallback(() => { dispatch(changeSetting(['dismissed_banners', DISMISSIBLE_ID], true)); }, [dispatch]); + const { + bodyRef, + handleScroll, + canScrollLeft, + canScrollRight, + handleLeftNav, + handleRightNav, + } = useOverflowScroll({ absoluteDistance: true }); + if (dismissed || (!isLoading && suggestions.length === 0)) { return null; } diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 30d7578c55..4aae94574b 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -88,7 +88,6 @@ export const MODAL_COMPONENTS = { 'ANNUAL_REPORT': AnnualReportModal, 'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }), 'ACCOUNT_NOTE': () => import('@/mastodon/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })), - 'ACCOUNT_FIELDS': () => import('mastodon/features/account_timeline/components/fields_modal.tsx').then(module => ({ default: module.AccountFieldsModal })), }; export default class ModalRoot extends PureComponent { diff --git a/app/javascript/mastodon/hooks/useOverflow.ts b/app/javascript/mastodon/hooks/useOverflow.ts index e5a9ab407e..b306fb4871 100644 --- a/app/javascript/mastodon/hooks/useOverflow.ts +++ b/app/javascript/mastodon/hooks/useOverflow.ts @@ -1,14 +1,15 @@ +import type { MutableRefObject, RefCallback } from 'react'; import { useState, useRef, useCallback, useEffect } from 'react'; /** - * Calculate and manage overflow of child elements within a container. + * Hook to manage overflow of items in a container with a "more" button. * * To use, wire up the `wrapperRef` to the container element, and the `listRef` to the * child element that contains the items to be measured. If autoResize is true, * the list element will have its max-width set to prevent wrapping. The listRef element * requires both position:relative and overflow:hidden styles to work correctly. */ -export function useOverflow({ +export function useOverflowButton({ autoResize, padding = 4, }: { autoResize?: boolean; padding?: number } = {}) { @@ -76,6 +77,111 @@ export function useOverflow({ } }, [autoResize, maxWidth]); + const { listRefCallback, wrapperRefCallback } = useOverflowObservers({ + onRecalculate: handleRecalculate, + onListRef: listRef, + }); + + return { + hiddenCount, + hasOverflow: hiddenCount > 0, + wrapperRef: wrapperRefCallback, + hiddenIndex, + maxWidth, + listRef: listRefCallback, + recalculate: handleRecalculate, + }; +} + +export function useOverflowScroll({ + widthOffset = 200, + absoluteDistance = false, +} = {}) { + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + const bodyRef = useRef(null); + + // Recalculate scrollable state + const handleRecalculate = useCallback(() => { + if (!bodyRef.current) { + return; + } + + if (getComputedStyle(bodyRef.current).direction === 'rtl') { + setCanScrollLeft( + bodyRef.current.clientWidth - bodyRef.current.scrollLeft < + bodyRef.current.scrollWidth, + ); + setCanScrollRight(bodyRef.current.scrollLeft < 0); + } else { + setCanScrollLeft(bodyRef.current.scrollLeft > 0); + setCanScrollRight( + bodyRef.current.scrollLeft + bodyRef.current.clientWidth < + bodyRef.current.scrollWidth, + ); + } + }, []); + + const { wrapperRefCallback } = useOverflowObservers({ + onRecalculate: handleRecalculate, + onWrapperRef: bodyRef, + }); + + useEffect(() => { + handleRecalculate(); + }, [handleRecalculate]); + + // Handle scroll event using requestAnimationFrame to avoid excessive recalculations. + const handleScroll = useCallback(() => { + requestAnimationFrame(handleRecalculate); + }, [handleRecalculate]); + + // Jump a full screen minus the width offset so that we don't skip a lot. + const handleLeftNav = useCallback(() => { + if (!bodyRef.current) { + return; + } + + bodyRef.current.scrollLeft -= absoluteDistance + ? widthOffset + : Math.max(widthOffset, bodyRef.current.clientWidth - widthOffset); + }, [absoluteDistance, widthOffset]); + + const handleRightNav = useCallback(() => { + if (!bodyRef.current) { + return; + } + + bodyRef.current.scrollLeft += absoluteDistance + ? widthOffset + : Math.max(widthOffset, bodyRef.current.clientWidth - widthOffset); + }, [absoluteDistance, widthOffset]); + + return { + bodyRef: wrapperRefCallback, + canScrollLeft, + canScrollRight, + handleLeftNav, + handleRightNav, + handleScroll, + }; +} + +export function useOverflowObservers({ + onRecalculate, + onListRef, + onWrapperRef, +}: { + onRecalculate: () => void; + onListRef?: RefCallback | MutableRefObject; + onWrapperRef?: + | RefCallback + | MutableRefObject; +}) { + // This is the item container element. + const listRef = useRef(null); + // Set up observers to watch for size and content changes. const resizeObserverRef = useRef(null); const mutationObserverRef = useRef(null); @@ -83,10 +189,10 @@ export function useOverflow({ // Helper to get or create the resize observer. const resizeObserver = useCallback(() => { const observer = (resizeObserverRef.current ??= new ResizeObserver( - handleRecalculate, + onRecalculate, )); return observer; - }, [handleRecalculate]); + }, [onRecalculate]); // Iterate through children and observe them for size changes. const handleChildrenChange = useCallback(() => { @@ -100,8 +206,8 @@ export function useOverflow({ } } } - handleRecalculate(); - }, [handleRecalculate, resizeObserver]); + onRecalculate(); + }, [onRecalculate, resizeObserver]); // Helper to get or create the mutation observer. const mutationObserver = useCallback(() => { @@ -129,9 +235,14 @@ export function useOverflow({ if (node) { wrapperRef.current = node; handleObserve(); + if (typeof onWrapperRef === 'function') { + onWrapperRef(node); + } else if (onWrapperRef && 'current' in onWrapperRef) { + onWrapperRef.current = node; + } } }, - [handleObserve], + [handleObserve, onWrapperRef], ); // If there are changes to the children, recalculate which are visible. @@ -140,9 +251,14 @@ export function useOverflow({ if (node) { listRef.current = node; handleObserve(); + if (typeof onListRef === 'function') { + onListRef(node); + } else if (onListRef && 'current' in onListRef) { + onListRef.current = node; + } } }, - [handleObserve], + [handleObserve, onListRef], ); useEffect(() => { @@ -161,12 +277,7 @@ export function useOverflow({ }, [handleObserve]); return { - hiddenCount, - hasOverflow: hiddenCount > 0, - wrapperRef: wrapperRefCallback, - hiddenIndex, - maxWidth, - listRef: listRefCallback, - recalculate: handleRecalculate, + wrapperRefCallback, + listRefCallback, }; } diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 02cbb73877..0a9987e40c 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -17,8 +17,15 @@ "account.activity": "Актыўнасць", "account.add_note": "Дадаць асабістую нататку", "account.add_or_remove_from_list": "Дадаць або выдаліць са спісаў", + "account.badges.admin": "Адмін", + "account.badges.blocked": "Заблакіраваны", "account.badges.bot": "Бот", + "account.badges.domain_blocked": "Заблакіраваны дамен", + "account.badges.follows_you": "Падпісаны(-ая) на Вас", "account.badges.group": "Група", + "account.badges.muted": "Ігнаруецца", + "account.badges.mutuals": "Вы падпісаныя адно на аднаго", + "account.badges.requested_to_follow": "Адправіў(-ла) запыт на падпіску", "account.block": "Заблакіраваць @{name}", "account.block_domain": "Заблакіраваць дамен {domain}", "account.block_short": "Заблакіраваць", @@ -79,7 +86,7 @@ "account.mute_short": "Ігнараваць", "account.muted": "Ігнаруецца", "account.muting": "Ігнараванне", - "account.mutual": "Вы падпісаны адно на аднаго", + "account.mutual": "Вы падпісаныя адно на аднаго", "account.no_bio": "Апісанне адсутнічае.", "account.node_modal.callout": "Асабістыя нататкі бачныя толькі Вам.", "account.node_modal.edit_title": "Рэдагаваць асабістую нататку", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 185e3a54ac..be5f2eec41 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -17,8 +17,15 @@ "account.activity": "Aktivitet", "account.add_note": "Tilføj en personlig note", "account.add_or_remove_from_list": "Tilføj eller fjern fra lister", + "account.badges.admin": "Admin", + "account.badges.blocked": "Blokeret", "account.badges.bot": "Automatisert", + "account.badges.domain_blocked": "Blokeret domæne", + "account.badges.follows_you": "Følger dig", "account.badges.group": "Gruppe", + "account.badges.muted": "Skjult", + "account.badges.mutuals": "I følger hinanden", + "account.badges.requested_to_follow": "Anmodede om at følge dig", "account.block": "Blokér @{name}", "account.block_domain": "Blokér domænet {domain}", "account.block_short": "Bloker", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index c67339dd32..7ae66c2984 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -17,8 +17,15 @@ "account.activity": "Aktivitäten", "account.add_note": "Persönliche Notiz hinzufügen", "account.add_or_remove_from_list": "Listen verwalten", + "account.badges.admin": "Admin", + "account.badges.blocked": "Blockiert", "account.badges.bot": "Bot", + "account.badges.domain_blocked": "Domain blockiert", + "account.badges.follows_you": "Folgt dir", "account.badges.group": "Gruppe", + "account.badges.muted": "Stummgeschaltet", + "account.badges.mutuals": "Ihr folgt einander", + "account.badges.requested_to_follow": "Möchte dir folgen", "account.block": "@{name} blockieren", "account.block_domain": "{domain} blockieren", "account.block_short": "Blockieren", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 17dd53a7b3..7140d344a9 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -17,8 +17,15 @@ "account.activity": "Δραστηριότητα", "account.add_note": "Προσθέστε μια προσωπική σημείωση", "account.add_or_remove_from_list": "Προσθήκη ή Αφαίρεση από λίστες", + "account.badges.admin": "Διαχειριστής", + "account.badges.blocked": "Αποκλεισμένος", "account.badges.bot": "Αυτοματοποιημένος", + "account.badges.domain_blocked": "Αποκλεισμένος τομέας", + "account.badges.follows_you": "Σε ακολουθεί", "account.badges.group": "Ομάδα", + "account.badges.muted": "Σε σίγαση", + "account.badges.mutuals": "Ακολουθείτε ο ένας τον άλλο", + "account.badges.requested_to_follow": "Ζήτησε να σε ακολουθήσει", "account.block": "Αποκλεισμός @{name}", "account.block_domain": "Αποκλεισμός τομέα {domain}", "account.block_short": "Αποκλεισμός", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 84c656903c..d2c7ed4d39 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -17,8 +17,15 @@ "account.activity": "Activity", "account.add_note": "Add a personal note", "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.admin": "Admin", + "account.badges.blocked": "Blocked", "account.badges.bot": "Automated", + "account.badges.domain_blocked": "Blocked domain", + "account.badges.follows_you": "Follows you", "account.badges.group": "Group", + "account.badges.muted": "Muted", + "account.badges.mutuals": "You follow each other", + "account.badges.requested_to_follow": "Requested to follow you", "account.block": "Block @{name}", "account.block_domain": "Block domain {domain}", "account.block_short": "Block", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 0ed0b7599c..b722b21898 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -21,11 +21,8 @@ "account.badges.blocked": "Blocked", "account.badges.bot": "Automated", "account.badges.domain_blocked": "Blocked domain", - "account.badges.follows_you": "Follows you", "account.badges.group": "Group", "account.badges.muted": "Muted", - "account.badges.mutuals": "You follow each other", - "account.badges.requested_to_follow": "Requested to follow you", "account.block": "Block @{name}", "account.block_domain": "Block domain {domain}", "account.block_short": "Block", @@ -49,6 +46,8 @@ "account.featured.hashtags": "Hashtags", "account.featured_tags.last_status_at": "Last post on {date}", "account.featured_tags.last_status_never": "No posts", + "account.fields.scroll_next": "Show next", + "account.fields.scroll_prev": "Show previous", "account.filters.all": "All activity", "account.filters.boosts_toggle": "Show boosts", "account.filters.posts_boosts": "Posts and boosts", @@ -80,6 +79,23 @@ "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", "account.mention": "Mention @{name}", + "account.menu.add_to_list": "Add to list…", + "account.menu.block": "Block account", + "account.menu.block_domain": "Block {domain}", + "account.menu.copied": "Copied account link to clipboard", + "account.menu.copy": "Copy link", + "account.menu.direct": "Privately mention", + "account.menu.hide_reblogs": "Hide boosts in timeline", + "account.menu.mention": "Mention", + "account.menu.mute": "Mute account", + "account.menu.open_original_page": "View on {domain}", + "account.menu.remove_follower": "Remove follower", + "account.menu.report": "Report account", + "account.menu.share": "Share…", + "account.menu.show_reblogs": "Show boosts in timeline", + "account.menu.unblock": "Unblock account", + "account.menu.unblock_domain": "Unblock {domain}", + "account.menu.unmute": "Unmute account", "account.moved_to": "{name} has indicated that their new account is now:", "account.mute": "Mute @{name}", "account.mute_notifications_short": "Mute notifications", @@ -115,8 +131,6 @@ "account.unmute": "Unmute @{name}", "account.unmute_notifications_short": "Unmute notifications", "account.unmute_short": "Unmute", - "account_fields_modal.close": "Close", - "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Click to add note", "admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up", @@ -639,7 +653,6 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", - "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 8563cbae81..0a78a6ffea 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -17,8 +17,15 @@ "account.activity": "Actividad", "account.add_note": "Agregar una nota personal", "account.add_or_remove_from_list": "Agregar o quitar de las listas", + "account.badges.admin": "Administración", + "account.badges.blocked": "Cuenta bloqueada", "account.badges.bot": "Automatizada", + "account.badges.domain_blocked": "Dominio bloqueado", + "account.badges.follows_you": "Te sigue", "account.badges.group": "Grupo", + "account.badges.muted": "Cuenta silenciada", + "account.badges.mutuals": "Seguimiento mutuo", + "account.badges.requested_to_follow": "Solicitó seguirte", "account.block": "Bloquear a @{name}", "account.block_domain": "Bloquear dominio {domain}", "account.block_short": "Bloquear", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 425c0f058c..1ce981a91d 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -17,8 +17,15 @@ "account.activity": "Actividad", "account.add_note": "Añadir una nota personal", "account.add_or_remove_from_list": "Agregar o eliminar de las listas", + "account.badges.admin": "Administrador", + "account.badges.blocked": "Bloqueado", "account.badges.bot": "Automatizada", + "account.badges.domain_blocked": "Dominio bloqueado", + "account.badges.follows_you": "Te sigue", "account.badges.group": "Grupo", + "account.badges.muted": "Silenciado", + "account.badges.mutuals": "Se siguen el uno al otro", + "account.badges.requested_to_follow": "Solicitó seguirte", "account.block": "Bloquear a @{name}", "account.block_domain": "Bloquear dominio {domain}", "account.block_short": "Bloquear", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index eb1cdc578c..5fdc11a972 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -17,8 +17,15 @@ "account.activity": "Tegevus", "account.add_note": "Lisa isiklik märge", "account.add_or_remove_from_list": "Lisa või eemalda loenditest", + "account.badges.admin": "Peakasutaja", + "account.badges.blocked": "Blokeeritud", "account.badges.bot": "Robot", + "account.badges.domain_blocked": "Blokeeritud domeen", + "account.badges.follows_you": "Jälgib sind", "account.badges.group": "Grupp", + "account.badges.muted": "Summutatud", + "account.badges.mutuals": "Te jälgite teineteist", + "account.badges.requested_to_follow": "Soovib sind jälgida", "account.block": "Blokeeri @{name}", "account.block_domain": "Blokeeri kõik domeenist {domain}", "account.block_short": "Blokeerimine", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index d4fe7cee82..810e787b03 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -17,8 +17,15 @@ "account.activity": "Toiminta", "account.add_note": "Lisää henkilökohtainen muistiinpano", "account.add_or_remove_from_list": "Lisää tai poista listoista", + "account.badges.admin": "Ylläpitäjä", + "account.badges.blocked": "Estetty", "account.badges.bot": "Botti", + "account.badges.domain_blocked": "Estetty verkkotunnus", + "account.badges.follows_you": "Seuraa sinua", "account.badges.group": "Ryhmä", + "account.badges.muted": "Mykistetty", + "account.badges.mutuals": "Seuraatte toisianne", + "account.badges.requested_to_follow": "Pyytänyt lupaa seurata sinua", "account.block": "Estä @{name}", "account.block_domain": "Estä verkkotunnus {domain}", "account.block_short": "Estä", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 16861bb5a2..a48c15f7d0 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -17,8 +17,15 @@ "account.activity": "Virksemi", "account.add_note": "Legg persónliga notu afturat", "account.add_or_remove_from_list": "Legg afturat ella tak av listum", + "account.badges.admin": "Umsitari", + "account.badges.blocked": "Blokerað/ur", "account.badges.bot": "Bottur", + "account.badges.domain_blocked": "Blokerað økisnavn", + "account.badges.follows_you": "Fylgir tær", "account.badges.group": "Bólkur", + "account.badges.muted": "Doyvdur", + "account.badges.mutuals": "Tit fylgja hvønn annan", + "account.badges.requested_to_follow": "Hevur biðið um at fylgja tær", "account.block": "Banna @{name}", "account.block_domain": "Banna økisnavnið {domain}", "account.block_short": "Banna", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index eff2112649..09e1b993e6 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -17,8 +17,15 @@ "account.activity": "Activités", "account.add_note": "Ajouter une note personnelle", "account.add_or_remove_from_list": "Ajouter ou enlever de listes", + "account.badges.admin": "Admin", + "account.badges.blocked": "Bloqué", "account.badges.bot": "Bot", + "account.badges.domain_blocked": "Domaine bloqué", + "account.badges.follows_you": "Vous suit", "account.badges.group": "Groupe", + "account.badges.muted": "Masqué", + "account.badges.mutuals": "Vous vous suivez mutuellement", + "account.badges.requested_to_follow": "A demandé à vous suivre", "account.block": "Bloquer @{name}", "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index e07584de63..ccaeaa8772 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -17,8 +17,15 @@ "account.activity": "Activités", "account.add_note": "Ajouter une note personnelle", "account.add_or_remove_from_list": "Ajouter ou retirer des listes", + "account.badges.admin": "Admin", + "account.badges.blocked": "Bloqué", "account.badges.bot": "Bot", + "account.badges.domain_blocked": "Domaine bloqué", + "account.badges.follows_you": "Vous suit", "account.badges.group": "Groupe", + "account.badges.muted": "Masqué", + "account.badges.mutuals": "Vous vous suivez mutuellement", + "account.badges.requested_to_follow": "A demandé à vous suivre", "account.block": "Bloquer @{name}", "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", @@ -73,7 +80,7 @@ "account.locked_info": "Ce compte est privé. Son ou sa propriétaire approuve manuellement qui peut le suivre.", "account.media": "Médias", "account.mention": "Mentionner @{name}", - "account.moved_to": "{name} a indiqué que son nouveau compte est maintenant :", + "account.moved_to": "{name} a indiqué que son nouveau compte est maintenant :", "account.mute": "Masquer @{name}", "account.mute_notifications_short": "Désactiver les notifications", "account.mute_short": "Mettre en sourdine", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 06b940593c..5041637d86 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -17,8 +17,15 @@ "account.activity": "Actividade", "account.add_note": "Engadir nota persoal", "account.add_or_remove_from_list": "Engadir ou eliminar das listaxes", + "account.badges.admin": "Admin", + "account.badges.blocked": "Bloqueada", "account.badges.bot": "Automatizada", + "account.badges.domain_blocked": "Dominio bloqueado", + "account.badges.follows_you": "Séguete", "account.badges.group": "Grupo", + "account.badges.muted": "Silenciada", + "account.badges.mutuals": "Seguimento mútuo", + "account.badges.requested_to_follow": "Solicitou seguirte", "account.block": "Bloquear @{name}", "account.block_domain": "Bloquear o dominio {domain}", "account.block_short": "Bloquear", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 91eb94e52b..531aec11b7 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -17,8 +17,15 @@ "account.activity": "פעילות", "account.add_note": "הוספת הערה פרטית", "account.add_or_remove_from_list": "הוספה או הסרה מרשימות", + "account.badges.admin": "מנהל", + "account.badges.blocked": "חסום", "account.badges.bot": "בוט", + "account.badges.domain_blocked": "דומיין חסום", + "account.badges.follows_you": "במעקב אחריך", "account.badges.group": "קבוצה", + "account.badges.muted": "מושתק", + "account.badges.mutuals": "אתם עוקביםות הדדית", + "account.badges.requested_to_follow": "ביקשו לעקוב אחריך", "account.block": "חסמי את @{name}", "account.block_domain": "חסמו את קהילת {domain}", "account.block_short": "לחסום", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 4ce5cfa4c8..511cc91805 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -17,8 +17,15 @@ "account.activity": "Virkni", "account.add_note": "Bæta við einkaminnispunkti", "account.add_or_remove_from_list": "Bæta við eða fjarlægja af listum", + "account.badges.admin": "Stjóri", + "account.badges.blocked": "Lokað á", "account.badges.bot": "Yrki", + "account.badges.domain_blocked": "Útilokað lén", + "account.badges.follows_you": "Fylgist með þér", "account.badges.group": "Hópur", + "account.badges.muted": "Þaggað", + "account.badges.mutuals": "Þið fylgist með hvor öðrum", + "account.badges.requested_to_follow": "Bað um að fylgjast með þér", "account.block": "Loka á @{name}", "account.block_domain": "Útiloka lénið {domain}", "account.block_short": "Útiloka", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index c813e0f6b8..5b4559ba1b 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -17,8 +17,15 @@ "account.activity": "Attività", "account.add_note": "Aggiungi una nota personale", "account.add_or_remove_from_list": "Aggiungi o Rimuovi dalle liste", + "account.badges.admin": "Amministratore/trice", + "account.badges.blocked": "Bloccato", "account.badges.bot": "Bot", + "account.badges.domain_blocked": "Dominio bloccato", + "account.badges.follows_you": "Ti segue", "account.badges.group": "Gruppo", + "account.badges.muted": "Silenziato", + "account.badges.mutuals": "Vi seguite a vicenda", + "account.badges.requested_to_follow": "Ha richiesto di seguirti", "account.block": "Blocca @{name}", "account.block_domain": "Blocca dominio {domain}", "account.block_short": "Blocca", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 154d4aeb42..23461f9c9f 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -14,9 +14,18 @@ "about.powered_by": "Decentralizuota socialinė medija, veikianti pagal „{mastodon}“", "about.rules": "Serverio taisyklės", "account.account_note_header": "Asmeninė pastaba", + "account.activity": "Veikla", + "account.add_note": "Pridėti asmeninę pastabą", "account.add_or_remove_from_list": "Įtraukti arba šalinti iš sąrašų", + "account.badges.admin": "Administratorius", + "account.badges.blocked": "Užblokuota", "account.badges.bot": "Automatizuotas", + "account.badges.domain_blocked": "Užblokuoti serveriai", + "account.badges.follows_you": "Seka jus", "account.badges.group": "Grupė", + "account.badges.muted": "Nutildytas", + "account.badges.mutuals": "Sekate vienas kitą", + "account.badges.requested_to_follow": "Prašymai sekti jus", "account.block": "Blokuoti @{name}", "account.block_domain": "Blokuoti serverį {domain}", "account.block_short": "Blokuoti", @@ -27,6 +36,7 @@ "account.direct": "Privačiai paminėti @{name}", "account.disable_notifications": "Nustoti man pranešti, kai @{name} paskelbia", "account.domain_blocking": "Blokuoti domeną", + "account.edit_note": "Redaguoti asmeninę pastabą", "account.edit_profile": "Redaguoti profilį", "account.edit_profile_short": "Redaguoti", "account.enable_notifications": "Pranešti man, kai @{name} paskelbia", @@ -39,6 +49,11 @@ "account.featured.hashtags": "Grotažymės", "account.featured_tags.last_status_at": "Paskutinis įrašas {date}", "account.featured_tags.last_status_never": "Nėra įrašų", + "account.filters.all": "Visa veikla", + "account.filters.boosts_toggle": "Rodyti pasidalinimus", + "account.filters.posts_boosts": "Įrašai ir pasidalinimai", + "account.filters.posts_only": "Įrašai", + "account.filters.replies_toggle": "rodyti atsakymus", "account.follow": "Sekti", "account.follow_back": "Sekti atgal", "account.follow_back_short": "Sekti atgal", @@ -57,6 +72,7 @@ "account.go_to_profile": "Eiti į profilį", "account.hide_reblogs": "Slėpti pasidalinimus iš @{name}", "account.in_memoriam": "Atminimui.", + "account.joined_long": "Prisijungė {date}", "account.joined_short": "Prisijungė", "account.languages": "Keisti prenumeruojamas kalbas", "account.link_verified_on": "Šios nuorodos nuosavybė buvo patikrinta {date}", @@ -71,6 +87,14 @@ "account.muting": "Užtildymas", "account.mutual": "Sekate vienas kitą", "account.no_bio": "Nėra pateikto aprašymo.", + "account.node_modal.callout": "Asmeninės pastabos matomos tik jums.", + "account.node_modal.edit_title": "Redaguoti asmeninę pastabą", + "account.node_modal.error_unknown": "Nepavyko išsaugoti pastabos", + "account.node_modal.field_label": "Asmeninė pastaba", + "account.node_modal.save": "Išsaugoti", + "account.node_modal.title": "Pridėti asmeninę pastabą", + "account.note.edit_button": "Redaguoti", + "account.note.title": "Asmeninės pastabos (matomos tik jums)", "account.open_original_page": "Atidaryti originalų puslapį", "account.posts": "Įrašai", "account.posts_with_replies": "Įrašai ir atsakymai", @@ -90,6 +114,8 @@ "account.unmute": "Atšaukti nutildymą @{name}", "account.unmute_notifications_short": "Atšaukti pranešimų nutildymą", "account.unmute_short": "Atšaukti nutildymą", + "account_fields_modal.close": "Uždaryti", + "account_fields_modal.title": "{name} informacija", "account_note.placeholder": "Spustelėk, kad pridėtum pastabą.", "admin.dashboard.daily_retention": "Naudotojų pasilikimo rodiklis pagal dieną po registracijos", "admin.dashboard.monthly_retention": "Naudotojų pasilikimo rodiklis pagal mėnesį po registracijos", @@ -114,9 +140,20 @@ "alt_text_modal.done": "Atlikta", "announcement.announcement": "Skelbimas", "annual_report.announcement.action_build": "Sukurti mano Wrapstodon", + "annual_report.announcement.action_dismiss": "Ne, ačiū", "annual_report.announcement.action_view": "Peržiūrėti mano Wrapstodon", "annual_report.announcement.description": "Sužinokite daugiau apie savo aktyvumą Mastodon\"e per pastaruosius metus.", "annual_report.announcement.title": "Wrapstodon {year} jau čia", + "annual_report.nav_item.badge": "Nauja", + "annual_report.shared_page.donate": "Paremti", + "annual_report.shared_page.footer": "Sukurta su {heart} Mastodon komandos", + "annual_report.shared_page.footer_server_info": "{username} naudoja {domain}, vieną iš daugelio bendruomenių, kurios veikia „Mastodon“ dėka.", + "annual_report.summary.archetype.booster.desc_public": "{name} tęsė ieškojimus, kokiais įrašais pasidalinti, kad padėtų kitiems kūrėjams pasiekti puikių rezultatų.", + "annual_report.summary.archetype.booster.desc_self": "Tu nuolat ieškojai įrašų, kurais galėtum pasidalinti, tuo puikiai padėdamas kitiems kūrėjams.", + "annual_report.summary.archetype.booster.name": "Lankininkas", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.close": "Užverti", + "annual_report.summary.highlighted_post.title": "Populiariausi įrašai", "annual_report.summary.most_used_app.most_used_app": "labiausiai naudota programa", "annual_report.summary.most_used_hashtag.most_used_hashtag": "labiausiai naudota grotažymė", "annual_report.summary.new_posts.new_posts": "nauji įrašai", @@ -243,11 +280,11 @@ "confirmations.private_quote_notify.cancel": "Grįžti prie redagavimo", "confirmations.private_quote_notify.confirm": "Paskelbti įrašą", "confirmations.private_quote_notify.do_not_show_again": "Neberodyti šio pranešimo dar kartą", - "confirmations.private_quote_notify.message": "Asmuo, kurį paminite, ir kiti paminėti asmenys bus informuoti ir galės peržiūrėti jūsų įrašą, net jei jie neseka jūsų.", + "confirmations.private_quote_notify.message": "Asmuo, kurį komentuojate, ir kiti paminėti asmenys bus informuoti ir galės peržiūrėti jūsų įrašą, net jei jie neseka jūsų.", "confirmations.private_quote_notify.title": "Bendrinti su sekėjais ir paminėtais (su @) naudotojais?", "confirmations.quiet_post_quote_info.dismiss": "Daugiau man nepriminti", "confirmations.quiet_post_quote_info.got_it": "Supratau", - "confirmations.quiet_post_quote_info.message": "Kai norite paminėti tylų viešą įrašą, jūsų įrašas bus paslėptas Tendencijų sąrašuose.", + "confirmations.quiet_post_quote_info.message": "Kai norite komentuoti užtildytą viešą įrašą, jūsų įrašas bus paslėptas „Populiaru“ sąrašuose.", "confirmations.quiet_post_quote_info.title": "Kai paminite tylius viešus įrašus", "confirmations.redraft.confirm": "Ištrinti ir iš naujo parengti", "confirmations.redraft.message": "Ar tikrai nori ištrinti šį įrašą ir parengti jį iš naujo? Bus prarasti mėgstami ir pasidalinimai, o atsakymai į originalų įrašą bus panaikinti.", @@ -442,7 +479,7 @@ "hints.profiles.see_more_followers": "Žiūrėti daugiau sekėjų serveryje {domain}", "hints.profiles.see_more_follows": "Žiūrėti daugiau sekimų serveryje {domain}", "hints.profiles.see_more_posts": "Žiūrėti daugiau įrašų serveryje {domain}", - "home.column_settings.show_quotes": "Rodyti paminėjimus", + "home.column_settings.show_quotes": "Rodyti komentarus", "home.column_settings.show_reblogs": "Rodyti pakėlimus", "home.column_settings.show_replies": "Rodyti atsakymus", "home.hide_announcements": "Slėpti skelbimus", @@ -497,7 +534,7 @@ "keyboard_shortcuts.open_media": "Atidaryti mediją", "keyboard_shortcuts.pinned": "Atverti prisegtų įrašų sąrašą", "keyboard_shortcuts.profile": "Atidaryti autoriaus (-ės) profilį", - "keyboard_shortcuts.quote": "Paminėti įrašą", + "keyboard_shortcuts.quote": "Komentuoti įrašą", "keyboard_shortcuts.reply": "Atsakyti į įrašą", "keyboard_shortcuts.requests": "Atidaryti sekimo prašymų sąrašą", "keyboard_shortcuts.search": "Fokusuoti paieškos juostą", @@ -612,7 +649,7 @@ "notification.label.mention": "Paminėjimas", "notification.label.private_mention": "Privatus paminėjimas", "notification.label.private_reply": "Privatus atsakymas", - "notification.label.quote": "{name} paminėjo jūsų įrašą", + "notification.label.quote": "{name} pakomentavo jūsų įrašą", "notification.label.reply": "Atsakymas", "notification.mention": "Paminėjimas", "notification.mentioned_you": "{name} paminėjo jus", @@ -627,7 +664,7 @@ "notification.moderation_warning.action_suspend": "Tavo paskyra buvo sustabdyta.", "notification.own_poll": "Tavo apklausa baigėsi", "notification.poll": "Baigėsi apklausa, kurioje balsavai", - "notification.quoted_update": "{name} redagavo jūsų cituotą įrašą", + "notification.quoted_update": "{name} redagavo jūsų pakomentuotą įrašą", "notification.reblog": "{name} dalinosi tavo įrašu", "notification.reblog.name_and_others_with_link": "{name} ir {count, plural,one {dar kažkas} few {# kiti} other {# kitų}} pasidalino tavo įrašu", "notification.relationships_severance_event": "Prarasti sąryšiai su {name}", @@ -671,7 +708,7 @@ "notifications.column_settings.mention": "Paminėjimai:", "notifications.column_settings.poll": "Balsavimo rezultatai:", "notifications.column_settings.push": "Tiesioginiai pranešimai", - "notifications.column_settings.quote": "Paminėjimai:", + "notifications.column_settings.quote": "Komentarai:", "notifications.column_settings.reblog": "Pasidalinimai:", "notifications.column_settings.show": "Rodyti stulpelyje", "notifications.column_settings.sound": "Paleisti garsą", @@ -747,18 +784,19 @@ "privacy.private.short": "Sekėjai", "privacy.public.long": "Bet kas iš Mastodon ir ne Mastodon", "privacy.public.short": "Vieša", - "privacy.quote.disabled": "{visibility}, paminėjimai išjungti", - "privacy.quote.limited": "{visibility}, paminėjimai apriboti", + "privacy.quote.anyone": "{visibility}, komentarai galimi", + "privacy.quote.disabled": "{visibility}, komentavimas išjungtas", + "privacy.quote.limited": "{visibility}, komentavimas apribotas", "privacy.unlisted.additional": "Tai veikia lygiai taip pat, kaip ir vieša, tik įrašas nebus rodomas tiesioginiuose srautuose, grotažymėse, naršyme ar Mastodon paieškoje, net jei esi įtraukęs (-usi) visą paskyrą.", "privacy.unlisted.long": "Paslėptas nuo „Mastodon“ paieškos rezultatų, tendencijų ir viešų įrašų sienų", "privacy.unlisted.short": "Tyliai vieša", "privacy_policy.last_updated": "Paskutinį kartą atnaujinta {date}", "privacy_policy.title": "Privatumo politika", - "quote_error.edit": "Paminėjimai negali būti pridedami, kai keičiamas įrašas.", - "quote_error.poll": "Cituoti apklausose negalima.", - "quote_error.private_mentions": "Cituoti privačius paminėjus nėra leidžiama.", - "quote_error.quote": "Leidžiama pateikti tik vieną citatą vienu metu.", - "quote_error.unauthorized": "Jums neleidžiama cituoti šio įrašo.", + "quote_error.edit": "Komentuoti negalima, kai keičiamas įrašas.", + "quote_error.poll": "Komentuoti apklausose negalima.", + "quote_error.private_mentions": "Komentuoti privačius paminėjimus nėra leidžiama.", + "quote_error.quote": "Leidžiama pateikti tik vieną komentarą vienu metu.", + "quote_error.unauthorized": "Jums neleidžiama komentuoti šio įrašo.", "quote_error.upload": "Cituoti ir pridėti papildomas bylas negalima.", "recommended": "Rekomenduojama", "refresh": "Atnaujinti", @@ -777,7 +815,7 @@ "relative_time.today": "šiandien", "remove_quote_hint.button_label": "Supratau", "remove_quote_hint.message": "Tai galite padaryti iš {icon} parinkčių meniu.", - "remove_quote_hint.title": "Norite pašalinti savo citatą?", + "remove_quote_hint.title": "Norite pašalinti savo komentarą?", "reply_indicator.attachments": "{count, plural, one {# priedas} few {# priedai} many {# priedo} other {# priedų}}", "reply_indicator.cancel": "Atšaukti", "reply_indicator.poll": "Apklausa", @@ -869,13 +907,13 @@ "status.admin_account": "Atidaryti prižiūrėjimo sąsają @{name}", "status.admin_domain": "Atidaryti prižiūrėjimo sąsają {domain}", "status.admin_status": "Atidaryti šį įrašą prižiūrėjimo sąsajoje", - "status.all_disabled": "Įrašo dalinimaisi ir paminėjimai išjungti", + "status.all_disabled": "Įrašo pasidalijimai ir komentavimai išjungti", "status.block": "Blokuoti @{name}", "status.bookmark": "Žymė", "status.cancel_reblog_private": "Nesidalinti", - "status.cannot_quote": "Jums neleidžiama paminėti šio įrašo", + "status.cannot_quote": "Jums neleidžiama komentuoti šio įrašo", "status.cannot_reblog": "Šis įrašas negali būti pakeltas.", - "status.contains_quote": "Turi citatą", + "status.contains_quote": "Turi komentarą", "status.context.loading": "Įkeliama daugiau atsakymų", "status.context.loading_error": "Nepavyko įkelti naujų atsakymų", "status.context.loading_success": "Įkelti nauji atsakymai", @@ -908,8 +946,8 @@ "status.mute_conversation": "Nutildyti pokalbį", "status.open": "Išskleisti šį įrašą", "status.pin": "Prisegti prie profilio", - "status.quote": "Paminėjimai", - "status.quote.cancel": "Atšaukti paminėjimą", + "status.quote": "Komentuoti", + "status.quote.cancel": "Atšaukti komentarą", "status.quote_error.blocked_account_hint.title": "Šis įrašas yra paslėptas, nes jūs esate užblokavę @{name}.", "status.quote_error.blocked_domain_hint.title": "Šis įrašas yra paslėptas, nes jūs užblokavote {domain}.", "status.quote_error.filtered": "Paslėpta dėl vieno iš jūsų filtrų", @@ -925,14 +963,14 @@ "status.quote_noun": "Paminėjimas", "status.quote_policy_change": "Keisti, kas gali cituoti", "status.quote_post_author": "Paminėjo įrašą iš @{name}", - "status.quote_private": "Privačių įrašų negalima cituoti", - "status.quotes.empty": "Šio įrašo dar niekas nepaminėjo. Kai kas nors tai padarys, jie bus rodomi čia.", - "status.quotes.local_other_disclaimer": "Autoriaus atmesti įrašo paminėjimai nebus rodomi.", - "status.quotes.remote_other_disclaimer": "Čia bus rodoma tik paminėjimai iš {domain}. Autoriaus atmesti įrašo paminėjimai nebus rodomi.", - "status.quotes_count": "{count, plural, one {{counter} paminėjimas} few {{counter} paminėjimai} many {{counter} paminėjimai} other {{counter} paminėjimai}}", + "status.quote_private": "Privačių įrašų negalima komentuoti", + "status.quotes.empty": "Šio įrašo dar niekas nepakomentavo. Kai kas nors tai padarys, jie bus rodomi čia.", + "status.quotes.local_other_disclaimer": "Autoriaus atmesti įrašo komentavimai nebus rodomi.", + "status.quotes.remote_other_disclaimer": "Čia bus rodoma tik komentavimai iš {domain}. Autoriaus atmesti įrašo komentavimai nebus rodomi.", + "status.quotes_count": "{count, plural, one {{counter} komentaras} few {{counter} komentarai} many {{counter} komentarų} other {{counter} komentarai}}", "status.read_more": "Skaityti daugiau", "status.reblog": "Dalintis", - "status.reblog_or_quote": "Dalintis arba cituoti", + "status.reblog_or_quote": "Dalintis arba komentuoti", "status.reblog_private": "Vėl pasidalinkite su savo sekėjais", "status.reblogged_by": "{name} pasidalino", "status.reblogs.empty": "Šiuo įrašu dar niekas nesidalino. Kai kas nors tai padarys, jie bus rodomi čia.", @@ -946,8 +984,8 @@ "status.reply": "Atsakyti", "status.replyAll": "Atsakyti į giją", "status.report": "Pranešti apie @{name}", - "status.request_quote": "Citavimo sutikimas", - "status.revoke_quote": "Pašalinti mano įrašo citavimą iš @{name}’s įrašo", + "status.request_quote": "Prašymas pakomentuoti", + "status.revoke_quote": "Pašalinti mano įrašą iš @{name}’s įrašo", "status.sensitive_warning": "Jautrus turinys", "status.share": "Bendrinti", "status.show_less_all": "Rodyti mažiau visiems", @@ -985,7 +1023,7 @@ "upload_button.label": "Pridėti vaizdų, vaizdo įrašą arba garso failą", "upload_error.limit": "Viršyta failo įkėlimo riba.", "upload_error.poll": "Failų įkėlimas neleidžiamas su apklausomis.", - "upload_error.quote": "Paminint įrašą bylos įkėlimas negalimas.", + "upload_error.quote": "Komentuojant įrašą failo įkėlimas negalimas.", "upload_form.drag_and_drop.instructions": "Kad paimtum medijos priedą, paspausk tarpo arba įvedimo klavišą. Tempant naudok rodyklių klavišus, kad perkeltum medijos priedą bet kuria kryptimi. Dar kartą paspausk tarpo arba įvedimo klavišą, kad nuleistum medijos priedą naujoje vietoje, arba paspausk grįžimo klavišą, kad atšauktum.", "upload_form.drag_and_drop.on_drag_cancel": "Nutempimas buvo atšauktas. Medijos priedas {item} buvo nuleistas.", "upload_form.drag_and_drop.on_drag_end": "Medijos priedas {item} buvo nuleistas.", @@ -1010,18 +1048,18 @@ "video.volume_down": "Patildyti", "video.volume_up": "Pagarsinti", "visibility_modal.button_title": "Nustatyti matomumą", - "visibility_modal.direct_quote_warning.text": "Jei išsaugosite dabartinius nustatymus, įterpta citata bus konvertuota į nuorodą.", - "visibility_modal.direct_quote_warning.title": "Cituojami įrašai negali būti įterpiami į privačius paminėjimus", + "visibility_modal.direct_quote_warning.text": "Jei išsaugosite dabartinius nustatymus, įterptas (embeded) komentaras bus konvertuotas į nuorodą.", + "visibility_modal.direct_quote_warning.title": "Komentarai negali būti įterpiami į privačius paminėjimus", "visibility_modal.header": "Matomumas ir sąveika", - "visibility_modal.helper.direct_quoting": "Privatūs paminėjimai su žyma @, parašyti platformoje „Mastodon“, negali būti cituojami kitų.", + "visibility_modal.helper.direct_quoting": "Privatūs paminėjimai su žyma @, parašyti platformoje „Mastodon“, negali būti komentuojami kitų.", "visibility_modal.helper.privacy_editing": "Matomumo nustatymai negali būti keičiami po to, kai įrašas yra paskelbtas.", - "visibility_modal.helper.privacy_private_self_quote": "Privačių įrašų paminėjimai negali būti skelbiami viešai.", - "visibility_modal.helper.private_quoting": "Tik sekėjams skirti įrašai, parašyti platformoje „Mastodon“, negali būti cituojami kitų.", - "visibility_modal.helper.unlisted_quoting": "Kai žmonės jus cituos, jų įrašai taip pat bus paslėpti iš populiariausių naujienų srauto.", + "visibility_modal.helper.privacy_private_self_quote": "Prie privataus įrašo tavo pridėti komentarai negali būti skelbiami viešai.", + "visibility_modal.helper.private_quoting": "Tik sekėjams skirti įrašai, parašyti platformoje „Mastodon“, negali būti komentuojami kitų.", + "visibility_modal.helper.unlisted_quoting": "Kai žmonės jus komentuoja, jų įrašai taip pat bus paslėpti iš populiariausių naujienų srauto.", "visibility_modal.instructions": "Kontroliuokite, kas gali bendrauti su šiuo įrašu. Taip pat galite taikyti nustatymus visiems būsimiems įrašams, pereidami į Preferences > Posting defaults.", "visibility_modal.privacy_label": "Matomumas", "visibility_modal.quote_followers": "Tik sekėjai", - "visibility_modal.quote_label": "Kas gali cituoti", + "visibility_modal.quote_label": "Kas gali komentuoti", "visibility_modal.quote_nobody": "Tik aš", "visibility_modal.quote_public": "Visi", "visibility_modal.save": "Išsaugoti" diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index ad9e19c2af..6f5f9043b1 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -17,8 +17,15 @@ "account.activity": "Veprimtari", "account.add_note": "Shtoni një shënim personal", "account.add_or_remove_from_list": "Shtoni ose Hiqni prej listash", + "account.badges.admin": "Përgjegjës", + "account.badges.blocked": "E bllokuar", "account.badges.bot": "Robot", + "account.badges.domain_blocked": "Përkatësi e bllokuar", + "account.badges.follows_you": "Ju ndjek", "account.badges.group": "Grup", + "account.badges.muted": "E heshtuar", + "account.badges.mutuals": "Ndiqni njëri-tjetrin", + "account.badges.requested_to_follow": "Kërkoi t’ju ndjekë", "account.block": "Blloko @{name}", "account.block_domain": "Blloko përkatësinë {domain}", "account.block_short": "Bllokoje", @@ -219,7 +226,7 @@ "collections.description_length_hint": "Kufi prej 100 shenjash", "collections.error_loading_collections": "Pati një gabim teksa provohej të ngarkoheshin koleksionet tuaj.", "collections.mark_as_sensitive": "Vëri shenjë si rezervat", - "collections.mark_as_sensitive_hint": "Bën fshehejn e përshkrimit të koleksionit dhe llogarive prapa një sinjalizimi lënde. Emri i koleksionit do të jetë ende i dukshëm.", + "collections.mark_as_sensitive_hint": "Bën fshehjen e përshkrimit të koleksionit dhe llogarive prapa një sinjalizimi lënde. Emri i koleksionit do të jetë ende i dukshëm.", "collections.name_length_hint": "Kufi prej 100 shenjash", "collections.no_collections_yet": "Ende pa koleksione.", "collections.topic_hint": "Shtoni një hashtag që ndihmon të tjerët të kuptojnë temën kryesore të këtij koleksion.", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index f3beb3f9d9..68e0b1dca0 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -17,8 +17,15 @@ "account.activity": "Aktivite", "account.add_note": "Kişisel bir not ekle", "account.add_or_remove_from_list": "Listelere ekle veya kaldır", + "account.badges.admin": "Yönetici", + "account.badges.blocked": "Engellendi", "account.badges.bot": "Bot", + "account.badges.domain_blocked": "Engellenen alan adı", + "account.badges.follows_you": "Seni takip ediyor", "account.badges.group": "Grup", + "account.badges.muted": "Sessize Alındı", + "account.badges.mutuals": "Birbirinizi takip ediyorsunuz", + "account.badges.requested_to_follow": "Size takip isteği gönderdi", "account.block": "@{name} adlı kişiyi engelle", "account.block_domain": "{domain} alan adını engelle", "account.block_short": "Engelle", @@ -212,21 +219,31 @@ "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", + "collections.collection_description": "Açıklama", + "collections.collection_name": "Ad", + "collections.collection_topic": "Konu", "collections.create_a_collection_hint": "En sevdiğiniz hesapları başkalarına önermek veya paylaşmak için bir koleksiyon oluşturun.", "collections.create_collection": "Koleksiyon oluştur", "collections.delete_collection": "Koleksiyonu sil", + "collections.description_length_hint": "100 karakterle sınırlı", "collections.error_loading_collections": "Koleksiyonlarınızı yüklemeye çalışırken bir hata oluştu.", + "collections.mark_as_sensitive": "Hassas olarak işaretle", + "collections.mark_as_sensitive_hint": "Koleksiyonun açıklamasını ve hesaplarını içerik uyarısının arkasında gizler. Koleksiyon adı hala görünür olacaktır.", + "collections.name_length_hint": "100 karakterle sınırlı", "collections.no_collections_yet": "Henüz hiçbir koleksiyon yok.", + "collections.topic_hint": "Bu koleksiyonun ana konusunu başkalarının anlamasına yardımcı olacak bir etiket ekleyin.", "collections.view_collection": "Koleksiyonu görüntüle", "column.about": "Hakkında", "column.blocks": "Engellenen kullanıcılar", "column.bookmarks": "Yer İşaretleri", "column.collections": "Koleksiyonlarım", "column.community": "Yerel ağ akışı", + "column.create_collection": "Koleksiyon oluştur", "column.create_list": "Liste oluştur", "column.direct": "Özel bahsetmeler", "column.directory": "Profillere göz at", "column.domain_blocks": "Engellenen alan adları", + "column.edit_collection": "Koleksiyonu düzenle", "column.edit_list": "Listeyi düzenle", "column.favourites": "Gözdelerin", "column.firehose": "Anlık Akışlar", @@ -281,6 +298,9 @@ "confirmations.delete.confirm": "Sil", "confirmations.delete.message": "Bu tootu silmek istediğinden emin misin?", "confirmations.delete.title": "Gönderiyi sil?", + "confirmations.delete_collection.confirm": "Sil", + "confirmations.delete_collection.message": "Bu işlem geri alınamaz.", + "confirmations.delete_collection.title": "\"{name}\" silinsin mi?", "confirmations.delete_list.confirm": "Sil", "confirmations.delete_list.message": "Bu listeyi kalıcı olarak silmek istediğinden emin misin?", "confirmations.delete_list.title": "Listeyi sil?", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 86f78674c8..4411ca10c5 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -17,8 +17,15 @@ "account.activity": "Hoạt động", "account.add_note": "Thêm ghi chú", "account.add_or_remove_from_list": "Sửa danh sách", + "account.badges.admin": "Quản trị viên", + "account.badges.blocked": "Đã chặn", "account.badges.bot": "Bot", + "account.badges.domain_blocked": "Máy chủ đã chặn", + "account.badges.follows_you": "Theo dõi bạn", "account.badges.group": "Nhóm", + "account.badges.muted": "Đã phớt lờ", + "account.badges.mutuals": "Theo dõi nhau", + "account.badges.requested_to_follow": "Yêu cầu theo dõi bạn", "account.block": "Chặn @{name}", "account.block_domain": "Chặn mọi thứ từ {domain}", "account.block_short": "Chặn", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index ca39da4c2c..88ffcfcbc7 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -17,8 +17,15 @@ "account.activity": "活动", "account.add_note": "添加个人备注", "account.add_or_remove_from_list": "从列表中添加或移除", + "account.badges.admin": "管理员", + "account.badges.blocked": "已屏蔽", "account.badges.bot": "机器人", + "account.badges.domain_blocked": "已屏蔽域名", + "account.badges.follows_you": "关注了你", "account.badges.group": "群组", + "account.badges.muted": "已隐藏", + "account.badges.mutuals": "你们互相关注", + "account.badges.requested_to_follow": "向您发送了关注请求", "account.block": "屏蔽 @{name}", "account.block_domain": "屏蔽 {domain} 实例", "account.block_short": "屏蔽", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 2644956ced..5684636b13 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -17,8 +17,15 @@ "account.activity": "活動", "account.add_note": "新增個人備註", "account.add_or_remove_from_list": "自列表中新增或移除", + "account.badges.admin": "管理員", + "account.badges.blocked": "已封鎖", "account.badges.bot": "機器人", + "account.badges.domain_blocked": "已封鎖網域", + "account.badges.follows_you": "已跟隨您", "account.badges.group": "群組", + "account.badges.muted": "已靜音", + "account.badges.mutuals": "跟隨彼此", + "account.badges.requested_to_follow": "要求跟隨您", "account.block": "封鎖 @{name}", "account.block_domain": "封鎖來自 {domain} 網域之所有內容", "account.block_short": "封鎖", diff --git a/app/javascript/mastodon/models/dropdown_menu.ts b/app/javascript/mastodon/models/dropdown_menu.ts index 01da286936..a5decf607b 100644 --- a/app/javascript/mastodon/models/dropdown_menu.ts +++ b/app/javascript/mastodon/models/dropdown_menu.ts @@ -6,6 +6,7 @@ interface BaseMenuItem { text: string; description?: string; icon?: IconProp; + iconId?: string; highlighted?: boolean; disabled?: boolean; dangerous?: boolean; diff --git a/app/javascript/mastodon/utils/checks.ts b/app/javascript/mastodon/utils/checks.ts new file mode 100644 index 0000000000..8b05ac24a7 --- /dev/null +++ b/app/javascript/mastodon/utils/checks.ts @@ -0,0 +1,11 @@ +export function isValidUrl( + url: string, + allowedProtocols = ['https:'], +): boolean { + try { + const parsedUrl = new URL(url); + return allowedProtocols.includes(parsedUrl.protocol); + } catch { + return false; + } +} diff --git a/app/javascript/material-icons/400-24px/link_2-fill.svg b/app/javascript/material-icons/400-24px/link_2-fill.svg new file mode 100644 index 0000000000..65d3084a2f --- /dev/null +++ b/app/javascript/material-icons/400-24px/link_2-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/link_2.svg b/app/javascript/material-icons/400-24px/link_2.svg new file mode 100644 index 0000000000..65d3084a2f --- /dev/null +++ b/app/javascript/material-icons/400-24px/link_2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 4bab648961..93d77e2905 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -8248,7 +8248,6 @@ noscript { } .account__header { - overflow: hidden; container: account-header / inline-size; &.inactive { diff --git a/config/locales/de.yml b/config/locales/de.yml index 7db19f067a..fe4c6dfd7c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -994,7 +994,7 @@ de: trendable: Trendfähig unreviewed: Ungeprüft usable: Verwendbar - name: Name + name: Hashtag newest: Neueste oldest: Älteste open: Öffentlich anzeigen diff --git a/docker-compose.yml b/docker-compose.yml index bcda267f57..cf0e198222 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/glitch-soc/mastodon:v4.5.5 + image: ghcr.io/glitch-soc/mastodon:v4.5.6 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.5 + image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.6 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/glitch-soc/mastodon:v4.5.5 + image: ghcr.io/glitch-soc/mastodon:v4.5.6 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 947106aeae..5a64867efd 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,7 +17,7 @@ module Mastodon end def default_prerelease - 'alpha.3' + 'alpha.4' end def prerelease