Merge pull request #3409 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to 3df8fb8fe9d33c7fbe51aa0a418b8b9ca46bb61b
This commit is contained in:
Claire 2026-02-18 20:15:48 +01:00 committed by GitHub
commit 4977fb1570
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
185 changed files with 2041 additions and 1041 deletions

View File

@ -154,9 +154,15 @@
groupName: 'opentelemetry-ruby (non-major)', groupName: 'opentelemetry-ruby (non-major)',
}, },
{ {
// Group Playwright Ruby & JS deps in the same PR, as they need to be in sync // The ruby portion of the Playwright group
matchManagers: ['bundler', 'npm'], matchManagers: ['bundler'],
matchPackageNames: ['playwright-ruby-client', 'playwright'], matchPackageNames: ['playwright-ruby-client'],
groupName: 'Playwright',
},
{
// The node portion of the Playwright group
matchManagers: ['npm'],
matchPackageNames: ['playwright'],
groupName: 'Playwright', groupName: 'Playwright',
}, },
// Add labels depending on package manager // Add labels depending on package manager

View File

@ -4,3 +4,6 @@ Layout/FirstHashElementIndentation:
Layout/LineLength: Layout/LineLength:
Max: 300 # Default of 120 causes a duplicate entry in generated todo file Max: 300 # Default of 120 causes a duplicate entry in generated todo file
Layout/MultilineMethodCallIndentation:
EnforcedStyle: indented

View File

@ -470,7 +470,7 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.5) nio4r (2.7.5)
nokogiri (1.19.0) nokogiri (1.19.1)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.15) oj (3.16.15)
@ -631,7 +631,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.4) rack (3.2.5)
rack-attack (6.8.0) rack-attack (6.8.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-cors (3.0.0) rack-cors (3.0.0)
@ -741,7 +741,7 @@ GEM
rspec-mocks (3.13.7) rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (8.0.2) rspec-rails (8.0.3)
actionpack (>= 7.2) actionpack (>= 7.2)
activesupport (>= 7.2) activesupport (>= 7.2)
railties (>= 7.2) railties (>= 7.2)
@ -755,7 +755,7 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9) sidekiq (>= 5, < 9)
rspec-support (3.13.7) rspec-support (3.13.7)
rubocop (1.84.0) rubocop (1.84.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)

View File

@ -11,14 +11,14 @@ export interface ApiCollectionJSON {
account_id: string; account_id: string;
id: string; id: string;
uri: string; uri: string | null;
local: boolean; local: boolean;
item_count: number; item_count: number;
name: string; name: string;
description: string; description: string;
tag?: ApiTagJSON; tag: ApiTagJSON | null;
language: string; language: string | null;
sensitive: boolean; sensitive: boolean;
discoverable: boolean; discoverable: boolean;

View File

@ -298,7 +298,7 @@ export const Account: React.FC<AccountProps> = ({
> >
<div className='account__info-wrapper'> <div className='account__info-wrapper'>
<Permalink <Permalink
className='account__display-name' className='account__display-name focusable'
title={account?.acct} title={account?.acct}
href={account?.url} href={account?.url}
to={`/@${account?.acct}`} to={`/@${account?.acct}`}

View File

@ -53,7 +53,7 @@ export const Avatar: React.FC<Props> = ({
}, [setError]); }, [setError]);
const avatar = ( const avatar = (
<div <span
className={classNames(className, 'account__avatar', { className={classNames(className, 'account__avatar', {
'account__avatar--inline': inline, 'account__avatar--inline': inline,
'account__avatar--loading': loading, 'account__avatar--loading': loading,
@ -68,14 +68,14 @@ export const Avatar: React.FC<Props> = ({
)} )}
{counter && ( {counter && (
<div <span
className='account__avatar__counter' className='account__avatar__counter'
style={{ borderColor: counterBorderColor }} style={{ borderColor: counterBorderColor }}
> >
{counter} {counter}
</div> </span>
)} )}
</div> </span>
); );
if (withLink) { if (withLink) {

View File

@ -1,6 +1,8 @@
import { forwardRef, useRef, useImperativeHandle } from 'react'; import { forwardRef, useRef, useImperativeHandle } from 'react';
import type { Ref } from 'react'; import type { Ref } from 'react';
import classNames from 'classnames';
import { scrollTop } from 'flavours/glitch/scroll'; import { scrollTop } from 'flavours/glitch/scroll';
export interface ColumnRef { export interface ColumnRef {
@ -12,10 +14,11 @@ interface ColumnProps {
children?: React.ReactNode; children?: React.ReactNode;
label?: string; label?: string;
bindToDocument?: boolean; bindToDocument?: boolean;
className?: string;
} }
export const Column = forwardRef<ColumnRef, ColumnProps>( export const Column = forwardRef<ColumnRef, ColumnProps>(
({ children, label, bindToDocument }, ref: Ref<ColumnRef>) => { ({ children, label, bindToDocument, className }, ref: Ref<ColumnRef>) => {
const nodeRef = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@ -39,7 +42,12 @@ export const Column = forwardRef<ColumnRef, ColumnProps>(
})); }));
return ( return (
<div role='region' aria-label={label} className='column' ref={nodeRef}> <div
role='region'
aria-label={label}
className={classNames('column', className)}
ref={nodeRef}
>
{children} {children}
</div> </div>
); );

View File

@ -73,6 +73,7 @@ export interface Props {
iconComponent?: IconProp; iconComponent?: IconProp;
active?: boolean; active?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
className?: string;
pinned?: boolean; pinned?: boolean;
multiColumn?: boolean; multiColumn?: boolean;
extraButton?: React.ReactNode; extraButton?: React.ReactNode;
@ -91,6 +92,7 @@ export const ColumnHeader: React.FC<Props> = ({
iconComponent, iconComponent,
active, active,
children, children,
className,
pinned, pinned,
multiColumn, multiColumn,
extraButton, extraButton,
@ -141,7 +143,7 @@ export const ColumnHeader: React.FC<Props> = ({
onPin?.(); onPin?.();
}, [history, pinned, onPin]); }, [history, pinned, onPin]);
const wrapperClassName = classNames('column-header__wrapper', { const wrapperClassName = classNames('column-header__wrapper', className, {
active, active,
}); });
@ -256,7 +258,8 @@ export const ColumnHeader: React.FC<Props> = ({
} }
const hasIcon = icon && iconComponent; const hasIcon = icon && iconComponent;
const hasTitle = hasIcon && title; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasTitle = (hasIcon || backButton) && title;
const component = ( const component = (
<div className={wrapperClassName}> <div className={wrapperClassName}>
@ -270,7 +273,7 @@ export const ColumnHeader: React.FC<Props> = ({
className='column-header__title' className='column-header__title'
type='button' type='button'
> >
{!backButton && ( {!backButton && hasIcon && (
<Icon <Icon
id={icon} id={icon}
icon={iconComponent} icon={iconComponent}

View File

@ -311,6 +311,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
status?: ImmutableMap<string, unknown>; status?: ImmutableMap<string, unknown>;
needsStatusRefresh?: boolean; needsStatusRefresh?: boolean;
forceDropdown?: boolean; forceDropdown?: boolean;
className?: string;
renderItem?: RenderItemFn<Item>; renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>; renderHeader?: RenderHeaderFn<Item>;
onOpen?: // Must use a union type for the full function as a union with void is not allowed. onOpen?: // Must use a union type for the full function as a union with void is not allowed.
@ -335,6 +336,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
status, status,
needsStatusRefresh, needsStatusRefresh,
forceDropdown = false, forceDropdown = false,
className,
renderItem, renderItem,
renderHeader, renderHeader,
onOpen, onOpen,
@ -434,6 +436,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
modalProps: { modalProps: {
actions: items, actions: items,
onClick: handleItemClick, onClick: handleItemClick,
className,
}, },
}), }),
); );
@ -462,6 +465,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
handleClose, handleClose,
statusId, statusId,
needsStatusRefresh, needsStatusRefresh,
className,
], ],
); );
@ -515,7 +519,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
popperConfig={popperConfig} popperConfig={popperConfig}
> >
{({ props, arrowProps, placement }) => ( {({ props, arrowProps, placement }) => (
<div {...props} id={menuId}> <div {...props} className={className} id={menuId}>
<div className={`dropdown-animation dropdown-menu ${placement}`}> <div className={`dropdown-animation dropdown-menu ${placement}`}>
<div <div
className={`dropdown-menu__arrow ${placement}`} className={`dropdown-menu__arrow ${placement}`}

View File

@ -3,8 +3,10 @@ import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { useIdentity } from '@/flavours/glitch/identity_context'; import { useIdentity } from '@/flavours/glitch/identity_context';
import { isClientFeatureEnabled } from '@/flavours/glitch/utils/environment';
import { import {
fetchRelationships, fetchRelationships,
followAccount, followAccount,
@ -158,14 +160,24 @@ export const FollowButton: React.FC<{
} }
if (accountId === me) { if (accountId === me) {
const buttonClasses = classNames(className, 'button button-secondary', {
'button--compact': compact,
});
if (isClientFeatureEnabled('profile_editing')) {
return (
<Link to='/profile/edit' className={buttonClasses}>
{label}
</Link>
);
}
return ( return (
<a <a
href='/settings/profile' href='/settings/profile'
target='_blank' target='_blank'
rel='noopener' rel='noopener'
className={classNames(className, 'button button-secondary', { className={buttonClasses}
'button--compact': compact,
})}
> >
{label} {label}
</a> </a>

View File

@ -0,0 +1,53 @@
import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { Column } from '@/flavours/glitch/components/column';
import { ColumnHeader } from '@/flavours/glitch/components/column_header';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import classes from './styles.module.scss';
export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
if (!accountId) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
if (!account) {
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<LoadingIndicator />
</Column>
);
}
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader
title={intl.formatMessage({
id: 'account_edit.column_title',
defaultMessage: 'Edit Profile',
})}
className={classes.header}
showBackButton
extraButton={
<Link to={`/@${account.acct}`} className='button'>
<FormattedMessage
id='account_edit.column_button'
defaultMessage='Done'
/>
</Link>
}
/>
</Column>
);
};

View File

@ -0,0 +1,26 @@
.column {
border: 1px solid var(--color-border-primary);
border-top-width: 0;
}
.header {
:global(.column-header__buttons) {
align-items: center;
padding-inline-end: 16px;
height: auto;
}
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 24px 24px 12px;
> h1 {
flex-grow: 1;
font-weight: 600;
font-size: 15px;
}
}

View File

@ -214,7 +214,10 @@ export const AccountHeader: React.FC<{
<> <>
<AccountBio <AccountBio
accountId={accountId} accountId={accountId}
className='account__header__content' className={classNames(
'account__header__content',
isRedesign && redesignClasses.bio,
)}
/> />
<AccountHeaderFields accountId={accountId} /> <AccountHeaderFields accountId={accountId} />
</> </>

View File

@ -14,8 +14,10 @@ import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_dat
import { Icon } from '@/flavours/glitch/components/icon'; import { Icon } from '@/flavours/glitch/components/icon';
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link'; import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import type { Account } from '@/flavours/glitch/models/account'; import type {
import { isValidUrl } from '@/flavours/glitch/utils/checks'; Account,
AccountFieldShape,
} from '@/flavours/glitch/models/account';
import type { OnElementHandler } from '@/flavours/glitch/utils/html'; import type { OnElementHandler } from '@/flavours/glitch/utils/html';
import IconVerified from '@/images/icons/icon_verified.svg?react'; import IconVerified from '@/images/icons/icon_verified.svg?react';
@ -76,8 +78,8 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
[account.emojis], [account.emojis],
); );
const textHasCustomEmoji = useCallback( const textHasCustomEmoji = useCallback(
(text: string) => { (text?: string | null) => {
if (!emojis) { if (!emojis || !text) {
return false; return false;
} }
for (const emoji of Object.keys(emojis)) { for (const emoji of Object.keys(emojis)) {
@ -92,22 +94,57 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
const htmlHandlers = useElementHandledLink({ const htmlHandlers = useElementHandledLink({
hashtagAccountId: account.id, hashtagAccountId: account.id,
}); });
const intl = useIntl();
if (account.fields.isEmpty()) {
return null;
}
return ( return (
<CustomEmojiProvider emojis={emojis}> <CustomEmojiProvider emojis={emojis}>
<dl className={classes.fieldList}> <dl className={classes.fieldList}>
{account.fields.map( {account.fields.map((field, key) => (
( <FieldRow
{ name, name_emojified, value_emojified, value_plain, verified_at },
key,
) => (
<div
key={key} key={key}
{...field.toJSON()}
htmlHandlers={htmlHandlers}
textHasCustomEmoji={textHasCustomEmoji}
/>
))}
</dl>
</CustomEmojiProvider>
);
};
const FieldRow: FC<
{
textHasCustomEmoji: (text?: string | null) => boolean;
htmlHandlers: ReturnType<typeof useElementHandledLink>;
} & AccountFieldShape
> = ({
textHasCustomEmoji,
htmlHandlers,
name,
name_emojified,
value_emojified,
value_plain,
verified_at,
}) => {
const intl = useIntl();
const [showAll, setShowAll] = useState(false);
const handleClick = useCallback(() => {
setShowAll((prev) => !prev);
}, []);
return (
/* eslint-disable -- This method of showing field contents is not very accessible, but it's what we've got for now */
<div
className={classNames( className={classNames(
classes.fieldRow, classes.fieldRow,
verified_at && classes.fieldVerified, verified_at && classes.fieldVerified,
showAll && classes.fieldShowAll,
)} )}
onClick={handleClick}
/* eslint-enable */
> >
<FieldHTML <FieldHTML
as='dt' as='dt'
@ -118,14 +155,16 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
className='translate' className='translate'
{...htmlHandlers} {...htmlHandlers}
/> />
<dd>
<FieldHTML <FieldHTML
as='dd' as='span'
text={value_plain ?? ''} text={value_plain ?? ''}
textEmojified={value_emojified} textEmojified={value_emojified}
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')} textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
titleLength={120} titleLength={120}
{...htmlHandlers} {...htmlHandlers}
/> />
{verified_at && ( {verified_at && (
<Icon <Icon
id='verified' id='verified'
@ -137,17 +176,14 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
noFill noFill
/> />
)} )}
</dd>
</div> </div>
),
)}
</dl>
</CustomEmojiProvider>
); );
}; };
const FieldHTML: FC< const FieldHTML: FC<
{ {
as: 'dd' | 'dt'; as?: 'span' | 'dt';
text: string; text: string;
textEmojified: string; textEmojified: string;
textHasCustomEmoji: boolean; textHasCustomEmoji: boolean;
@ -164,11 +200,6 @@ const FieldHTML: FC<
onElement, onElement,
...props ...props
}) => { }) => {
const [showAll, setShowAll] = useState(false);
const handleClick = useCallback(() => {
setShowAll((prev) => !prev);
}, []);
const handleElement: OnElementHandler = useCallback( const handleElement: OnElementHandler = useCallback(
(element, props, children, extra) => { (element, props, children, extra) => {
if (element instanceof HTMLAnchorElement) { if (element instanceof HTMLAnchorElement) {
@ -186,17 +217,13 @@ const FieldHTML: FC<
}, },
[onElement, textHasCustomEmoji], [onElement, textHasCustomEmoji],
); );
return ( return (
<EmojiHTML <EmojiHTML
as={as} as={as}
htmlString={textEmojified} htmlString={textEmojified}
title={showTitleOnLength(text, titleLength)} title={showTitleOnLength(text, titleLength)}
className={classNames( className={className}
className,
text && isValidUrl(text) && classes.fieldLink,
showAll && classes.fieldShowAll,
)}
onClick={handleClick}
onElement={handleElement} onElement={handleElement}
{...props} {...props}
/> />

View File

@ -45,6 +45,8 @@ import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common'; import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl(); const intl = useIntl();
const { signedIn, permissions } = useIdentity(); const { signedIn, permissions } = useIdentity();
@ -89,6 +91,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
items={menuItems} items={menuItems}
icon='ellipsis-v' icon='ellipsis-v'
iconComponent={MoreHorizIcon} iconComponent={MoreHorizIcon}
className={classes.buttonMenu}
/> />
); );
}; };

View File

@ -24,11 +24,10 @@
.name { .name {
flex-grow: 1; flex-grow: 1;
font-size: 22px;
white-space: initial;
line-height: normal;
> h1 { > h1 {
font-size: 22px;
line-height: normal;
white-space: initial; white-space: initial;
} }
} }
@ -149,11 +148,33 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
border-top: 1px solid var(--color-border-primary); border-top: 1px solid var(--color-border-primary);
} }
.buttonMenu {
// Override the modal for mobile.
&:global(.actions-modal) {
max-height: none;
}
li :global(.icon) {
width: 20px;
height: 20px;
}
}
.bio {
font-size: 15px;
}
.badge { .badge {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
border: none; border: none;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-weight: 500; font-weight: 500;
padding: 4px;
font-size: 13px;
:global(.account__header__badges) > & {
line-height: 1;
}
> span { > span {
font-weight: unset; font-weight: unset;
@ -194,12 +215,13 @@ svg.badgeIcon {
.fieldList { .fieldList {
display: grid; display: grid;
grid-template-columns: 160px 1fr min-content; grid-template-columns: 160px 1fr;
column-gap: 12px; column-gap: 12px;
margin: 4px 0 16px; margin: 16px 0;
border-top: 0.5px solid var(--color-border-primary);
@container (width < 420px) { @container (width < 420px) {
grid-template-columns: 100px 1fr min-content; grid-template-columns: 100px 1fr;
} }
} }
@ -208,11 +230,10 @@ svg.badgeIcon {
grid-column: 1 / -1; grid-column: 1 / -1;
align-items: start; align-items: start;
grid-template-columns: subgrid; grid-template-columns: subgrid;
padding: 0 4px; padding: 8px;
border-bottom: 0.5px solid var(--color-border-primary);
> :is(dt, dd) { > :is(dt, dd) {
margin: 8px 0;
&:not(.fieldShowAll) { &:not(.fieldShowAll) {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
@ -227,43 +248,34 @@ svg.badgeIcon {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
&:not(.fieldVerified) > dd { > dd {
grid-column: span 2; display: flex;
align-items: center;
gap: 4px;
} }
a { a {
font-weight: 500; color: inherit;
color: var(--color-text-brand);
text-decoration: none; text-decoration: none;
transition: 0.2s ease-in-out;
&:hover, &:hover,
&:focus { &:focus {
color: var(--color-text-brand-soft); text-decoration: underline;
} }
} }
} }
.fieldVerified { .fieldVerified {
background-color: var(--color-bg-brand-softer); background-color: var(--color-bg-success-softer);
}
.fieldLink:is(dd, dt) {
margin: 0;
}
.fieldLink > a {
display: block;
padding: 8px 0;
} }
.fieldVerifiedIcon { .fieldVerifiedIcon {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-top: 8px;
} }
.fieldNumbersWrapper { .fieldNumbersWrapper {
font-size: 13px;
padding: 0; padding: 0;
a { a {
@ -323,10 +335,15 @@ svg.badgeIcon {
border-bottom: 1px solid var(--color-border-primary); border-bottom: 1px solid var(--color-border-primary);
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 0 24px;
@container (width < 500px) {
padding: 0 12px; padding: 0 12px;
@container (width >= 500px) { a {
padding: 0 24px; flex: 1 1 0px;
text-align: center;
}
} }
a { a {

View File

@ -39,6 +39,7 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 15px;
} }
} }

View File

@ -1,4 +1,4 @@
.collectionItemWrapper { .wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
@ -7,13 +7,13 @@
border-bottom: 1px solid var(--color-border-primary); border-bottom: 1px solid var(--color-border-primary);
} }
.collectionItemContent { .content {
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
padding: 15px 5px; padding: 15px 5px;
} }
.collectionItemLink { .link {
display: block; display: block;
margin-bottom: 2px; margin-bottom: 2px;
font-size: 15px; font-size: 15px;
@ -33,16 +33,19 @@
} }
} }
.collectionItemInfo { .info {
font-size: 13px;
color: var(--color-text-secondary);
}
.metaList {
--gap: 0.75ch; --gap: 0.75ch;
display: flex; display: flex;
gap: var(--gap); gap: var(--gap);
font-size: 13px;
color: var(--color-text-secondary);
& > li:not(:first-child)::before { & > li:not(:last-child)::after {
content: '·'; content: '·';
margin-inline-end: var(--gap); margin-inline-start: var(--gap);
} }
} }

View File

@ -0,0 +1,67 @@
import { useId } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import classes from './collection_list_item.module.scss';
import { CollectionMenu } from './collection_menu';
export const CollectionMetaData: React.FC<{
collection: ApiCollectionJSON;
className?: string;
}> = ({ collection, className }) => {
return (
<ul className={classNames(classes.metaList, className)}>
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
tagName='li'
/>
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: (
<RelativeTimestamp
timestamp={collection.updated_at}
short={false}
/>
),
}}
tagName='li'
/>
</ul>
);
};
export const CollectionListItem: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
const { id, name } = collection;
const linkId = useId();
return (
<article
className={classNames(classes.wrapper, 'focusable')}
tabIndex={-1}
aria-labelledby={linkId}
>
<div className={classes.content}>
<h2 id={linkId}>
<Link to={`/collections/${id}`} className={classes.link}>
{name}
</Link>
</h2>
<CollectionMetaData collection={collection} className={classes.info} />
</div>
<CollectionMenu context='list' collection={collection} />
</article>
);
};

View File

@ -0,0 +1,91 @@
import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Dropdown } from 'flavours/glitch/components/dropdown_menu';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { useAppDispatch } from 'flavours/glitch/store';
import { messages as editorMessages } from '../editor';
const messages = defineMessages({
view: {
id: 'collections.view_collection',
defaultMessage: 'View collection',
},
delete: {
id: 'collections.delete_collection',
defaultMessage: 'Delete collection',
},
more: { id: 'status.more', defaultMessage: 'More' },
});
export const CollectionMenu: React.FC<{
collection: ApiCollectionJSON;
context: 'list' | 'collection';
className?: string;
}> = ({ collection, context, className }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { id, name } = collection;
const handleDeleteClick = useCallback(() => {
dispatch(
openModal({
modalType: 'CONFIRM_DELETE_COLLECTION',
modalProps: {
name,
id,
},
}),
);
}, [dispatch, id, name]);
const menu = useMemo(() => {
const commonItems = [
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
},
{
text: intl.formatMessage(editorMessages.editDetails),
to: `/collections/${id}/edit/details`,
},
{
text: intl.formatMessage(editorMessages.editSettings),
to: `/collections/${id}/edit/settings`,
},
null,
{
text: intl.formatMessage(messages.delete),
action: handleDeleteClick,
dangerous: true,
},
];
if (context === 'list') {
return [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
...commonItems,
];
} else {
return commonItems;
}
}, [intl, id, handleDeleteClick, context]);
return (
<Dropdown scrollKey='collections' items={menu}>
<IconButton
icon='menu-icon'
iconComponent={MoreVertIcon}
title={intl.formatMessage(messages.more)}
className={className}
/>
</Dropdown>
);
};

View File

@ -0,0 +1,178 @@
import { useCallback, useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useParams } from 'react-router';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { showAlert } from 'flavours/glitch/actions/alerts';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Account } from 'flavours/glitch/components/account';
import { Avatar } from 'flavours/glitch/components/avatar';
import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { LinkedDisplayName } from 'flavours/glitch/components/display_name';
import { IconButton } from 'flavours/glitch/components/icon_button';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import { Tag } from 'flavours/glitch/components/tags/tag';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { me } from 'flavours/glitch/initial_state';
import { fetchCollection } from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { CollectionMetaData } from './collection_list_item';
import { CollectionMenu } from './collection_menu';
import classes from './styles.module.scss';
const messages = defineMessages({
empty: {
id: 'collections.accounts.empty_title',
defaultMessage: 'This collection is empty',
},
loading: {
id: 'collections.detail.loading',
defaultMessage: 'Loading collection…',
},
share: {
id: 'collections.detail.share',
defaultMessage: 'Share this collection',
},
accounts: {
id: 'collections.detail.accounts_heading',
defaultMessage: 'Accounts',
},
});
const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
const account = useAccount(id);
const author = (
<span className={classes.displayNameWithAvatar}>
<Avatar size={18} account={account} />
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
</span>
);
if (id === me) {
return (
<p className={classes.authorNote}>
<FormattedMessage
id='collections.detail.curated_by_you'
defaultMessage='Curated by you'
/>
</p>
);
}
return (
<p className={classes.authorNote}>
<FormattedMessage
id='collections.detail.curated_by_author'
defaultMessage='Curated by {author}'
values={{ author }}
/>
</p>
);
};
const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
collection,
}) => {
const intl = useIntl();
const { name, description, tag } = collection;
const dispatch = useAppDispatch();
const handleShare = useCallback(() => {
dispatch(showAlert({ message: 'Collection sharing not yet implemented' }));
}, [dispatch]);
return (
<div className={classes.header}>
<div className={classes.titleWithMenu}>
<div className={classes.titleWrapper}>
{tag && (
// TODO: Make non-interactive tag component
<Tag name={tag.name} className={classes.tag} />
)}
<h2 className={classes.name}>{name}</h2>
</div>
<div className={classes.headerButtonWrapper}>
<IconButton
iconComponent={ShareIcon}
icon='share-icon'
title={intl.formatMessage(messages.share)}
className={classes.iconButton}
onClick={handleShare}
/>
<CollectionMenu
context='collection'
collection={collection}
className={classes.iconButton}
/>
</div>
</div>
{description && <p className={classes.description}>{description}</p>}
<AuthorNote id={collection.account_id} />
<CollectionMetaData
collection={collection}
className={classes.metaData}
/>
<h2 className='sr-only'>{intl.formatMessage(messages.accounts)}</h2>
</div>
);
};
export const CollectionDetailPage: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const collection = useAppSelector((state) =>
id ? state.collections.collections[id] : undefined,
);
const isLoading = !!id && !collection;
useEffect(() => {
if (id) {
void dispatch(fetchCollection({ collectionId: id }));
}
}, [dispatch, id]);
const pageTitle = collection?.name ?? intl.formatMessage(messages.loading);
return (
<Column bindToDocument={!multiColumn} label={pageTitle}>
<ColumnHeader
showBackButton
title={pageTitle}
icon='collection-icon'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
/>
<ScrollableList
scrollKey='collection-detail'
emptyMessage={intl.formatMessage(messages.empty)}
showLoading={isLoading}
bindToDocument={!multiColumn}
alwaysPrepend
prepend={
collection ? <CollectionHeader collection={collection} /> : null
}
>
{collection?.items.map(({ account_id }) =>
account_id ? (
<Account key={account_id} minimal id={account_id} />
) : null,
)}
</ScrollableList>
<Helmet>
<title>{pageTitle}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};

View File

@ -0,0 +1,74 @@
.header {
padding: 16px;
border-bottom: 1px solid var(--color-border-primary);
}
.titleWithMenu {
display: flex;
align-items: start;
gap: 10px;
}
.titleWrapper {
flex-grow: 1;
min-width: 0;
}
.tag {
margin-bottom: 4px;
margin-inline-start: -8px;
}
.name {
font-size: 28px;
line-height: 1.2;
overflow-wrap: anywhere;
}
.description {
font-size: 15px;
margin-top: 8px;
}
.headerButtonWrapper {
display: flex;
gap: 8px;
}
.iconButton {
box-sizing: content-box;
padding: 5px;
border-radius: 4px;
border: 1px solid var(--color-border-primary);
}
.authorNote {
margin-top: 8px;
font-size: 13px;
color: var(--color-text-secondary);
}
.metaData {
margin-top: 16px;
font-size: 15px;
}
.displayNameWithAvatar {
display: inline-flex;
gap: 4px;
align-items: baseline;
a {
color: inherit;
text-decoration: underline;
&:hover,
&:focus {
text-decoration: none;
}
}
> :global(.account__avatar) {
align-self: center;
}
}

View File

@ -71,7 +71,7 @@ export const CollectionDetails: React.FC<{
}; };
void dispatch(updateCollection({ payload })).then(() => { void dispatch(updateCollection({ payload })).then(() => {
history.push(`/collections`); history.push(`/collections/${id}`);
}); });
} else { } else {
const payload: Partial<ApiCreateCollectionPayload> = { const payload: Partial<ApiCreateCollectionPayload> = {

View File

@ -68,7 +68,7 @@ export const CollectionSettings: React.FC<{
}; };
void dispatch(updateCollection({ payload })).then(() => { void dispatch(updateCollection({ payload })).then(() => {
history.push(`/collections`); history.push(`/collections/${id}`);
}); });
} else { } else {
const payload: ApiCreateCollectionPayload = { const payload: ApiCreateCollectionPayload = {

View File

@ -1,22 +1,16 @@
import { useEffect, useMemo, useCallback, useId } from 'react'; import { useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react'; import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react'; import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Column } from 'flavours/glitch/components/column'; import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header'; import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { Dropdown } from 'flavours/glitch/components/dropdown_menu';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import ScrollableList from 'flavours/glitch/components/scrollable_list'; import ScrollableList from 'flavours/glitch/components/scrollable_list';
import { import {
fetchAccountCollections, fetchAccountCollections,
@ -24,119 +18,13 @@ import {
} from 'flavours/glitch/reducers/slices/collections'; } from 'flavours/glitch/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { CollectionListItem } from './detail/collection_list_item';
import { messages as editorMessages } from './editor'; import { messages as editorMessages } from './editor';
import classes from './styles.module.scss';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.collections', defaultMessage: 'My collections' }, heading: { id: 'column.collections', defaultMessage: 'My collections' },
view: {
id: 'collections.view_collection',
defaultMessage: 'View collection',
},
delete: {
id: 'collections.delete_collection',
defaultMessage: 'Delete collection',
},
more: { id: 'status.more', defaultMessage: 'More' },
}); });
const CollectionItem: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { id, name } = collection;
const handleDeleteClick = useCallback(() => {
dispatch(
openModal({
modalType: 'CONFIRM_DELETE_COLLECTION',
modalProps: {
name,
id,
},
}),
);
}, [dispatch, id, name]);
const menu = useMemo(
() => [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
},
{
text: intl.formatMessage(editorMessages.editDetails),
to: `/collections/${id}/edit/details`,
},
{
text: intl.formatMessage(editorMessages.editSettings),
to: `/collections/${id}/edit/settings`,
},
null,
{
text: intl.formatMessage(messages.delete),
action: handleDeleteClick,
dangerous: true,
},
],
[intl, id, handleDeleteClick],
);
const linkId = useId();
return (
<article
className={classNames(classes.collectionItemWrapper, 'focusable')}
tabIndex={-1}
aria-labelledby={linkId}
>
<div className={classes.collectionItemContent}>
<h2 id={linkId}>
<Link
to={`/collections/${id}/edit/details`}
className={classes.collectionItemLink}
>
{name}
</Link>
</h2>
<ul className={classes.collectionItemInfo}>
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
tagName='li'
/>
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: (
<RelativeTimestamp
timestamp={collection.updated_at}
short={false}
/>
),
}}
tagName='li'
/>
</ul>
</div>
<Dropdown
scrollKey='collections'
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
title={intl.formatMessage(messages.more)}
/>
</article>
);
};
export const Collections: React.FC<{ export const Collections: React.FC<{
multiColumn?: boolean; multiColumn?: boolean;
}> = ({ multiColumn }) => { }> = ({ multiColumn }) => {
@ -202,7 +90,7 @@ export const Collections: React.FC<{
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{collections.map((item) => ( {collections.map((item) => (
<CollectionItem key={item.id} collection={item} /> <CollectionListItem key={item.id} collection={item} />
))} ))}
</ScrollableList> </ScrollableList>

View File

@ -11,8 +11,9 @@ import {
export const ActionsModal: React.FC<{ export const ActionsModal: React.FC<{
actions: MenuItem[]; actions: MenuItem[];
onClick: React.MouseEventHandler; onClick: React.MouseEventHandler;
}> = ({ actions, onClick }) => ( className?: string;
<div className='modal-root__modal actions-modal'> }> = ({ actions, onClick, className }) => (
<div className={classNames('modal-root__modal actions-modal', className)}>
<ul> <ul>
{actions.map((option, i: number) => { {actions.map((option, i: number) => {
if (option === null) { if (option === null) {

View File

@ -25,7 +25,7 @@ import { layoutFromWindow } from 'flavours/glitch/is_mobile';
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications'; import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report'; import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment'; import { isClientFeatureEnabled, isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
@ -68,6 +68,7 @@ import {
ListEdit, ListEdit,
ListMembers, ListMembers,
Collections, Collections,
CollectionDetail,
CollectionsEditor, CollectionsEditor,
Blocks, Blocks,
DomainBlocks, DomainBlocks,
@ -83,6 +84,7 @@ import {
TermsOfService, TermsOfService,
AccountFeatured, AccountFeatured,
AccountAbout, AccountAbout,
AccountEdit,
Quotes, Quotes,
} from './util/async-components'; } from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context'; import { ColumnsContextProvider } from './util/columns_context';
@ -240,6 +242,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
{isClientFeatureEnabled('profile_editing') && <WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />}
<WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} /> <WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} />
<WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} /> <WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} /> <WrappedRoute path='/directory' component={Directory} content={children} />
@ -274,12 +278,12 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/mutes' component={Mutes} content={children} /> <WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} /> <WrappedRoute path='/lists' component={Lists} content={children} />
{areCollectionsEnabled() && {areCollectionsEnabled() &&
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} /> [
} <WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />,
{areCollectionsEnabled() && <WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} />,
<WrappedRoute path='/collections' component={Collections} content={children} /> <WrappedRoute path='/collections' component={Collections} content={children} />
]
} }
<Route component={BundleColumnError} /> <Route component={BundleColumnError} />
</WrappedSwitch> </WrappedSwitch>
</ColumnsAreaContainer> </ColumnsAreaContainer>

View File

@ -44,13 +44,19 @@ export function DirectTimeline() {
return import('../../direct_timeline'); return import('../../direct_timeline');
} }
export function Collections () { export function Collections() {
return import('../../collections').then( return import('../../collections').then(
module => ({default: module.Collections}) module => ({default: module.Collections})
); );
} }
export function CollectionsEditor () { export function CollectionDetail() {
return import('../../collections/detail/index').then(
module => ({default: module.CollectionDetailPage})
);
}
export function CollectionsEditor() {
return import('../../collections/editor').then( return import('../../collections/editor').then(
module => ({default: module.CollectionEditorPage}) module => ({default: module.CollectionEditorPage})
); );
@ -92,6 +98,11 @@ export function AccountAbout() {
.then((module) => ({ default: module.AccountAbout })); .then((module) => ({ default: module.AccountAbout }));
} }
export function AccountEdit() {
return import('../../account_edit')
.then((module) => ({ default: module.AccountEdit }));
}
export function Followers () { export function Followers () {
return import('../../followers'); return import('../../followers');
} }

View File

@ -55,3 +55,7 @@ export function useAccountId() {
return accountId satisfies AccountId; return accountId satisfies AccountId;
} }
export function useCurrentAccountId() {
return useAppSelector((state) => state.meta.get('me', null) as string | null);
}

View File

@ -14,7 +14,7 @@ import { CustomEmojiFactory } from './custom_emoji';
import type { CustomEmoji } from './custom_emoji'; import type { CustomEmoji } from './custom_emoji';
// AccountField // AccountField
interface AccountFieldShape extends Required<ApiAccountFieldJSON> { export interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
name_emojified: string; name_emojified: string;
value_emojified: string; value_emojified: string;
value_plain: string | null; value_plain: string | null;

View File

@ -4280,7 +4280,8 @@ input.glitch-setting-text {
cursor: pointer; cursor: pointer;
& > div { & > div {
background: rgb(from var(--color-shadow-primary) r g b / 60%); color: var(--color-text-on-media);
background: rgb(from var(--color-bg-media) r g b / 60%);
border-radius: 8px; border-radius: 8px;
padding: 12px 9px; padding: 12px 9px;
backdrop-filter: $backdrop-blur-filter; backdrop-filter: $backdrop-blur-filter;
@ -4293,7 +4294,7 @@ input.glitch-setting-text {
button, button,
a { a {
display: inline; display: inline;
color: var(--color-text-primary); color: var(--color-text-on-media);
background: transparent; background: transparent;
border: 0; border: 0;
padding: 0 8px; padding: 0 8px;
@ -4304,7 +4305,7 @@ input.glitch-setting-text {
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
color: var(--color-text-primary); color: var(--color-text-on-media);
} }
} }

View File

@ -18,7 +18,7 @@ export function isServerFeatureEnabled(feature: ServerFeatures) {
return initialState?.features.includes(feature) ?? false; return initialState?.features.includes(feature) ?? false;
} }
type ClientFeatures = 'collections'; type ClientFeatures = 'collections' | 'profile_editing';
export function isClientFeatureEnabled(feature: ClientFeatures) { export function isClientFeatureEnabled(feature: ClientFeatures) {
try { try {

View File

@ -2,9 +2,9 @@
<path fill="url(#VerifiedGradient)" d="M8 .837a3.168 3.168 0 0 1 2.47 1.187 3.166 3.166 0 0 1 2.601.906 3.168 3.168 0 0 1 .905 2.6A3.167 3.167 0 0 1 15.164 8a3.172 3.172 0 0 1-1.188 2.47 3.167 3.167 0 0 1-.903 2.597 3.168 3.168 0 0 1-2.596.909 3.167 3.167 0 0 1-4.95.001 3.166 3.166 0 0 1-3.397-2.258 3.169 3.169 0 0 1-.107-1.24A3.168 3.168 0 0 1 .826 8a3.17 3.17 0 0 1 1.197-2.479 3.168 3.168 0 0 1 .91-2.593 3.166 3.166 0 0 1 2.596-.905A3.169 3.169 0 0 1 8 .837Z"/> <path fill="url(#VerifiedGradient)" d="M8 .837a3.168 3.168 0 0 1 2.47 1.187 3.166 3.166 0 0 1 2.601.906 3.168 3.168 0 0 1 .905 2.6A3.167 3.167 0 0 1 15.164 8a3.172 3.172 0 0 1-1.188 2.47 3.167 3.167 0 0 1-.903 2.597 3.168 3.168 0 0 1-2.596.909 3.167 3.167 0 0 1-4.95.001 3.166 3.166 0 0 1-3.397-2.258 3.169 3.169 0 0 1-.107-1.24A3.168 3.168 0 0 1 .826 8a3.17 3.17 0 0 1 1.197-2.479 3.168 3.168 0 0 1 .91-2.593 3.166 3.166 0 0 1 2.596-.905A3.169 3.169 0 0 1 8 .837Z"/>
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="m6 8 1.333 1.333L10 6.667"/> <path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="m6 8 1.333 1.333L10 6.667"/>
<defs> <defs>
<linearGradient id="VerifiedGradient" x1="-.966" x2="12.162" y1="2.629" y2="17.493" gradientUnits="userSpaceOnUse"> <linearGradient id="VerifiedGradient" x1="7.99512" y1="0.836914" x2="7.99512" y2="15.1689" gradientUnits="userSpaceOnUse">
<stop offset=".13" stop-color="#5638CC"/> <stop stop-color="#00BC7D"/>
<stop offset=".995" stop-color="#DC03F0"/> <stop offset="1" stop-color="#00BBA7"/>
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 930 B

After

Width:  |  Height:  |  Size: 921 B

View File

@ -11,14 +11,14 @@ export interface ApiCollectionJSON {
account_id: string; account_id: string;
id: string; id: string;
uri: string; uri: string | null;
local: boolean; local: boolean;
item_count: number; item_count: number;
name: string; name: string;
description: string; description: string;
tag?: ApiTagJSON; tag: ApiTagJSON | null;
language: string; language: string | null;
sensitive: boolean; sensitive: boolean;
discoverable: boolean; discoverable: boolean;

View File

@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = ` exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
<div <span
className="account__avatar account__avatar--loading" className="account__avatar account__avatar--loading"
onMouseEnter={[Function]} onMouseEnter={[Function]}
onMouseLeave={[Function]} onMouseLeave={[Function]}
@ -18,11 +18,11 @@ exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
onLoad={[Function]} onLoad={[Function]}
src="/animated/alice.gif" src="/animated/alice.gif"
/> />
</div> </span>
`; `;
exports[`<Avatar /> > Still > renders a still avatar 1`] = ` exports[`<Avatar /> > Still > renders a still avatar 1`] = `
<div <span
className="account__avatar account__avatar--loading" className="account__avatar account__avatar--loading"
onMouseEnter={[Function]} onMouseEnter={[Function]}
onMouseLeave={[Function]} onMouseLeave={[Function]}
@ -39,5 +39,5 @@ exports[`<Avatar /> > Still > renders a still avatar 1`] = `
onLoad={[Function]} onLoad={[Function]}
src="/static/alice.jpg" src="/static/alice.jpg"
/> />
</div> </span>
`; `;

View File

@ -297,7 +297,7 @@ export const Account: React.FC<AccountProps> = ({
> >
<div className='account__info-wrapper'> <div className='account__info-wrapper'>
<Link <Link
className='account__display-name' className='account__display-name focusable'
title={account?.acct} title={account?.acct}
to={`/@${account?.acct}`} to={`/@${account?.acct}`}
data-hover-card-account={id} data-hover-card-account={id}

View File

@ -53,7 +53,7 @@ export const Avatar: React.FC<Props> = ({
}, [setError]); }, [setError]);
const avatar = ( const avatar = (
<div <span
className={classNames(className, 'account__avatar', { className={classNames(className, 'account__avatar', {
'account__avatar--inline': inline, 'account__avatar--inline': inline,
'account__avatar--loading': loading, 'account__avatar--loading': loading,
@ -67,14 +67,14 @@ export const Avatar: React.FC<Props> = ({
)} )}
{counter && ( {counter && (
<div <span
className='account__avatar__counter' className='account__avatar__counter'
style={{ borderColor: counterBorderColor }} style={{ borderColor: counterBorderColor }}
> >
{counter} {counter}
</div> </span>
)} )}
</div> </span>
); );
if (withLink) { if (withLink) {

View File

@ -1,6 +1,8 @@
import { forwardRef, useRef, useImperativeHandle } from 'react'; import { forwardRef, useRef, useImperativeHandle } from 'react';
import type { Ref } from 'react'; import type { Ref } from 'react';
import classNames from 'classnames';
import { scrollTop } from 'mastodon/scroll'; import { scrollTop } from 'mastodon/scroll';
export interface ColumnRef { export interface ColumnRef {
@ -12,10 +14,11 @@ interface ColumnProps {
children?: React.ReactNode; children?: React.ReactNode;
label?: string; label?: string;
bindToDocument?: boolean; bindToDocument?: boolean;
className?: string;
} }
export const Column = forwardRef<ColumnRef, ColumnProps>( export const Column = forwardRef<ColumnRef, ColumnProps>(
({ children, label, bindToDocument }, ref: Ref<ColumnRef>) => { ({ children, label, bindToDocument, className }, ref: Ref<ColumnRef>) => {
const nodeRef = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@ -39,7 +42,12 @@ export const Column = forwardRef<ColumnRef, ColumnProps>(
})); }));
return ( return (
<div role='region' aria-label={label} className='column' ref={nodeRef}> <div
role='region'
aria-label={label}
className={classNames('column', className)}
ref={nodeRef}
>
{children} {children}
</div> </div>
); );

View File

@ -73,6 +73,7 @@ export interface Props {
iconComponent?: IconProp; iconComponent?: IconProp;
active?: boolean; active?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
className?: string;
pinned?: boolean; pinned?: boolean;
multiColumn?: boolean; multiColumn?: boolean;
extraButton?: React.ReactNode; extraButton?: React.ReactNode;
@ -91,6 +92,7 @@ export const ColumnHeader: React.FC<Props> = ({
iconComponent, iconComponent,
active, active,
children, children,
className,
pinned, pinned,
multiColumn, multiColumn,
extraButton, extraButton,
@ -141,7 +143,7 @@ export const ColumnHeader: React.FC<Props> = ({
onPin?.(); onPin?.();
}, [history, pinned, onPin]); }, [history, pinned, onPin]);
const wrapperClassName = classNames('column-header__wrapper', { const wrapperClassName = classNames('column-header__wrapper', className, {
active, active,
}); });
@ -256,7 +258,8 @@ export const ColumnHeader: React.FC<Props> = ({
} }
const hasIcon = icon && iconComponent; const hasIcon = icon && iconComponent;
const hasTitle = hasIcon && title; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasTitle = (hasIcon || backButton) && title;
const component = ( const component = (
<div className={wrapperClassName}> <div className={wrapperClassName}>
@ -270,7 +273,7 @@ export const ColumnHeader: React.FC<Props> = ({
className='column-header__title' className='column-header__title'
type='button' type='button'
> >
{!backButton && ( {!backButton && hasIcon && (
<Icon <Icon
id={icon} id={icon}
icon={iconComponent} icon={iconComponent}

View File

@ -311,6 +311,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
status?: ImmutableMap<string, unknown>; status?: ImmutableMap<string, unknown>;
needsStatusRefresh?: boolean; needsStatusRefresh?: boolean;
forceDropdown?: boolean; forceDropdown?: boolean;
className?: string;
renderItem?: RenderItemFn<Item>; renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>; renderHeader?: RenderHeaderFn<Item>;
onOpen?: // Must use a union type for the full function as a union with void is not allowed. onOpen?: // Must use a union type for the full function as a union with void is not allowed.
@ -335,6 +336,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
status, status,
needsStatusRefresh, needsStatusRefresh,
forceDropdown = false, forceDropdown = false,
className,
renderItem, renderItem,
renderHeader, renderHeader,
onOpen, onOpen,
@ -434,6 +436,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
modalProps: { modalProps: {
actions: items, actions: items,
onClick: handleItemClick, onClick: handleItemClick,
className,
}, },
}), }),
); );
@ -462,6 +465,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
handleClose, handleClose,
statusId, statusId,
needsStatusRefresh, needsStatusRefresh,
className,
], ],
); );
@ -515,7 +519,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
popperConfig={popperConfig} popperConfig={popperConfig}
> >
{({ props, arrowProps, placement }) => ( {({ props, arrowProps, placement }) => (
<div {...props} id={menuId}> <div {...props} className={className} id={menuId}>
<div className={`dropdown-animation dropdown-menu ${placement}`}> <div className={`dropdown-animation dropdown-menu ${placement}`}>
<div <div
className={`dropdown-menu__arrow ${placement}`} className={`dropdown-menu__arrow ${placement}`}

View File

@ -3,8 +3,10 @@ import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { useIdentity } from '@/mastodon/identity_context'; import { useIdentity } from '@/mastodon/identity_context';
import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
import { import {
fetchRelationships, fetchRelationships,
followAccount, followAccount,
@ -158,14 +160,24 @@ export const FollowButton: React.FC<{
} }
if (accountId === me) { if (accountId === me) {
const buttonClasses = classNames(className, 'button button-secondary', {
'button--compact': compact,
});
if (isClientFeatureEnabled('profile_editing')) {
return (
<Link to='/profile/edit' className={buttonClasses}>
{label}
</Link>
);
}
return ( return (
<a <a
href='/settings/profile' href='/settings/profile'
target='_blank' target='_blank'
rel='noopener' rel='noopener'
className={classNames(className, 'button button-secondary', { className={buttonClasses}
'button--compact': compact,
})}
> >
{label} {label}
</a> </a>

View File

@ -0,0 +1,53 @@
import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { Column } from '@/mastodon/components/column';
import { ColumnHeader } from '@/mastodon/components/column_header';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import classes from './styles.module.scss';
export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
if (!accountId) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
if (!account) {
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<LoadingIndicator />
</Column>
);
}
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader
title={intl.formatMessage({
id: 'account_edit.column_title',
defaultMessage: 'Edit Profile',
})}
className={classes.header}
showBackButton
extraButton={
<Link to={`/@${account.acct}`} className='button'>
<FormattedMessage
id='account_edit.column_button'
defaultMessage='Done'
/>
</Link>
}
/>
</Column>
);
};

View File

@ -0,0 +1,26 @@
.column {
border: 1px solid var(--color-border-primary);
border-top-width: 0;
}
.header {
:global(.column-header__buttons) {
align-items: center;
padding-inline-end: 16px;
height: auto;
}
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 24px 24px 12px;
> h1 {
flex-grow: 1;
font-weight: 600;
font-size: 15px;
}
}

View File

@ -214,7 +214,10 @@ export const AccountHeader: React.FC<{
<> <>
<AccountBio <AccountBio
accountId={accountId} accountId={accountId}
className='account__header__content' className={classNames(
'account__header__content',
isRedesign && redesignClasses.bio,
)}
/> />
<AccountHeaderFields accountId={accountId} /> <AccountHeaderFields accountId={accountId} />
</> </>

View File

@ -15,8 +15,7 @@ import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import { Icon } from '@/mastodon/components/icon'; import { Icon } from '@/mastodon/components/icon';
import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccount } from '@/mastodon/hooks/useAccount';
import type { Account } from '@/mastodon/models/account'; import type { Account, AccountFieldShape } from '@/mastodon/models/account';
import { isValidUrl } from '@/mastodon/utils/checks';
import type { OnElementHandler } from '@/mastodon/utils/html'; import type { OnElementHandler } from '@/mastodon/utils/html';
import { cleanExtraEmojis } from '../../emoji/normalize'; import { cleanExtraEmojis } from '../../emoji/normalize';
@ -76,8 +75,8 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
[account.emojis], [account.emojis],
); );
const textHasCustomEmoji = useCallback( const textHasCustomEmoji = useCallback(
(text: string) => { (text?: string | null) => {
if (!emojis) { if (!emojis || !text) {
return false; return false;
} }
for (const emoji of Object.keys(emojis)) { for (const emoji of Object.keys(emojis)) {
@ -92,22 +91,57 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
const htmlHandlers = useElementHandledLink({ const htmlHandlers = useElementHandledLink({
hashtagAccountId: account.id, hashtagAccountId: account.id,
}); });
const intl = useIntl();
if (account.fields.isEmpty()) {
return null;
}
return ( return (
<CustomEmojiProvider emojis={emojis}> <CustomEmojiProvider emojis={emojis}>
<dl className={classes.fieldList}> <dl className={classes.fieldList}>
{account.fields.map( {account.fields.map((field, key) => (
( <FieldRow
{ name, name_emojified, value_emojified, value_plain, verified_at },
key,
) => (
<div
key={key} key={key}
{...field.toJSON()}
htmlHandlers={htmlHandlers}
textHasCustomEmoji={textHasCustomEmoji}
/>
))}
</dl>
</CustomEmojiProvider>
);
};
const FieldRow: FC<
{
textHasCustomEmoji: (text?: string | null) => boolean;
htmlHandlers: ReturnType<typeof useElementHandledLink>;
} & AccountFieldShape
> = ({
textHasCustomEmoji,
htmlHandlers,
name,
name_emojified,
value_emojified,
value_plain,
verified_at,
}) => {
const intl = useIntl();
const [showAll, setShowAll] = useState(false);
const handleClick = useCallback(() => {
setShowAll((prev) => !prev);
}, []);
return (
/* eslint-disable -- This method of showing field contents is not very accessible, but it's what we've got for now */
<div
className={classNames( className={classNames(
classes.fieldRow, classes.fieldRow,
verified_at && classes.fieldVerified, verified_at && classes.fieldVerified,
showAll && classes.fieldShowAll,
)} )}
onClick={handleClick}
/* eslint-enable */
> >
<FieldHTML <FieldHTML
as='dt' as='dt'
@ -118,14 +152,16 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
className='translate' className='translate'
{...htmlHandlers} {...htmlHandlers}
/> />
<dd>
<FieldHTML <FieldHTML
as='dd' as='span'
text={value_plain ?? ''} text={value_plain ?? ''}
textEmojified={value_emojified} textEmojified={value_emojified}
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')} textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
titleLength={120} titleLength={120}
{...htmlHandlers} {...htmlHandlers}
/> />
{verified_at && ( {verified_at && (
<Icon <Icon
id='verified' id='verified'
@ -137,17 +173,14 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
noFill noFill
/> />
)} )}
</dd>
</div> </div>
),
)}
</dl>
</CustomEmojiProvider>
); );
}; };
const FieldHTML: FC< const FieldHTML: FC<
{ {
as: 'dd' | 'dt'; as?: 'span' | 'dt';
text: string; text: string;
textEmojified: string; textEmojified: string;
textHasCustomEmoji: boolean; textHasCustomEmoji: boolean;
@ -164,11 +197,6 @@ const FieldHTML: FC<
onElement, onElement,
...props ...props
}) => { }) => {
const [showAll, setShowAll] = useState(false);
const handleClick = useCallback(() => {
setShowAll((prev) => !prev);
}, []);
const handleElement: OnElementHandler = useCallback( const handleElement: OnElementHandler = useCallback(
(element, props, children, extra) => { (element, props, children, extra) => {
if (element instanceof HTMLAnchorElement) { if (element instanceof HTMLAnchorElement) {
@ -186,17 +214,13 @@ const FieldHTML: FC<
}, },
[onElement, textHasCustomEmoji], [onElement, textHasCustomEmoji],
); );
return ( return (
<EmojiHTML <EmojiHTML
as={as} as={as}
htmlString={textEmojified} htmlString={textEmojified}
title={showTitleOnLength(text, titleLength)} title={showTitleOnLength(text, titleLength)}
className={classNames( className={className}
className,
text && isValidUrl(text) && classes.fieldLink,
showAll && classes.fieldShowAll,
)}
onClick={handleClick}
onElement={handleElement} onElement={handleElement}
{...props} {...props}
/> />

View File

@ -42,6 +42,8 @@ import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common'; import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl(); const intl = useIntl();
const { signedIn, permissions } = useIdentity(); const { signedIn, permissions } = useIdentity();
@ -86,6 +88,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
items={menuItems} items={menuItems}
icon='ellipsis-v' icon='ellipsis-v'
iconComponent={MoreHorizIcon} iconComponent={MoreHorizIcon}
className={classes.buttonMenu}
/> />
); );
}; };

View File

@ -24,11 +24,10 @@
.name { .name {
flex-grow: 1; flex-grow: 1;
font-size: 22px;
white-space: initial;
line-height: normal;
> h1 { > h1 {
font-size: 22px;
line-height: normal;
white-space: initial; white-space: initial;
} }
} }
@ -149,11 +148,33 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
border-top: 1px solid var(--color-border-primary); border-top: 1px solid var(--color-border-primary);
} }
.buttonMenu {
// Override the modal for mobile.
&:global(.actions-modal) {
max-height: none;
}
li :global(.icon) {
width: 20px;
height: 20px;
}
}
.bio {
font-size: 15px;
}
.badge { .badge {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
border: none; border: none;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-weight: 500; font-weight: 500;
padding: 4px;
font-size: 13px;
:global(.account__header__badges) > & {
line-height: 1;
}
> span { > span {
font-weight: unset; font-weight: unset;
@ -194,12 +215,13 @@ svg.badgeIcon {
.fieldList { .fieldList {
display: grid; display: grid;
grid-template-columns: 160px 1fr min-content; grid-template-columns: 160px 1fr;
column-gap: 12px; column-gap: 12px;
margin: 4px 0 16px; margin: 16px 0;
border-top: 0.5px solid var(--color-border-primary);
@container (width < 420px) { @container (width < 420px) {
grid-template-columns: 100px 1fr min-content; grid-template-columns: 100px 1fr;
} }
} }
@ -208,11 +230,10 @@ svg.badgeIcon {
grid-column: 1 / -1; grid-column: 1 / -1;
align-items: start; align-items: start;
grid-template-columns: subgrid; grid-template-columns: subgrid;
padding: 0 4px; padding: 8px;
border-bottom: 0.5px solid var(--color-border-primary);
> :is(dt, dd) { > :is(dt, dd) {
margin: 8px 0;
&:not(.fieldShowAll) { &:not(.fieldShowAll) {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
@ -227,43 +248,34 @@ svg.badgeIcon {
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
&:not(.fieldVerified) > dd { > dd {
grid-column: span 2; display: flex;
align-items: center;
gap: 4px;
} }
a { a {
font-weight: 500; color: inherit;
color: var(--color-text-brand);
text-decoration: none; text-decoration: none;
transition: 0.2s ease-in-out;
&:hover, &:hover,
&:focus { &:focus {
color: var(--color-text-brand-soft); text-decoration: underline;
} }
} }
} }
.fieldVerified { .fieldVerified {
background-color: var(--color-bg-brand-softer); background-color: var(--color-bg-success-softer);
}
.fieldLink:is(dd, dt) {
margin: 0;
}
.fieldLink > a {
display: block;
padding: 8px 0;
} }
.fieldVerifiedIcon { .fieldVerifiedIcon {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-top: 8px;
} }
.fieldNumbersWrapper { .fieldNumbersWrapper {
font-size: 13px;
padding: 0; padding: 0;
a { a {
@ -323,10 +335,15 @@ svg.badgeIcon {
border-bottom: 1px solid var(--color-border-primary); border-bottom: 1px solid var(--color-border-primary);
display: flex; display: flex;
gap: 12px; gap: 12px;
padding: 0 24px;
@container (width < 500px) {
padding: 0 12px; padding: 0 12px;
@container (width >= 500px) { a {
padding: 0 24px; flex: 1 1 0px;
text-align: center;
}
} }
a { a {

View File

@ -39,6 +39,7 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 15px;
} }
} }

View File

@ -1,4 +1,4 @@
.collectionItemWrapper { .wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
@ -7,13 +7,13 @@
border-bottom: 1px solid var(--color-border-primary); border-bottom: 1px solid var(--color-border-primary);
} }
.collectionItemContent { .content {
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
padding: 15px 5px; padding: 15px 5px;
} }
.collectionItemLink { .link {
display: block; display: block;
margin-bottom: 2px; margin-bottom: 2px;
font-size: 15px; font-size: 15px;
@ -33,16 +33,19 @@
} }
} }
.collectionItemInfo { .info {
font-size: 13px;
color: var(--color-text-secondary);
}
.metaList {
--gap: 0.75ch; --gap: 0.75ch;
display: flex; display: flex;
gap: var(--gap); gap: var(--gap);
font-size: 13px;
color: var(--color-text-secondary);
& > li:not(:first-child)::before { & > li:not(:last-child)::after {
content: '·'; content: '·';
margin-inline-end: var(--gap); margin-inline-start: var(--gap);
} }
} }

View File

@ -0,0 +1,67 @@
import { useId } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import classes from './collection_list_item.module.scss';
import { CollectionMenu } from './collection_menu';
export const CollectionMetaData: React.FC<{
collection: ApiCollectionJSON;
className?: string;
}> = ({ collection, className }) => {
return (
<ul className={classNames(classes.metaList, className)}>
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
tagName='li'
/>
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: (
<RelativeTimestamp
timestamp={collection.updated_at}
short={false}
/>
),
}}
tagName='li'
/>
</ul>
);
};
export const CollectionListItem: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
const { id, name } = collection;
const linkId = useId();
return (
<article
className={classNames(classes.wrapper, 'focusable')}
tabIndex={-1}
aria-labelledby={linkId}
>
<div className={classes.content}>
<h2 id={linkId}>
<Link to={`/collections/${id}`} className={classes.link}>
{name}
</Link>
</h2>
<CollectionMetaData collection={collection} className={classes.info} />
</div>
<CollectionMenu context='list' collection={collection} />
</article>
);
};

View File

@ -0,0 +1,91 @@
import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
import { openModal } from 'mastodon/actions/modal';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { IconButton } from 'mastodon/components/icon_button';
import { useAppDispatch } from 'mastodon/store';
import { messages as editorMessages } from '../editor';
const messages = defineMessages({
view: {
id: 'collections.view_collection',
defaultMessage: 'View collection',
},
delete: {
id: 'collections.delete_collection',
defaultMessage: 'Delete collection',
},
more: { id: 'status.more', defaultMessage: 'More' },
});
export const CollectionMenu: React.FC<{
collection: ApiCollectionJSON;
context: 'list' | 'collection';
className?: string;
}> = ({ collection, context, className }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { id, name } = collection;
const handleDeleteClick = useCallback(() => {
dispatch(
openModal({
modalType: 'CONFIRM_DELETE_COLLECTION',
modalProps: {
name,
id,
},
}),
);
}, [dispatch, id, name]);
const menu = useMemo(() => {
const commonItems = [
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
},
{
text: intl.formatMessage(editorMessages.editDetails),
to: `/collections/${id}/edit/details`,
},
{
text: intl.formatMessage(editorMessages.editSettings),
to: `/collections/${id}/edit/settings`,
},
null,
{
text: intl.formatMessage(messages.delete),
action: handleDeleteClick,
dangerous: true,
},
];
if (context === 'list') {
return [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
...commonItems,
];
} else {
return commonItems;
}
}, [intl, id, handleDeleteClick, context]);
return (
<Dropdown scrollKey='collections' items={menu}>
<IconButton
icon='menu-icon'
iconComponent={MoreVertIcon}
title={intl.formatMessage(messages.more)}
className={className}
/>
</Dropdown>
);
};

View File

@ -0,0 +1,178 @@
import { useCallback, useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useParams } from 'react-router';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { showAlert } from 'mastodon/actions/alerts';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Account } from 'mastodon/components/account';
import { Avatar } from 'mastodon/components/avatar';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { LinkedDisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import ScrollableList from 'mastodon/components/scrollable_list';
import { Tag } from 'mastodon/components/tags/tag';
import { useAccount } from 'mastodon/hooks/useAccount';
import { me } from 'mastodon/initial_state';
import { fetchCollection } from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { CollectionMetaData } from './collection_list_item';
import { CollectionMenu } from './collection_menu';
import classes from './styles.module.scss';
const messages = defineMessages({
empty: {
id: 'collections.accounts.empty_title',
defaultMessage: 'This collection is empty',
},
loading: {
id: 'collections.detail.loading',
defaultMessage: 'Loading collection…',
},
share: {
id: 'collections.detail.share',
defaultMessage: 'Share this collection',
},
accounts: {
id: 'collections.detail.accounts_heading',
defaultMessage: 'Accounts',
},
});
const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
const account = useAccount(id);
const author = (
<span className={classes.displayNameWithAvatar}>
<Avatar size={18} account={account} />
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
</span>
);
if (id === me) {
return (
<p className={classes.authorNote}>
<FormattedMessage
id='collections.detail.curated_by_you'
defaultMessage='Curated by you'
/>
</p>
);
}
return (
<p className={classes.authorNote}>
<FormattedMessage
id='collections.detail.curated_by_author'
defaultMessage='Curated by {author}'
values={{ author }}
/>
</p>
);
};
const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
collection,
}) => {
const intl = useIntl();
const { name, description, tag } = collection;
const dispatch = useAppDispatch();
const handleShare = useCallback(() => {
dispatch(showAlert({ message: 'Collection sharing not yet implemented' }));
}, [dispatch]);
return (
<div className={classes.header}>
<div className={classes.titleWithMenu}>
<div className={classes.titleWrapper}>
{tag && (
// TODO: Make non-interactive tag component
<Tag name={tag.name} className={classes.tag} />
)}
<h2 className={classes.name}>{name}</h2>
</div>
<div className={classes.headerButtonWrapper}>
<IconButton
iconComponent={ShareIcon}
icon='share-icon'
title={intl.formatMessage(messages.share)}
className={classes.iconButton}
onClick={handleShare}
/>
<CollectionMenu
context='collection'
collection={collection}
className={classes.iconButton}
/>
</div>
</div>
{description && <p className={classes.description}>{description}</p>}
<AuthorNote id={collection.account_id} />
<CollectionMetaData
collection={collection}
className={classes.metaData}
/>
<h2 className='sr-only'>{intl.formatMessage(messages.accounts)}</h2>
</div>
);
};
export const CollectionDetailPage: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const collection = useAppSelector((state) =>
id ? state.collections.collections[id] : undefined,
);
const isLoading = !!id && !collection;
useEffect(() => {
if (id) {
void dispatch(fetchCollection({ collectionId: id }));
}
}, [dispatch, id]);
const pageTitle = collection?.name ?? intl.formatMessage(messages.loading);
return (
<Column bindToDocument={!multiColumn} label={pageTitle}>
<ColumnHeader
showBackButton
title={pageTitle}
icon='collection-icon'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
/>
<ScrollableList
scrollKey='collection-detail'
emptyMessage={intl.formatMessage(messages.empty)}
showLoading={isLoading}
bindToDocument={!multiColumn}
alwaysPrepend
prepend={
collection ? <CollectionHeader collection={collection} /> : null
}
>
{collection?.items.map(({ account_id }) =>
account_id ? (
<Account key={account_id} minimal id={account_id} />
) : null,
)}
</ScrollableList>
<Helmet>
<title>{pageTitle}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};

View File

@ -0,0 +1,74 @@
.header {
padding: 16px;
border-bottom: 1px solid var(--color-border-primary);
}
.titleWithMenu {
display: flex;
align-items: start;
gap: 10px;
}
.titleWrapper {
flex-grow: 1;
min-width: 0;
}
.tag {
margin-bottom: 4px;
margin-inline-start: -8px;
}
.name {
font-size: 28px;
line-height: 1.2;
overflow-wrap: anywhere;
}
.description {
font-size: 15px;
margin-top: 8px;
}
.headerButtonWrapper {
display: flex;
gap: 8px;
}
.iconButton {
box-sizing: content-box;
padding: 5px;
border-radius: 4px;
border: 1px solid var(--color-border-primary);
}
.authorNote {
margin-top: 8px;
font-size: 13px;
color: var(--color-text-secondary);
}
.metaData {
margin-top: 16px;
font-size: 15px;
}
.displayNameWithAvatar {
display: inline-flex;
gap: 4px;
align-items: baseline;
a {
color: inherit;
text-decoration: underline;
&:hover,
&:focus {
text-decoration: none;
}
}
> :global(.account__avatar) {
align-self: center;
}
}

View File

@ -68,7 +68,7 @@ export const CollectionDetails: React.FC<{
}; };
void dispatch(updateCollection({ payload })).then(() => { void dispatch(updateCollection({ payload })).then(() => {
history.push(`/collections`); history.push(`/collections/${id}`);
}); });
} else { } else {
const payload: Partial<ApiCreateCollectionPayload> = { const payload: Partial<ApiCreateCollectionPayload> = {

View File

@ -68,7 +68,7 @@ export const CollectionSettings: React.FC<{
}; };
void dispatch(updateCollection({ payload })).then(() => { void dispatch(updateCollection({ payload })).then(() => {
history.push(`/collections`); history.push(`/collections/${id}`);
}); });
} else { } else {
const payload: ApiCreateCollectionPayload = { const payload: ApiCreateCollectionPayload = {

View File

@ -1,22 +1,16 @@
import { useEffect, useMemo, useCallback, useId } from 'react'; import { useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react'; import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react'; import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { openModal } from 'mastodon/actions/modal';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Column } from 'mastodon/components/column'; import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header'; import { ColumnHeader } from 'mastodon/components/column_header';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import ScrollableList from 'mastodon/components/scrollable_list'; import ScrollableList from 'mastodon/components/scrollable_list';
import { import {
fetchAccountCollections, fetchAccountCollections,
@ -24,119 +18,13 @@ import {
} from 'mastodon/reducers/slices/collections'; } from 'mastodon/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { CollectionListItem } from './detail/collection_list_item';
import { messages as editorMessages } from './editor'; import { messages as editorMessages } from './editor';
import classes from './styles.module.scss';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.collections', defaultMessage: 'My collections' }, heading: { id: 'column.collections', defaultMessage: 'My collections' },
view: {
id: 'collections.view_collection',
defaultMessage: 'View collection',
},
delete: {
id: 'collections.delete_collection',
defaultMessage: 'Delete collection',
},
more: { id: 'status.more', defaultMessage: 'More' },
}); });
const CollectionItem: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { id, name } = collection;
const handleDeleteClick = useCallback(() => {
dispatch(
openModal({
modalType: 'CONFIRM_DELETE_COLLECTION',
modalProps: {
name,
id,
},
}),
);
}, [dispatch, id, name]);
const menu = useMemo(
() => [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
},
{
text: intl.formatMessage(editorMessages.editDetails),
to: `/collections/${id}/edit/details`,
},
{
text: intl.formatMessage(editorMessages.editSettings),
to: `/collections/${id}/edit/settings`,
},
null,
{
text: intl.formatMessage(messages.delete),
action: handleDeleteClick,
dangerous: true,
},
],
[intl, id, handleDeleteClick],
);
const linkId = useId();
return (
<article
className={classNames(classes.collectionItemWrapper, 'focusable')}
tabIndex={-1}
aria-labelledby={linkId}
>
<div className={classes.collectionItemContent}>
<h2 id={linkId}>
<Link
to={`/collections/${id}/edit/details`}
className={classes.collectionItemLink}
>
{name}
</Link>
</h2>
<ul className={classes.collectionItemInfo}>
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
tagName='li'
/>
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: (
<RelativeTimestamp
timestamp={collection.updated_at}
short={false}
/>
),
}}
tagName='li'
/>
</ul>
</div>
<Dropdown
scrollKey='collections'
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
title={intl.formatMessage(messages.more)}
/>
</article>
);
};
export const Collections: React.FC<{ export const Collections: React.FC<{
multiColumn?: boolean; multiColumn?: boolean;
}> = ({ multiColumn }) => { }> = ({ multiColumn }) => {
@ -202,7 +90,7 @@ export const Collections: React.FC<{
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{collections.map((item) => ( {collections.map((item) => (
<CollectionItem key={item.id} collection={item} /> <CollectionListItem key={item.id} collection={item} />
))} ))}
</ScrollableList> </ScrollableList>

View File

@ -11,8 +11,9 @@ import {
export const ActionsModal: React.FC<{ export const ActionsModal: React.FC<{
actions: MenuItem[]; actions: MenuItem[];
onClick: React.MouseEventHandler; onClick: React.MouseEventHandler;
}> = ({ actions, onClick }) => ( className?: string;
<div className='modal-root__modal actions-modal'> }> = ({ actions, onClick, className }) => (
<div className={classNames('modal-root__modal actions-modal', className)}>
<ul> <ul>
{actions.map((option, i: number) => { {actions.map((option, i: number) => {
if (option === null) { if (option === null) {

View File

@ -22,7 +22,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { layoutFromWindow } from 'mastodon/is_mobile'; import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { isClientFeatureEnabled, isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
@ -65,6 +65,7 @@ import {
ListEdit, ListEdit,
ListMembers, ListMembers,
Collections, Collections,
CollectionDetail,
CollectionsEditor, CollectionsEditor,
Blocks, Blocks,
DomainBlocks, DomainBlocks,
@ -80,6 +81,7 @@ import {
TermsOfService, TermsOfService,
AccountFeatured, AccountFeatured,
AccountAbout, AccountAbout,
AccountEdit,
Quotes, Quotes,
} from './util/async-components'; } from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context'; import { ColumnsContextProvider } from './util/columns_context';
@ -232,6 +234,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
{isClientFeatureEnabled('profile_editing') && <WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />}
<WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} /> <WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} />
<WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} /> <WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} /> <WrappedRoute path='/directory' component={Directory} content={children} />
@ -266,12 +270,12 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/mutes' component={Mutes} content={children} /> <WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} /> <WrappedRoute path='/lists' component={Lists} content={children} />
{areCollectionsEnabled() && {areCollectionsEnabled() &&
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} /> [
} <WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />,
{areCollectionsEnabled() && <WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} />,
<WrappedRoute path='/collections' component={Collections} content={children} /> <WrappedRoute path='/collections' component={Collections} content={children} />
]
} }
<Route component={BundleColumnError} /> <Route component={BundleColumnError} />
</WrappedSwitch> </WrappedSwitch>
</ColumnsAreaContainer> </ColumnsAreaContainer>

View File

@ -44,13 +44,19 @@ export function Lists () {
return import('../../lists'); return import('../../lists');
} }
export function Collections () { export function Collections() {
return import('../../collections').then( return import('../../collections').then(
module => ({default: module.Collections}) module => ({default: module.Collections})
); );
} }
export function CollectionsEditor () { export function CollectionDetail() {
return import('../../collections/detail/index').then(
module => ({default: module.CollectionDetailPage})
);
}
export function CollectionsEditor() {
return import('../../collections/editor').then( return import('../../collections/editor').then(
module => ({default: module.CollectionEditorPage}) module => ({default: module.CollectionEditorPage})
); );
@ -92,6 +98,11 @@ export function AccountAbout() {
.then((module) => ({ default: module.AccountAbout })); .then((module) => ({ default: module.AccountAbout }));
} }
export function AccountEdit() {
return import('../../account_edit')
.then((module) => ({ default: module.AccountEdit }));
}
export function Followers () { export function Followers () {
return import('../../followers'); return import('../../followers');
} }

View File

@ -55,3 +55,7 @@ export function useAccountId() {
return accountId satisfies AccountId; return accountId satisfies AccountId;
} }
export function useCurrentAccountId() {
return useAppSelector((state) => state.meta.get('me', null) as string | null);
}

View File

@ -141,6 +141,8 @@
"account.unmute": "Не ігнараваць @{name}", "account.unmute": "Не ігнараваць @{name}",
"account.unmute_notifications_short": "Апавяшчаць", "account.unmute_notifications_short": "Апавяшчаць",
"account.unmute_short": "Не ігнараваць", "account.unmute_short": "Не ігнараваць",
"account_edit.column_button": "Гатова",
"account_edit.column_title": "Рэдагаваць профіль",
"account_note.placeholder": "Націсніце, каб дадаць нататку", "account_note.placeholder": "Націсніце, каб дадаць нататку",
"admin.dashboard.daily_retention": "Штодзённы паказчык утрымання карыстальнікаў пасля рэгістрацыі", "admin.dashboard.daily_retention": "Штодзённы паказчык утрымання карыстальнікаў пасля рэгістрацыі",
"admin.dashboard.monthly_retention": "Штомесячны паказчык утрымання карыстальнікаў пасля рэгістрацыі", "admin.dashboard.monthly_retention": "Штомесячны паказчык утрымання карыстальнікаў пасля рэгістрацыі",

View File

@ -13,6 +13,7 @@
"about.not_available": "Aquesta informació no és disponible en aquest servidor.", "about.not_available": "Aquesta informació no és disponible en aquest servidor.",
"about.powered_by": "Xarxa social descentralitzada impulsada per {mastodon}", "about.powered_by": "Xarxa social descentralitzada impulsada per {mastodon}",
"about.rules": "Normes del servidor", "about.rules": "Normes del servidor",
"account.about": "Quant a",
"account.account_note_header": "Nota personal", "account.account_note_header": "Nota personal",
"account.activity": "Activitat", "account.activity": "Activitat",
"account.add_note": "Afegeix una nota personal", "account.add_note": "Afegeix una nota personal",

View File

@ -141,6 +141,8 @@
"account.unmute": "Vis @{name} igen", "account.unmute": "Vis @{name} igen",
"account.unmute_notifications_short": "Vis notifikationer igen", "account.unmute_notifications_short": "Vis notifikationer igen",
"account.unmute_short": "Vis igen", "account.unmute_short": "Vis igen",
"account_edit.column_button": "Færdig",
"account_edit.column_title": "Rediger profil",
"account_note.placeholder": "Klik for at tilføje notat", "account_note.placeholder": "Klik for at tilføje notat",
"admin.dashboard.daily_retention": "Brugerfastholdelsesrate pr. dag efter tilmelding", "admin.dashboard.daily_retention": "Brugerfastholdelsesrate pr. dag efter tilmelding",
"admin.dashboard.monthly_retention": "Brugerfastholdelsesrate pr. måned efter tilmelding", "admin.dashboard.monthly_retention": "Brugerfastholdelsesrate pr. måned efter tilmelding",

View File

@ -141,6 +141,8 @@
"account.unmute": "Stummschaltung von @{name} aufheben", "account.unmute": "Stummschaltung von @{name} aufheben",
"account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben", "account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben",
"account.unmute_short": "Stummschaltung aufheben", "account.unmute_short": "Stummschaltung aufheben",
"account_edit.column_button": "Erledigt",
"account_edit.column_title": "Profil bearbeiten",
"account_note.placeholder": "Klicken, um private Anmerkung hinzuzufügen", "account_note.placeholder": "Klicken, um private Anmerkung hinzuzufügen",
"admin.dashboard.daily_retention": "Verweildauer der Nutzer*innen pro Tag seit der Registrierung", "admin.dashboard.daily_retention": "Verweildauer der Nutzer*innen pro Tag seit der Registrierung",
"admin.dashboard.monthly_retention": "Verweildauer der Nutzer*innen pro Monat seit der Registrierung", "admin.dashboard.monthly_retention": "Verweildauer der Nutzer*innen pro Monat seit der Registrierung",
@ -264,7 +266,7 @@
"collections.edit_details": "Allgemeine Informationen bearbeiten", "collections.edit_details": "Allgemeine Informationen bearbeiten",
"collections.edit_settings": "Einstellungen bearbeiten", "collections.edit_settings": "Einstellungen bearbeiten",
"collections.error_loading_collections": "Beim Laden deiner Sammlungen ist ein Fehler aufgetreten.", "collections.error_loading_collections": "Beim Laden deiner Sammlungen ist ein Fehler aufgetreten.",
"collections.hints.accounts_counter": "{count} / {max} Konten", "collections.hints.accounts_counter": "{count}/{max} Konten",
"collections.hints.add_more_accounts": "Füge mindestens {count, plural, one {# Konto} other {# Konten}} hinzu, um fortzufahren", "collections.hints.add_more_accounts": "Füge mindestens {count, plural, one {# Konto} other {# Konten}} hinzu, um fortzufahren",
"collections.hints.can_not_remove_more_accounts": "Sammlungen müssen mindestens {count, plural, one {# Konto} other {# Konten}} enthalten. Weitere Konten zu entfernen, ist daher nicht erlaubt.", "collections.hints.can_not_remove_more_accounts": "Sammlungen müssen mindestens {count, plural, one {# Konto} other {# Konten}} enthalten. Weitere Konten zu entfernen, ist daher nicht erlaubt.",
"collections.last_updated_at": "Aktualisiert: {date}", "collections.last_updated_at": "Aktualisiert: {date}",
@ -276,7 +278,7 @@
"collections.new_collection": "Neue Sammlung", "collections.new_collection": "Neue Sammlung",
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.", "collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
"collections.remove_account": "Dieses Konto entfernen", "collections.remove_account": "Dieses Konto entfernen",
"collections.search_accounts_label": "Konten suchen, um sie hinzuzufügen …", "collections.search_accounts_label": "Suche nach Konten, um sie hinzuzufügen …",
"collections.search_accounts_max_reached": "Du hast die Höchstzahl an Konten hinzugefügt", "collections.search_accounts_max_reached": "Du hast die Höchstzahl an Konten hinzugefügt",
"collections.topic_hint": "Ein Hashtag für diese Sammlung kann anderen dabei helfen, dein Anliegen besser einordnen zu können.", "collections.topic_hint": "Ein Hashtag für diese Sammlung kann anderen dabei helfen, dein Anliegen besser einordnen zu können.",
"collections.view_collection": "Sammlungen anzeigen", "collections.view_collection": "Sammlungen anzeigen",

View File

@ -141,6 +141,8 @@
"account.unmute": "Άρση σίγασης @{name}", "account.unmute": "Άρση σίγασης @{name}",
"account.unmute_notifications_short": "Σίγαση ειδοποιήσεων", "account.unmute_notifications_short": "Σίγαση ειδοποιήσεων",
"account.unmute_short": "Κατάργηση σίγασης", "account.unmute_short": "Κατάργηση σίγασης",
"account_edit.column_button": "Έγινε",
"account_edit.column_title": "Επεξεργασία Προφίλ",
"account_note.placeholder": "Κάνε κλικ για να προσθέσεις σημείωση", "account_note.placeholder": "Κάνε κλικ για να προσθέσεις σημείωση",
"admin.dashboard.daily_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά ημέρα", "admin.dashboard.daily_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά ημέρα",
"admin.dashboard.monthly_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά μήνα", "admin.dashboard.monthly_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά μήνα",

View File

@ -141,6 +141,8 @@
"account.unmute": "Unmute @{name}", "account.unmute": "Unmute @{name}",
"account.unmute_notifications_short": "Unmute notifications", "account.unmute_notifications_short": "Unmute notifications",
"account.unmute_short": "Unmute", "account.unmute_short": "Unmute",
"account_edit.column_button": "Done",
"account_edit.column_title": "Edit Profile",
"account_note.placeholder": "Click to add note", "account_note.placeholder": "Click to add note",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up",

View File

@ -141,6 +141,8 @@
"account.unmute": "Unmute @{name}", "account.unmute": "Unmute @{name}",
"account.unmute_notifications_short": "Unmute notifications", "account.unmute_notifications_short": "Unmute notifications",
"account.unmute_short": "Unmute", "account.unmute_short": "Unmute",
"account_edit.column_button": "Done",
"account_edit.column_title": "Edit Profile",
"account_note.placeholder": "Click to add note", "account_note.placeholder": "Click to add note",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
@ -261,6 +263,11 @@
"collections.create_collection": "Create collection", "collections.create_collection": "Create collection",
"collections.delete_collection": "Delete collection", "collections.delete_collection": "Delete collection",
"collections.description_length_hint": "100 characters limit", "collections.description_length_hint": "100 characters limit",
"collections.detail.accounts_heading": "Accounts",
"collections.detail.curated_by_author": "Curated by {author}",
"collections.detail.curated_by_you": "Curated by you",
"collections.detail.loading": "Loading collection…",
"collections.detail.share": "Share this collection",
"collections.edit_details": "Edit basic details", "collections.edit_details": "Edit basic details",
"collections.edit_settings": "Edit settings", "collections.edit_settings": "Edit settings",
"collections.error_loading_collections": "There was an error when trying to load your collections.", "collections.error_loading_collections": "There was an error when trying to load your collections.",

View File

@ -141,6 +141,8 @@
"account.unmute": "Dejar de silenciar a @{name}", "account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_notifications_short": "Dejar de silenciar notificaciones",
"account.unmute_short": "Dejar de silenciar", "account.unmute_short": "Dejar de silenciar",
"account_edit.column_button": "Listo",
"account_edit.column_title": "Editar perfil",
"account_note.placeholder": "Hacé clic par agregar una nota", "account_note.placeholder": "Hacé clic par agregar una nota",
"admin.dashboard.daily_retention": "Tasa de retención de usuarios por día, después del registro", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día, después del registro",
"admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes, después del registro", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes, después del registro",

View File

@ -72,11 +72,11 @@
"account.go_to_profile": "Ir al perfil", "account.go_to_profile": "Ir al perfil",
"account.hide_reblogs": "Ocultar impulsos de @{name}", "account.hide_reblogs": "Ocultar impulsos de @{name}",
"account.in_memoriam": "En memoria.", "account.in_memoriam": "En memoria.",
"account.joined_long": "Se unión el {date}", "account.joined_long": "Se unió el {date}",
"account.joined_short": "Se unió", "account.joined_short": "Se unió",
"account.languages": "Cambiar idiomas suscritos", "account.languages": "Cambiar idiomas suscritos",
"account.link_verified_on": "El proprietario de este enlace fue comprobado el {date}", "account.link_verified_on": "Se verificó la propiedad de este enlace el {date}",
"account.locked_info": "El estado de privacidad de esta cuenta està configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.", "account.locked_info": "El estado de privacidad de esta cuenta está configurado como bloqueado. El propietario revisa manualmente quién puede seguirlo.",
"account.media": "Multimedia", "account.media": "Multimedia",
"account.mention": "Mencionar a @{name}", "account.mention": "Mencionar a @{name}",
"account.menu.add_to_list": "Añadir a lista…", "account.menu.add_to_list": "Añadir a lista…",
@ -141,6 +141,8 @@
"account.unmute": "Dejar de silenciar a @{name}", "account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_notifications_short": "Dejar de silenciar notificaciones",
"account.unmute_short": "Dejar de silenciar", "account.unmute_short": "Dejar de silenciar",
"account_edit.column_button": "Hecho",
"account_edit.column_title": "Editar perfil",
"account_note.placeholder": "Haz clic para agregar una nota", "account_note.placeholder": "Haz clic para agregar una nota",
"admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después de unirse", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después de unirse",
"admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después de unirse", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después de unirse",
@ -296,9 +298,9 @@
"column.domain_blocks": "Dominios ocultados", "column.domain_blocks": "Dominios ocultados",
"column.edit_list": "Editar lista", "column.edit_list": "Editar lista",
"column.favourites": "Favoritos", "column.favourites": "Favoritos",
"column.firehose": "Feeds en vivo", "column.firehose": "Cronologías en vivo",
"column.firehose_local": "Feed en vivo para este servidor", "column.firehose_local": "Cronología en vivo para este servidor",
"column.firehose_singular": "Feed en vivo", "column.firehose_singular": "Cronología en vivo",
"column.follow_requests": "Solicitudes de seguimiento", "column.follow_requests": "Solicitudes de seguimiento",
"column.home": "Inicio", "column.home": "Inicio",
"column.list_members": "Administrar miembros de la lista", "column.list_members": "Administrar miembros de la lista",
@ -313,7 +315,7 @@
"column_header.moveRight_settings": "Mover columna a la derecha", "column_header.moveRight_settings": "Mover columna a la derecha",
"column_header.pin": "Fijar", "column_header.pin": "Fijar",
"column_header.show_settings": "Mostrar ajustes", "column_header.show_settings": "Mostrar ajustes",
"column_header.unpin": "Desfijar", "column_header.unpin": "Dejar de fijar",
"column_search.cancel": "Cancelar", "column_search.cancel": "Cancelar",
"combobox.close_results": "Cerrar resultados", "combobox.close_results": "Cerrar resultados",
"combobox.loading": "Cargando", "combobox.loading": "Cargando",
@ -431,7 +433,7 @@
"domain_block_modal.you_will_lose_num_followers": "Vas a perder {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} y {followingCount, plural, one {{followingCountDisplay} persona a la que sigues} other {{followingCountDisplay} personas a las que sigas}}.", "domain_block_modal.you_will_lose_num_followers": "Vas a perder {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} y {followingCount, plural, one {{followingCountDisplay} persona a la que sigues} other {{followingCountDisplay} personas a las que sigas}}.",
"domain_block_modal.you_will_lose_relationships": "Perderás todos los seguidores y las personas que sigues de este servidor.", "domain_block_modal.you_will_lose_relationships": "Perderás todos los seguidores y las personas que sigues de este servidor.",
"domain_block_modal.you_wont_see_posts": "No verás publicaciones ni notificaciones de usuarios en este servidor.", "domain_block_modal.you_wont_see_posts": "No verás publicaciones ni notificaciones de usuarios en este servidor.",
"domain_pill.activitypub_lets_connect": "Te permite conectar e interactuar con personas no sólo en Mastodon, sino también a través de diferentes aplicaciones sociales.", "domain_pill.activitypub_lets_connect": "Te permite conectarte e interactuar con personas no solo en Mastodon, sino también en diferentes aplicaciones sociales.",
"domain_pill.activitypub_like_language": "ActivityPub es como el idioma que Mastodon habla con otras redes sociales.", "domain_pill.activitypub_like_language": "ActivityPub es como el idioma que Mastodon habla con otras redes sociales.",
"domain_pill.server": "Servidor", "domain_pill.server": "Servidor",
"domain_pill.their_handle": "Su alias:", "domain_pill.their_handle": "Su alias:",
@ -475,7 +477,7 @@
"empty_column.bookmarked_statuses": "Aún no tienes ninguna publicación guardada como marcador. Cuando guardes una, se mostrará aquí.", "empty_column.bookmarked_statuses": "Aún no tienes ninguna publicación guardada como marcador. Cuando guardes una, se mostrará aquí.",
"empty_column.community": "La cronología local está vacía. ¡Escribe algo públicamente para ponerla en marcha!", "empty_column.community": "La cronología local está vacía. ¡Escribe algo públicamente para ponerla en marcha!",
"empty_column.direct": "Aún no tienes ninguna mención privada. Cuando envíes o recibas una, aparecerá aquí.", "empty_column.direct": "Aún no tienes ninguna mención privada. Cuando envíes o recibas una, aparecerá aquí.",
"empty_column.disabled_feed": "Este feed fue desactivado por los administradores de tu servidor.", "empty_column.disabled_feed": "Esta cronología fue desactivada por los administradores de tu servidor.",
"empty_column.domain_blocks": "Todavía no hay dominios ocultos.", "empty_column.domain_blocks": "Todavía no hay dominios ocultos.",
"empty_column.explore_statuses": "Nada es tendencia en este momento. ¡Revisa más tarde!", "empty_column.explore_statuses": "Nada es tendencia en este momento. ¡Revisa más tarde!",
"empty_column.favourited_statuses": "Todavía no tienes publicaciones favoritas. Cuando le des favorito a una publicación se mostrarán acá.", "empty_column.favourited_statuses": "Todavía no tienes publicaciones favoritas. Cuando le des favorito a una publicación se mostrarán acá.",
@ -506,9 +508,9 @@
"featured_carousel.header": "{count, plural,one {Publicación fijada}other {Publicaciones fijadas}}", "featured_carousel.header": "{count, plural,one {Publicación fijada}other {Publicaciones fijadas}}",
"featured_carousel.slide": "Publicación {current, number} de {max, number}", "featured_carousel.slide": "Publicación {current, number} de {max, number}",
"featured_tags.more_items": "+{count}", "featured_tags.more_items": "+{count}",
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que has accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que has accedido a esta publicación. Si deseas que la publicación también se filtre en este contexto, tendrás que editar el filtro.",
"filter_modal.added.context_mismatch_title": "¡El contexto no coincide!", "filter_modal.added.context_mismatch_title": "¡El contexto no coincide!",
"filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado, necesitaras cambiar la fecha de caducidad para que se aplique.", "filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado; deberás cambiar la fecha de caducidad para que se aplique.",
"filter_modal.added.expired_title": "¡Filtro expirado!", "filter_modal.added.expired_title": "¡Filtro expirado!",
"filter_modal.added.review_and_configure": "Para revisar y configurar esta categoría de filtros, vaya a {settings_link}.", "filter_modal.added.review_and_configure": "Para revisar y configurar esta categoría de filtros, vaya a {settings_link}.",
"filter_modal.added.review_and_configure_title": "Ajustes de filtro", "filter_modal.added.review_and_configure_title": "Ajustes de filtro",
@ -746,7 +748,7 @@
"notification.admin.report_account_other": "{name} reportó {count, plural, one {una publicación} other {# publicaciones}} de {target}", "notification.admin.report_account_other": "{name} reportó {count, plural, one {una publicación} other {# publicaciones}} de {target}",
"notification.admin.report_statuses": "{name} reportó {target} por {category}", "notification.admin.report_statuses": "{name} reportó {target} por {category}",
"notification.admin.report_statuses_other": "{name} reportó {target}", "notification.admin.report_statuses_other": "{name} reportó {target}",
"notification.admin.sign_up": "{name} se unio", "notification.admin.sign_up": "{name} se registró",
"notification.admin.sign_up.name_and_others": "{name} y {count, plural, one {# otro} other {# otros}} se registraron", "notification.admin.sign_up.name_and_others": "{name} y {count, plural, one {# otro} other {# otros}} se registraron",
"notification.annual_report.message": "¡Tu #Wrapstodon {year} te espera! ¡Desvela los momentos más destacados y memorables de tu año en Mastodon!", "notification.annual_report.message": "¡Tu #Wrapstodon {year} te espera! ¡Desvela los momentos más destacados y memorables de tu año en Mastodon!",
"notification.annual_report.view": "Ver #Wrapstodon", "notification.annual_report.view": "Ver #Wrapstodon",
@ -866,17 +868,17 @@
"onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar gente a la que seguir, o inténtalo de nuevo más tarde.", "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar gente a la que seguir, o inténtalo de nuevo más tarde.",
"onboarding.follows.search": "Buscar", "onboarding.follows.search": "Buscar",
"onboarding.follows.title": "Sigue personas para comenzar", "onboarding.follows.title": "Sigue personas para comenzar",
"onboarding.profile.discoverable": "Make my profile discoverable", "onboarding.profile.discoverable": "Hacer que mi perfil aparezca en búsquedas",
"onboarding.profile.discoverable_hint": "Cuando aceptas ser descubierto en Mastodon, tus publicaciones pueden aparecer en resultados de búsqueda y tendencias, y tu perfil puede ser sugerido a personas con intereses similares a los tuyos.", "onboarding.profile.discoverable_hint": "Cuando permites que tu perfil aparezca en búsquedas en Mastodon, tus publicaciones pueden aparecer en los resultados de búsqueda y en las tendencias, y tu perfil puede ser sugerido a personas con intereses similares a los tuyos.",
"onboarding.profile.display_name": "Nombre a mostrar", "onboarding.profile.display_name": "Nombre para mostrar",
"onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…", "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…",
"onboarding.profile.note": "Biografía", "onboarding.profile.note": "Biografía",
"onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…", "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…",
"onboarding.profile.save_and_continue": "Guardar y continuar", "onboarding.profile.save_and_continue": "Guardar y continuar",
"onboarding.profile.title": "Configuración del perfil", "onboarding.profile.title": "Configuración del perfil",
"onboarding.profile.upload_avatar": "Subir foto de perfil", "onboarding.profile.upload_avatar": "Subir foto de perfil",
"onboarding.profile.upload_header": "Subir foto de cabecera", "onboarding.profile.upload_header": "Subir encabezado de perfil",
"password_confirmation.exceeds_maxlength": "La contraseña de confirmación excede la longitud máxima de la contraseña", "password_confirmation.exceeds_maxlength": "La contraseña de confirmación supera la longitud máxima permitida",
"password_confirmation.mismatching": "La contraseña de confirmación no coincide", "password_confirmation.mismatching": "La contraseña de confirmación no coincide",
"picture_in_picture.restore": "Restaurar", "picture_in_picture.restore": "Restaurar",
"poll.closed": "Cerrada", "poll.closed": "Cerrada",
@ -892,14 +894,14 @@
"privacy.change": "Ajustar privacidad", "privacy.change": "Ajustar privacidad",
"privacy.direct.long": "Todos los mencionados en la publicación", "privacy.direct.long": "Todos los mencionados en la publicación",
"privacy.direct.short": "Mención privada", "privacy.direct.short": "Mención privada",
"privacy.private.long": "Sólo tus seguidores", "privacy.private.long": "Solo tus seguidores",
"privacy.private.short": "Seguidores", "privacy.private.short": "Seguidores",
"privacy.public.long": "Cualquiera dentro y fuera de Mastodon", "privacy.public.long": "Cualquiera dentro y fuera de Mastodon",
"privacy.public.short": "Público", "privacy.public.short": "Público",
"privacy.quote.anyone": "{visibility}, citas permitidas", "privacy.quote.anyone": "{visibility}, citas permitidas",
"privacy.quote.disabled": "{visibility}, citas desactivadas", "privacy.quote.disabled": "{visibility}, citas desactivadas",
"privacy.quote.limited": "{visibility}, citas limitadas", "privacy.quote.limited": "{visibility}, citas limitadas",
"privacy.unlisted.additional": "Esto se comporta exactamente igual que el público, excepto que el post no aparecerá en las cronologías en directo o en las etiquetas, la exploración o busquedas en Mastodon, incluso si está optado por activar la cuenta de usuario.", "privacy.unlisted.additional": "Esto funciona exactamente igual que «público», excepto que la publicación no aparecerá en las transmisiones en vivo ni en las etiquetas, en «explorar» ni en la búsqueda de Mastodon, incluso si has optado por ello en toda tu cuenta.",
"privacy.unlisted.long": "Oculto de los resultados de búsquedas, tendencias y cronologías públicas de Mastodon", "privacy.unlisted.long": "Oculto de los resultados de búsquedas, tendencias y cronologías públicas de Mastodon",
"privacy.unlisted.short": "Pública, pero discreta", "privacy.unlisted.short": "Pública, pero discreta",
"privacy_policy.last_updated": "Actualizado por última vez {date}", "privacy_policy.last_updated": "Actualizado por última vez {date}",
@ -917,7 +919,7 @@
"relative_time.days": "{number} d", "relative_time.days": "{number} d",
"relative_time.full.days": "{number, plural, one {# día} other {# días hace}}", "relative_time.full.days": "{number, plural, one {# día} other {# días hace}}",
"relative_time.full.hours": "{number, plural, one {# hora} other {# horas}} hace", "relative_time.full.hours": "{number, plural, one {# hora} other {# horas}} hace",
"relative_time.full.just_now": "justo ahora", "relative_time.full.just_now": "ahora mismo",
"relative_time.full.minutes": "Hace {number, plural, one {# minute} other {# minutos}}", "relative_time.full.minutes": "Hace {number, plural, one {# minute} other {# minutos}}",
"relative_time.full.seconds": "Hace {number, plural, one {# second} other {# segundos}}", "relative_time.full.seconds": "Hace {number, plural, one {# second} other {# segundos}}",
"relative_time.hours": "{number} h", "relative_time.hours": "{number} h",
@ -932,7 +934,7 @@
"reply_indicator.cancel": "Cancelar", "reply_indicator.cancel": "Cancelar",
"reply_indicator.poll": "Encuesta", "reply_indicator.poll": "Encuesta",
"report.block": "Bloquear", "report.block": "Bloquear",
"report.block_explanation": "No veras sus publicaciones. No podrán ver tus publicaciones ni seguirte. Podrán saber que están bloqueados.", "report.block_explanation": "No verás sus publicaciones. Ellos no podrán ver tus publicaciones ni seguirte. Podrán saber que están bloqueados.",
"report.categories.legal": "Legal", "report.categories.legal": "Legal",
"report.categories.other": "Otro", "report.categories.other": "Otro",
"report.categories.spam": "Spam", "report.categories.spam": "Spam",
@ -944,7 +946,7 @@
"report.close": "Realizado", "report.close": "Realizado",
"report.comment.title": "¿Hay algo más que creas que deberíamos saber?", "report.comment.title": "¿Hay algo más que creas que deberíamos saber?",
"report.forward": "Reenviar a {target}", "report.forward": "Reenviar a {target}",
"report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?", "report.forward_hint": "La cuenta es de otro servidor. ¿Enviar también una copia anónima del informe allí?",
"report.mute": "Silenciar", "report.mute": "Silenciar",
"report.mute_explanation": "No verás sus publicaciones. Todavía pueden seguirte y ver tus publicaciones y no sabrán que están silenciados.", "report.mute_explanation": "No verás sus publicaciones. Todavía pueden seguirte y ver tus publicaciones y no sabrán que están silenciados.",
"report.next": "Siguiente", "report.next": "Siguiente",
@ -970,7 +972,7 @@
"report.thanks.title": "¿No quieres ver esto?", "report.thanks.title": "¿No quieres ver esto?",
"report.thanks.title_actionable": "Gracias por denunciar, revisaremos esto.", "report.thanks.title_actionable": "Gracias por denunciar, revisaremos esto.",
"report.unfollow": "Dejar de seguir @{name}", "report.unfollow": "Dejar de seguir @{name}",
"report.unfollow_explanation": "Estás siguiendo esta cuenta. Para no ver sus publicaciones en tu inicio, deja de seguirla.", "report.unfollow_explanation": "Estás siguiendo esta cuenta. Para dejar de ver sus publicaciones en tu página de inicio, deja de seguirla.",
"report_notification.attached_statuses": "{count, plural, one {{count} publicación} other {{count} publicaciones}} adjunta(s)", "report_notification.attached_statuses": "{count, plural, one {{count} publicación} other {{count} publicaciones}} adjunta(s)",
"report_notification.categories.legal": "Legal", "report_notification.categories.legal": "Legal",
"report_notification.categories.legal_sentence": "contenido ilegal", "report_notification.categories.legal_sentence": "contenido ilegal",
@ -991,7 +993,7 @@
"search.quick_action.status_search": "Publicaciones que coinciden con {x}", "search.quick_action.status_search": "Publicaciones que coinciden con {x}",
"search.search_or_paste": "Buscar o pegar URL", "search.search_or_paste": "Buscar o pegar URL",
"search_popout.full_text_search_disabled_message": "No disponible en {domain}.", "search_popout.full_text_search_disabled_message": "No disponible en {domain}.",
"search_popout.full_text_search_logged_out_message": "Sólo disponible al iniciar sesión.", "search_popout.full_text_search_logged_out_message": "Solo disponible al iniciar sesión.",
"search_popout.language_code": "Código de idioma ISO", "search_popout.language_code": "Código de idioma ISO",
"search_popout.options": "Opciones de búsqueda", "search_popout.options": "Opciones de búsqueda",
"search_popout.quick_actions": "Acciones rápidas", "search_popout.quick_actions": "Acciones rápidas",
@ -1049,8 +1051,8 @@
"status.history.created": "{name} creó {date}", "status.history.created": "{name} creó {date}",
"status.history.edited": "{name} editado {date}", "status.history.edited": "{name} editado {date}",
"status.load_more": "Cargar más", "status.load_more": "Cargar más",
"status.media.open": "Click para abrir", "status.media.open": "Haz clic para abrir",
"status.media.show": "Click para mostrar", "status.media.show": "Haz clic para mostrar",
"status.media_hidden": "Contenido multimedia oculto", "status.media_hidden": "Contenido multimedia oculto",
"status.mention": "Mencionar @{name}", "status.mention": "Mencionar @{name}",
"status.more": "Más", "status.more": "Más",

View File

@ -141,6 +141,8 @@
"account.unmute": "Kumoa käyttäjän @{name} mykistys", "account.unmute": "Kumoa käyttäjän @{name} mykistys",
"account.unmute_notifications_short": "Kumoa ilmoitusten mykistys", "account.unmute_notifications_short": "Kumoa ilmoitusten mykistys",
"account.unmute_short": "Kumoa mykistys", "account.unmute_short": "Kumoa mykistys",
"account_edit.column_button": "Valmis",
"account_edit.column_title": "Muokkaa profiilia",
"account_note.placeholder": "Lisää muistiinpano napsauttamalla", "account_note.placeholder": "Lisää muistiinpano napsauttamalla",
"admin.dashboard.daily_retention": "Käyttäjien pysyvyys päivittäin rekisteröitymisen jälkeen", "admin.dashboard.daily_retention": "Käyttäjien pysyvyys päivittäin rekisteröitymisen jälkeen",
"admin.dashboard.monthly_retention": "Käyttäjien pysyvyys kuukausittain rekisteröitymisen jälkeen", "admin.dashboard.monthly_retention": "Käyttäjien pysyvyys kuukausittain rekisteröitymisen jälkeen",

View File

@ -141,6 +141,8 @@
"account.unmute": "Doyv ikki @{name}", "account.unmute": "Doyv ikki @{name}",
"account.unmute_notifications_short": "Tendra fráboðanir", "account.unmute_notifications_short": "Tendra fráboðanir",
"account.unmute_short": "Doyv ikki", "account.unmute_short": "Doyv ikki",
"account_edit.column_button": "Liðugt",
"account_edit.column_title": "Rætta vanga",
"account_note.placeholder": "Klikka fyri at leggja viðmerking afturat", "account_note.placeholder": "Klikka fyri at leggja viðmerking afturat",
"admin.dashboard.daily_retention": "Hvussu nógvir brúkarar eru eftir, síðani tey skrásettu seg, roknað í døgum", "admin.dashboard.daily_retention": "Hvussu nógvir brúkarar eru eftir, síðani tey skrásettu seg, roknað í døgum",
"admin.dashboard.monthly_retention": "Hvussu nógvir brúkarar eru eftir síðani tey skrásettu seg, roknað í mánaðum", "admin.dashboard.monthly_retention": "Hvussu nógvir brúkarar eru eftir síðani tey skrásettu seg, roknað í mánaðum",

View File

@ -141,6 +141,8 @@
"account.unmute": "Deixar de silenciar a @{name}", "account.unmute": "Deixar de silenciar a @{name}",
"account.unmute_notifications_short": "Reactivar notificacións", "account.unmute_notifications_short": "Reactivar notificacións",
"account.unmute_short": "Non silenciar", "account.unmute_short": "Non silenciar",
"account_edit.column_button": "Feito",
"account_edit.column_title": "Editar perfil",
"account_note.placeholder": "Preme para engadir nota", "account_note.placeholder": "Preme para engadir nota",
"admin.dashboard.daily_retention": "Ratio de retención de usuarias diaria após rexistrarse", "admin.dashboard.daily_retention": "Ratio de retención de usuarias diaria após rexistrarse",
"admin.dashboard.monthly_retention": "Ratio de retención de usuarias mensual após o rexistro", "admin.dashboard.monthly_retention": "Ratio de retención de usuarias mensual após o rexistro",
@ -244,9 +246,12 @@
"closed_registrations_modal.preamble": "Mastodon é descentralizado, así que non importa onde crees a conta, poderás seguir e interactuar con calquera conta deste servidor. Incluso podes ter o teu servidor!", "closed_registrations_modal.preamble": "Mastodon é descentralizado, así que non importa onde crees a conta, poderás seguir e interactuar con calquera conta deste servidor. Incluso podes ter o teu servidor!",
"closed_registrations_modal.title": "Crear conta en Mastodon", "closed_registrations_modal.title": "Crear conta en Mastodon",
"collections.account_count": "{count, plural, one {# conta} other {# contas}}", "collections.account_count": "{count, plural, one {# conta} other {# contas}}",
"collections.accounts.empty_description": "Engade ata {count} contas que segues",
"collections.accounts.empty_title": "A colección está baleira",
"collections.collection_description": "Descrición", "collections.collection_description": "Descrición",
"collections.collection_name": "Nome", "collections.collection_name": "Nome",
"collections.collection_topic": "Temática", "collections.collection_topic": "Temática",
"collections.confirm_account_removal": "Tes certeza de querer retirar esta conta desta colección?",
"collections.content_warning": "Aviso sobre o contido", "collections.content_warning": "Aviso sobre o contido",
"collections.continue": "Continuar", "collections.continue": "Continuar",
"collections.create.accounts_subtitle": "Só se poden engadir contas que segues e que optaron por ser incluídas en descubrir.", "collections.create.accounts_subtitle": "Só se poden engadir contas que segues e que optaron por ser incluídas en descubrir.",
@ -261,6 +266,9 @@
"collections.edit_details": "Editar detalles básicos", "collections.edit_details": "Editar detalles básicos",
"collections.edit_settings": "Editar axustes", "collections.edit_settings": "Editar axustes",
"collections.error_loading_collections": "Houbo un erro ao intentar cargar as túas coleccións.", "collections.error_loading_collections": "Houbo un erro ao intentar cargar as túas coleccións.",
"collections.hints.accounts_counter": "{count} / {max} contas",
"collections.hints.add_more_accounts": "Engade polo menos {count, plural, one {# conta} other {# contas}} para continuar",
"collections.hints.can_not_remove_more_accounts": "As coleccións teñen que conter polo menos {count, plural, one {# conta} other {# contas}}. Non é posible retirar máis contas.",
"collections.last_updated_at": "Última actualización: {date}", "collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Xestionar contas", "collections.manage_accounts": "Xestionar contas",
"collections.manage_accounts_in_collection": "Xestionar as contas nesta colección", "collections.manage_accounts_in_collection": "Xestionar as contas nesta colección",
@ -269,6 +277,9 @@
"collections.name_length_hint": "Límite de 100 caracteres", "collections.name_length_hint": "Límite de 100 caracteres",
"collections.new_collection": "Nova colección", "collections.new_collection": "Nova colección",
"collections.no_collections_yet": "Aínda non tes coleccións.", "collections.no_collections_yet": "Aínda non tes coleccións.",
"collections.remove_account": "Retirar esta conta",
"collections.search_accounts_label": "Buscar contas para engadir…",
"collections.search_accounts_max_reached": "Acadaches o máximo de contas permitidas",
"collections.topic_hint": "Engadir un cancelo para que axudar a que outras persoas coñezan a temática desta colección.", "collections.topic_hint": "Engadir un cancelo para que axudar a que outras persoas coñezan a temática desta colección.",
"collections.view_collection": "Ver colección", "collections.view_collection": "Ver colección",
"collections.visibility_public": "Pública", "collections.visibility_public": "Pública",

View File

@ -141,6 +141,8 @@
"account.unmute": "הפסקת השתקת @{name}", "account.unmute": "הפסקת השתקת @{name}",
"account.unmute_notifications_short": "הפעלת הודעות", "account.unmute_notifications_short": "הפעלת הודעות",
"account.unmute_short": "ביטול השתקה", "account.unmute_short": "ביטול השתקה",
"account_edit.column_button": "סיום",
"account_edit.column_title": "עריכת הפרופיל",
"account_note.placeholder": "יש ללחוץ כדי להוסיף הערות", "account_note.placeholder": "יש ללחוץ כדי להוסיף הערות",
"admin.dashboard.daily_retention": "קצב שימור משתמשים יומי אחרי ההרשמה", "admin.dashboard.daily_retention": "קצב שימור משתמשים יומי אחרי ההרשמה",
"admin.dashboard.monthly_retention": "קצב שימור משתמשים (פר חודש) אחרי ההרשמה", "admin.dashboard.monthly_retention": "קצב שימור משתמשים (פר חודש) אחרי ההרשמה",
@ -244,9 +246,12 @@
"closed_registrations_modal.preamble": "מסטודון הוא רשת מבוזרת, כך שלא משנה היכן החשבון שלך, קיימת האפשרות לעקוב ולתקשר עם משתמשים בשרת הזה. אפשר אפילו להריץ שרת בעצמך!", "closed_registrations_modal.preamble": "מסטודון הוא רשת מבוזרת, כך שלא משנה היכן החשבון שלך, קיימת האפשרות לעקוב ולתקשר עם משתמשים בשרת הזה. אפשר אפילו להריץ שרת בעצמך!",
"closed_registrations_modal.title": "להרשם למסטודון", "closed_registrations_modal.title": "להרשם למסטודון",
"collections.account_count": "{count, plural, one {חשבון אחד} other {# חשבונות}}", "collections.account_count": "{count, plural, one {חשבון אחד} other {# חשבונות}}",
"collections.accounts.empty_description": "להוסיף עד ל־{count} חשבונות שאתם עוקבים אחריהם",
"collections.accounts.empty_title": "האוסף הזה ריק",
"collections.collection_description": "תיאור", "collections.collection_description": "תיאור",
"collections.collection_name": "כינוי", "collections.collection_name": "כינוי",
"collections.collection_topic": "נושא", "collections.collection_topic": "נושא",
"collections.confirm_account_removal": "בוודאות להסיר חשבון זה מהאוסף?",
"collections.content_warning": "אזהרת תוכן", "collections.content_warning": "אזהרת תוכן",
"collections.continue": "המשך", "collections.continue": "המשך",
"collections.create.accounts_subtitle": "רק חשבונות נעקבים שבחרו להופיע ב\"תגליות\" ניתנים להוספה.", "collections.create.accounts_subtitle": "רק חשבונות נעקבים שבחרו להופיע ב\"תגליות\" ניתנים להוספה.",
@ -261,6 +266,9 @@
"collections.edit_details": "עריכת פרטים בסיסיים", "collections.edit_details": "עריכת פרטים בסיסיים",
"collections.edit_settings": "עריכת הגדרות", "collections.edit_settings": "עריכת הגדרות",
"collections.error_loading_collections": "חלה שגיאה בנסיון לטעון את אוספיך.", "collections.error_loading_collections": "חלה שגיאה בנסיון לטעון את אוספיך.",
"collections.hints.accounts_counter": "{count} \\ {max} חשבונות",
"collections.hints.add_more_accounts": "הוסיפו לפחות {count, plural,one {חשבון אחד}other {# חשבונות}} כדי להמשיך",
"collections.hints.can_not_remove_more_accounts": "אוספים חייבים להכיל לפחות {count, plural,one {חשבון אחד}other {# חשבונות}}. הסרת חשבונות נוספים איננה אפשרית.",
"collections.last_updated_at": "עדכון אחרון: {date}", "collections.last_updated_at": "עדכון אחרון: {date}",
"collections.manage_accounts": "ניהול חשבונות", "collections.manage_accounts": "ניהול חשבונות",
"collections.manage_accounts_in_collection": "ניהול החשבונות שבאוסף זה", "collections.manage_accounts_in_collection": "ניהול החשבונות שבאוסף זה",
@ -269,6 +277,9 @@
"collections.name_length_hint": "מגבלה של 100 תווים", "collections.name_length_hint": "מגבלה של 100 תווים",
"collections.new_collection": "אוסף חדש", "collections.new_collection": "אוסף חדש",
"collections.no_collections_yet": "עוד אין אוספים.", "collections.no_collections_yet": "עוד אין אוספים.",
"collections.remove_account": "הסר חשבון זה",
"collections.search_accounts_label": "לחפש חשבונות להוספה…",
"collections.search_accounts_max_reached": "הגעת למספר החשבונות המירבי",
"collections.topic_hint": "הוספת תגית שמסייעת לאחרים להבין את הנושא הראשי של האוסף.", "collections.topic_hint": "הוספת תגית שמסייעת לאחרים להבין את הנושא הראשי של האוסף.",
"collections.view_collection": "צפיה באוסף", "collections.view_collection": "צפיה באוסף",
"collections.visibility_public": "פומבי", "collections.visibility_public": "פומבי",

View File

@ -141,6 +141,8 @@
"account.unmute": "Hætta að þagga niður í @{name}", "account.unmute": "Hætta að þagga niður í @{name}",
"account.unmute_notifications_short": "Hætta að þagga í tilkynningum", "account.unmute_notifications_short": "Hætta að þagga í tilkynningum",
"account.unmute_short": "Hætta að þagga niður", "account.unmute_short": "Hætta að þagga niður",
"account_edit.column_button": "Lokið",
"account_edit.column_title": "Breyta notandasniði",
"account_note.placeholder": "Smelltu til að bæta við minnispunkti", "account_note.placeholder": "Smelltu til að bæta við minnispunkti",
"admin.dashboard.daily_retention": "Hlutfall virkra notenda eftir nýskráningu eftir dögum", "admin.dashboard.daily_retention": "Hlutfall virkra notenda eftir nýskráningu eftir dögum",
"admin.dashboard.monthly_retention": "Hlutfall virkra notenda eftir nýskráningu eftir mánuðum", "admin.dashboard.monthly_retention": "Hlutfall virkra notenda eftir nýskráningu eftir mánuðum",

View File

@ -141,6 +141,8 @@
"account.unmute": "Riattiva @{name}", "account.unmute": "Riattiva @{name}",
"account.unmute_notifications_short": "Riattiva notifiche", "account.unmute_notifications_short": "Riattiva notifiche",
"account.unmute_short": "Attiva audio", "account.unmute_short": "Attiva audio",
"account_edit.column_button": "Fatto",
"account_edit.column_title": "Modifica il profilo",
"account_note.placeholder": "Clicca per aggiungere una nota", "account_note.placeholder": "Clicca per aggiungere una nota",
"admin.dashboard.daily_retention": "Tasso di ritenzione dell'utente per giorno, dopo la registrazione", "admin.dashboard.daily_retention": "Tasso di ritenzione dell'utente per giorno, dopo la registrazione",
"admin.dashboard.monthly_retention": "Tasso di ritenzione dell'utente per mese, dopo la registrazione", "admin.dashboard.monthly_retention": "Tasso di ritenzione dell'utente per mese, dopo la registrazione",
@ -244,9 +246,12 @@
"closed_registrations_modal.preamble": "Mastodon è decentralizzato, quindi, non importa dove crei il tuo profilo, potrai seguire e interagire con chiunque su questo server. Anche se sei tu stesso a ospitarlo!", "closed_registrations_modal.preamble": "Mastodon è decentralizzato, quindi, non importa dove crei il tuo profilo, potrai seguire e interagire con chiunque su questo server. Anche se sei tu stesso a ospitarlo!",
"closed_registrations_modal.title": "Registrazione su Mastodon", "closed_registrations_modal.title": "Registrazione su Mastodon",
"collections.account_count": "{count, plural, one {# account} other {# account}}", "collections.account_count": "{count, plural, one {# account} other {# account}}",
"collections.accounts.empty_description": "Aggiungi fino a {count} account che segui",
"collections.accounts.empty_title": "Questa collezione è vuota",
"collections.collection_description": "Descrizione", "collections.collection_description": "Descrizione",
"collections.collection_name": "Nome", "collections.collection_name": "Nome",
"collections.collection_topic": "Argomento", "collections.collection_topic": "Argomento",
"collections.confirm_account_removal": "Si è sicuri di voler rimuovere questo account da questa collezione?",
"collections.content_warning": "Avviso sul contenuto", "collections.content_warning": "Avviso sul contenuto",
"collections.continue": "Continua", "collections.continue": "Continua",
"collections.create.accounts_subtitle": "Possono essere aggiunti solo gli account che segui e che hanno aderito alla funzione di scoperta.", "collections.create.accounts_subtitle": "Possono essere aggiunti solo gli account che segui e che hanno aderito alla funzione di scoperta.",
@ -261,6 +266,9 @@
"collections.edit_details": "Modifica i dettagli di base", "collections.edit_details": "Modifica i dettagli di base",
"collections.edit_settings": "Modifica impostazioni", "collections.edit_settings": "Modifica impostazioni",
"collections.error_loading_collections": "Si è verificato un errore durante il tentativo di caricare le tue collezioni.", "collections.error_loading_collections": "Si è verificato un errore durante il tentativo di caricare le tue collezioni.",
"collections.hints.accounts_counter": "{count} / {max} account",
"collections.hints.add_more_accounts": "Aggiungi almeno {count, plural, one {# account} other {# account}} per continuare",
"collections.hints.can_not_remove_more_accounts": "Le collezioni devono contenere almeno {count, plural, one {# account} other {# account}}. La rimozione di altri account, non è possibile.",
"collections.last_updated_at": "Ultimo aggiornamento: {date}", "collections.last_updated_at": "Ultimo aggiornamento: {date}",
"collections.manage_accounts": "Gestisci account", "collections.manage_accounts": "Gestisci account",
"collections.manage_accounts_in_collection": "Gestisci gli account in questa collezione", "collections.manage_accounts_in_collection": "Gestisci gli account in questa collezione",
@ -269,6 +277,9 @@
"collections.name_length_hint": "Limite di 100 caratteri", "collections.name_length_hint": "Limite di 100 caratteri",
"collections.new_collection": "Nuova collezione", "collections.new_collection": "Nuova collezione",
"collections.no_collections_yet": "Nessuna collezione ancora.", "collections.no_collections_yet": "Nessuna collezione ancora.",
"collections.remove_account": "Rimuovi questo account",
"collections.search_accounts_label": "Cerca account da aggiungere…",
"collections.search_accounts_max_reached": "Hai aggiunto il numero massimo di account",
"collections.topic_hint": "Aggiungi un hashtag che aiuti gli altri a comprendere l'argomento principale di questa collezione.", "collections.topic_hint": "Aggiungi un hashtag che aiuti gli altri a comprendere l'argomento principale di questa collezione.",
"collections.view_collection": "Visualizza la collezione", "collections.view_collection": "Visualizza la collezione",
"collections.visibility_public": "Pubblica", "collections.visibility_public": "Pubblica",

View File

@ -13,6 +13,7 @@
"about.not_available": "Denne informasjonen er ikkje gjort tilgjengeleg på denne tenaren.", "about.not_available": "Denne informasjonen er ikkje gjort tilgjengeleg på denne tenaren.",
"about.powered_by": "Desentraliserte sosiale medium drive av {mastodon}", "about.powered_by": "Desentraliserte sosiale medium drive av {mastodon}",
"about.rules": "Tenarreglar", "about.rules": "Tenarreglar",
"account.about": "Om",
"account.account_note_header": "Personleg notat", "account.account_note_header": "Personleg notat",
"account.activity": "Aktivitet", "account.activity": "Aktivitet",
"account.add_note": "Legg til eit personleg notat", "account.add_note": "Legg til eit personleg notat",
@ -87,6 +88,7 @@
"account.menu.hide_reblogs": "Gøym framhevingar på tidslina", "account.menu.hide_reblogs": "Gøym framhevingar på tidslina",
"account.menu.mention": "Omtale", "account.menu.mention": "Omtale",
"account.menu.mute": "Demp konto", "account.menu.mute": "Demp konto",
"account.menu.note.description": "Berre synleg for deg",
"account.menu.open_original_page": "Vis på {domain}", "account.menu.open_original_page": "Vis på {domain}",
"account.menu.remove_follower": "Fjern fylgjar", "account.menu.remove_follower": "Fjern fylgjar",
"account.menu.report": "Rapporter kontoen", "account.menu.report": "Rapporter kontoen",
@ -102,6 +104,13 @@
"account.muted": "Målbunden", "account.muted": "Målbunden",
"account.muting": "Dempa", "account.muting": "Dempa",
"account.mutual": "De fylgjer kvarandre", "account.mutual": "De fylgjer kvarandre",
"account.name.help.domain": "{domain} er tenaren som lagrar brukarprofilen og innlegga.",
"account.name.help.domain_self": "{domain} er tenaren som lagrar brukarprofilen og innlegga dine.",
"account.name.help.footer": "På same måten som du kan senda epost til folk med ulike epostprogram og -kontoar, kan du kommunisera med folk på andre Mastodon-tenarar, og med folk på andre sosiale nettverk som samhandlar på same måte som Mastodon. Det er ActivityPub-protokollen.",
"account.name.help.header": "Ei brukaradresse er som ei epostadresse",
"account.name.help.username": "{username} er brukarnamnet til denne kontoen på tenaren deira. Folk på andre tenarar kan ha same brukarnamnet.",
"account.name.help.username_self": "{username} er brukarnamnet ditt på denne tenaren. Folk på andre tenarar kan ha same brukarnamnet.",
"account.name_info": "Kva tyder dette?",
"account.no_bio": "Inga skildring er gjeven.", "account.no_bio": "Inga skildring er gjeven.",
"account.node_modal.callout": "Berre du kan sjå personlege notat.", "account.node_modal.callout": "Berre du kan sjå personlege notat.",
"account.node_modal.edit_title": "Rediger det personlege notatet", "account.node_modal.edit_title": "Rediger det personlege notatet",
@ -234,9 +243,13 @@
"closed_registrations_modal.find_another_server": "Finn ein annan tenar", "closed_registrations_modal.find_another_server": "Finn ein annan tenar",
"closed_registrations_modal.preamble": "Mastodon er desentralisert, så uansett kvar du opprettar ein konto, vil du kunne fylgje og samhandle med alle på denne tenaren. Du kan til og med ha din eigen tenar!", "closed_registrations_modal.preamble": "Mastodon er desentralisert, så uansett kvar du opprettar ein konto, vil du kunne fylgje og samhandle med alle på denne tenaren. Du kan til og med ha din eigen tenar!",
"closed_registrations_modal.title": "Registrer deg på Mastodon", "closed_registrations_modal.title": "Registrer deg på Mastodon",
"collections.account_count": "{count, plural, one {# konto} other {# kontoar}}",
"collections.accounts.empty_description": "Legg til opp til {count} kontoar du fylgjer",
"collections.accounts.empty_title": "Denne samlinga er tom",
"collections.collection_description": "Skildring", "collections.collection_description": "Skildring",
"collections.collection_name": "Namn", "collections.collection_name": "Namn",
"collections.collection_topic": "Emne", "collections.collection_topic": "Emne",
"collections.confirm_account_removal": "Er du sikker på at du vil fjerna denne brukarkontoen frå samlinga?",
"collections.content_warning": "Innhaldsåtvaring", "collections.content_warning": "Innhaldsåtvaring",
"collections.continue": "Hald fram", "collections.continue": "Hald fram",
"collections.create.accounts_subtitle": "Du kan berre leggja til kontoar du fylgjer og som har sagt ja til å bli oppdaga.", "collections.create.accounts_subtitle": "Du kan berre leggja til kontoar du fylgjer og som har sagt ja til å bli oppdaga.",
@ -251,6 +264,10 @@
"collections.edit_details": "Rediger grunnleggjande opplysingar", "collections.edit_details": "Rediger grunnleggjande opplysingar",
"collections.edit_settings": "Rediger innstillingar", "collections.edit_settings": "Rediger innstillingar",
"collections.error_loading_collections": "Noko gjekk gale då me prøvde å henta samlingane dine.", "collections.error_loading_collections": "Noko gjekk gale då me prøvde å henta samlingane dine.",
"collections.hints.accounts_counter": "{count} av {max} kontoar",
"collections.hints.add_more_accounts": "Legg til minst {count, plural, one {# konto} other {# kontoar}} for å halda fram",
"collections.hints.can_not_remove_more_accounts": "Samlingar må innehalda minst {count, plural, one {# konto} other {# kontoar}}. Du kan ikkje fjerna fleire kontoar.",
"collections.last_updated_at": "Sist oppdatert: {date}",
"collections.manage_accounts": "Handter kontoar", "collections.manage_accounts": "Handter kontoar",
"collections.manage_accounts_in_collection": "Handter kontoar i denne samlinga", "collections.manage_accounts_in_collection": "Handter kontoar i denne samlinga",
"collections.mark_as_sensitive": "Merk som ømtolig", "collections.mark_as_sensitive": "Merk som ømtolig",
@ -258,6 +275,9 @@
"collections.name_length_hint": "Maks 100 teikn", "collections.name_length_hint": "Maks 100 teikn",
"collections.new_collection": "Ny samling", "collections.new_collection": "Ny samling",
"collections.no_collections_yet": "Du har ingen samlingar enno.", "collections.no_collections_yet": "Du har ingen samlingar enno.",
"collections.remove_account": "Fjern denne kontoen",
"collections.search_accounts_label": "Søk etter kontoar å leggja til…",
"collections.search_accounts_max_reached": "Du har nådd grensa for kor mange kontoar du kan leggja til",
"collections.topic_hint": "Legg til ein emneknagg som hjelper andre å forstå hovudemnet for denne samlinga.", "collections.topic_hint": "Legg til ein emneknagg som hjelper andre å forstå hovudemnet for denne samlinga.",
"collections.view_collection": "Sjå samlinga", "collections.view_collection": "Sjå samlinga",
"collections.visibility_public": "Offentleg", "collections.visibility_public": "Offentleg",
@ -442,6 +462,8 @@
"emoji_button.search_results": "Søkeresultat", "emoji_button.search_results": "Søkeresultat",
"emoji_button.symbols": "Symbol", "emoji_button.symbols": "Symbol",
"emoji_button.travel": "Reise & stader", "emoji_button.travel": "Reise & stader",
"empty_column.account_about.me": "Du har ikkje skrive noko om deg sjølv enno.",
"empty_column.account_about.other": "{acct} har ikkje skrive noko om seg sjølv enno.",
"empty_column.account_featured.me": "Du har ikkje valt ut noko enno. Visste du at du kan velja ut emneknaggar du bruker mykje, og til og med venekontoar på profilen din?", "empty_column.account_featured.me": "Du har ikkje valt ut noko enno. Visste du at du kan velja ut emneknaggar du bruker mykje, og til og med venekontoar på profilen din?",
"empty_column.account_featured.other": "{acct} har ikkje valt ut noko enno. Visste du at du kan velja ut emneknaggar du bruker mykje, og til og med venekontoar på profilen din?", "empty_column.account_featured.other": "{acct} har ikkje valt ut noko enno. Visste du at du kan velja ut emneknaggar du bruker mykje, og til og med venekontoar på profilen din?",
"empty_column.account_featured_other.unknown": "Denne kontoen har ikkje valt ut noko enno.", "empty_column.account_featured_other.unknown": "Denne kontoen har ikkje valt ut noko enno.",
@ -525,6 +547,8 @@
"follow_suggestions.view_all": "Vis alle", "follow_suggestions.view_all": "Vis alle",
"follow_suggestions.who_to_follow": "Kven du kan fylgja", "follow_suggestions.who_to_follow": "Kven du kan fylgja",
"followed_tags": "Fylgde emneknaggar", "followed_tags": "Fylgde emneknaggar",
"followers.hide_other_followers": "Denne personen har valt å ikkje syna dei andre fylgjarane sine",
"following.hide_other_following": "Denne personen har valt å ikkje syna kven andre dei fylgjer",
"footer.about": "Om", "footer.about": "Om",
"footer.about_mastodon": "Om Mastodon", "footer.about_mastodon": "Om Mastodon",
"footer.about_server": "Om {domain}", "footer.about_server": "Om {domain}",

View File

@ -141,6 +141,8 @@
"account.unmute": "Ktheji zërin @{name}", "account.unmute": "Ktheji zërin @{name}",
"account.unmute_notifications_short": "Shfaqi njoftimet", "account.unmute_notifications_short": "Shfaqi njoftimet",
"account.unmute_short": "Çheshtoje", "account.unmute_short": "Çheshtoje",
"account_edit.column_button": "U bë",
"account_edit.column_title": "Përpunoni Profil",
"account_note.placeholder": "Klikoni për të shtuar shënim", "account_note.placeholder": "Klikoni për të shtuar shënim",
"admin.dashboard.daily_retention": "Shkallë mbajtjeje përdoruesi, në ditë, pas regjistrimit", "admin.dashboard.daily_retention": "Shkallë mbajtjeje përdoruesi, në ditë, pas regjistrimit",
"admin.dashboard.monthly_retention": "Shkallë mbajtjeje përdoruesi, në muaj, pas regjistrimit", "admin.dashboard.monthly_retention": "Shkallë mbajtjeje përdoruesi, në muaj, pas regjistrimit",

View File

@ -92,6 +92,8 @@
"account.unmute": "Sluta tysta @{name}", "account.unmute": "Sluta tysta @{name}",
"account.unmute_notifications_short": "Aktivera aviseringsljud", "account.unmute_notifications_short": "Aktivera aviseringsljud",
"account.unmute_short": "Avtysta", "account.unmute_short": "Avtysta",
"account_edit.column_button": "Klar",
"account_edit.column_title": "Redigera profil",
"account_note.placeholder": "Klicka för att lägga till anteckning", "account_note.placeholder": "Klicka för att lägga till anteckning",
"admin.dashboard.daily_retention": "Användarlojalitet per dag efter registrering", "admin.dashboard.daily_retention": "Användarlojalitet per dag efter registrering",
"admin.dashboard.monthly_retention": "Användarlojalitet per månad efter registrering", "admin.dashboard.monthly_retention": "Användarlojalitet per månad efter registrering",

View File

@ -244,9 +244,12 @@
"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.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", "closed_registrations_modal.title": "Mastodon'a kayıt olmak",
"collections.account_count": "{count, plural, one {# hesap} other {# hesap}}", "collections.account_count": "{count, plural, one {# hesap} other {# hesap}}",
"collections.accounts.empty_description": "Takip ettiğiniz hesapların sayısını {count} kadar artırın",
"collections.accounts.empty_title": "Bu koleksiyon boş",
"collections.collection_description": "Açıklama", "collections.collection_description": "Açıklama",
"collections.collection_name": "Ad", "collections.collection_name": "Ad",
"collections.collection_topic": "Konu", "collections.collection_topic": "Konu",
"collections.confirm_account_removal": "Bu hesabı bu koleksiyondan çıkarmak istediğinizden emin misiniz?",
"collections.content_warning": "İçerik uyarısı", "collections.content_warning": "İçerik uyarısı",
"collections.continue": "Devam et", "collections.continue": "Devam et",
"collections.create.accounts_subtitle": "Yalnızca keşif seçeneğini etkinleştirmiş takip ettiğiniz hesaplar eklenebilir.", "collections.create.accounts_subtitle": "Yalnızca keşif seçeneğini etkinleştirmiş takip ettiğiniz hesaplar eklenebilir.",
@ -261,6 +264,9 @@
"collections.edit_details": "Temel bilgileri düzenle", "collections.edit_details": "Temel bilgileri düzenle",
"collections.edit_settings": "Ayarları düzenle", "collections.edit_settings": "Ayarları düzenle",
"collections.error_loading_collections": "Koleksiyonlarınızı yüklemeye çalışırken bir hata oluştu.", "collections.error_loading_collections": "Koleksiyonlarınızı yüklemeye çalışırken bir hata oluştu.",
"collections.hints.accounts_counter": "{count} / {max} hesap",
"collections.hints.add_more_accounts": "Devam etmek için en az {count, plural, one {# hesap} other {# hesap}} ekleyin",
"collections.hints.can_not_remove_more_accounts": "Koleksiyonlar en azından {count, plural, one {# hesap} other {# hesap}} içermelidir. Daha fazla hesap çıkarmak mümkün değil.",
"collections.last_updated_at": "Son güncelleme: {date}", "collections.last_updated_at": "Son güncelleme: {date}",
"collections.manage_accounts": "Hesapları yönet", "collections.manage_accounts": "Hesapları yönet",
"collections.manage_accounts_in_collection": "Bu koleksiyondaki hesapları yönet", "collections.manage_accounts_in_collection": "Bu koleksiyondaki hesapları yönet",
@ -269,6 +275,9 @@
"collections.name_length_hint": "100 karakterle sınırlı", "collections.name_length_hint": "100 karakterle sınırlı",
"collections.new_collection": "Yeni koleksiyon", "collections.new_collection": "Yeni koleksiyon",
"collections.no_collections_yet": "Henüz hiçbir koleksiyon yok.", "collections.no_collections_yet": "Henüz hiçbir koleksiyon yok.",
"collections.remove_account": "Bu hesabı çıkar",
"collections.search_accounts_label": "Eklemek için hesap arayın…",
"collections.search_accounts_max_reached": "Maksimum hesabı eklediniz",
"collections.topic_hint": "Bu koleksiyonun ana konusunu başkalarının anlamasına yardımcı olacak bir etiket ekleyin.", "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", "collections.view_collection": "Koleksiyonu görüntüle",
"collections.visibility_public": "Herkese açık", "collections.visibility_public": "Herkese açık",

View File

@ -141,6 +141,8 @@
"account.unmute": "Bỏ phớt lờ @{name}", "account.unmute": "Bỏ phớt lờ @{name}",
"account.unmute_notifications_short": "Bỏ phớt lờ thông báo", "account.unmute_notifications_short": "Bỏ phớt lờ thông báo",
"account.unmute_short": "Bỏ phớt lờ", "account.unmute_short": "Bỏ phớt lờ",
"account_edit.column_button": "Xong",
"account_edit.column_title": "Sửa hồ sơ",
"account_note.placeholder": "Nhấn để thêm", "account_note.placeholder": "Nhấn để thêm",
"admin.dashboard.daily_retention": "Tỉ lệ người dùng sau đăng ký ở lại theo ngày", "admin.dashboard.daily_retention": "Tỉ lệ người dùng sau đăng ký ở lại theo ngày",
"admin.dashboard.monthly_retention": "Tỉ lệ người dùng ở lại sau khi đăng ký", "admin.dashboard.monthly_retention": "Tỉ lệ người dùng ở lại sau khi đăng ký",

View File

@ -141,6 +141,8 @@
"account.unmute": "解除靜音 @{name}", "account.unmute": "解除靜音 @{name}",
"account.unmute_notifications_short": "解除靜音推播通知", "account.unmute_notifications_short": "解除靜音推播通知",
"account.unmute_short": "解除靜音", "account.unmute_short": "解除靜音",
"account_edit.column_button": "完成",
"account_edit.column_title": "編輯個人檔案",
"account_note.placeholder": "點擊以新增備註", "account_note.placeholder": "點擊以新增備註",
"admin.dashboard.daily_retention": "註冊後使用者存留率(日)", "admin.dashboard.daily_retention": "註冊後使用者存留率(日)",
"admin.dashboard.monthly_retention": "註冊後使用者存留率(月)", "admin.dashboard.monthly_retention": "註冊後使用者存留率(月)",
@ -245,7 +247,7 @@
"closed_registrations_modal.title": "註冊 Mastodon", "closed_registrations_modal.title": "註冊 Mastodon",
"collections.account_count": "{count, plural, other {# 個帳號}}", "collections.account_count": "{count, plural, other {# 個帳號}}",
"collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號", "collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號",
"collections.accounts.empty_title": "此收藏名單空的", "collections.accounts.empty_title": "此收藏名單空的",
"collections.collection_description": "說明", "collections.collection_description": "說明",
"collections.collection_name": "名稱", "collections.collection_name": "名稱",
"collections.collection_topic": "主題", "collections.collection_topic": "主題",

View File

@ -14,7 +14,7 @@ import { CustomEmojiFactory } from './custom_emoji';
import type { CustomEmoji } from './custom_emoji'; import type { CustomEmoji } from './custom_emoji';
// AccountField // AccountField
interface AccountFieldShape extends Required<ApiAccountFieldJSON> { export interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
name_emojified: string; name_emojified: string;
value_emojified: string; value_emojified: string;
value_plain: string | null; value_plain: string | null;

View File

@ -18,7 +18,7 @@ export function isServerFeatureEnabled(feature: ServerFeatures) {
return initialState?.features.includes(feature) ?? false; return initialState?.features.includes(feature) ?? false;
} }
type ClientFeatures = 'collections'; type ClientFeatures = 'collections' | 'profile_editing';
export function isClientFeatureEnabled(feature: ClientFeatures) { export function isClientFeatureEnabled(feature: ClientFeatures) {
try { try {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z"/></svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z"/></svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@ -4192,7 +4192,8 @@ a.account__display-name {
cursor: pointer; cursor: pointer;
& > div { & > div {
background: rgb(from var(--color-shadow-primary) r g b / 60%); color: var(--color-text-on-media);
background: rgb(from var(--color-bg-media) r g b / 60%);
border-radius: 8px; border-radius: 8px;
padding: 12px 9px; padding: 12px 9px;
backdrop-filter: $backdrop-blur-filter; backdrop-filter: $backdrop-blur-filter;
@ -4205,7 +4206,7 @@ a.account__display-name {
button, button,
a { a {
display: inline; display: inline;
color: var(--color-text-primary); color: var(--color-text-on-media);
background: transparent; background: transparent;
border: 0; border: 0;
padding: 0 8px; padding: 0 8px;
@ -4216,7 +4217,7 @@ a.account__display-name {
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
color: var(--color-text-primary); color: var(--color-text-on-media);
} }
} }

View File

@ -46,6 +46,9 @@
# reviewed_at :datetime # reviewed_at :datetime
# sensitized_at :datetime # sensitized_at :datetime
# shared_inbox_url :string default(""), not null # shared_inbox_url :string default(""), not null
# show_featured :boolean default(TRUE), not null
# show_media :boolean default(TRUE), not null
# show_media_replies :boolean default(TRUE), not null
# silenced_at :datetime # silenced_at :datetime
# suspended_at :datetime # suspended_at :datetime
# suspension_origin :integer # suspension_origin :integer

View File

@ -8,7 +8,8 @@ class REST::AccountSerializer < ActiveModel::Serializer
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :indexable, :group, :created_at, attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :indexable, :group, :created_at,
:note, :url, :uri, :avatar, :avatar_static, :avatar_description, :header, :header_static, :header_description, :note, :url, :uri, :avatar, :avatar_static, :avatar_description, :header, :header_static, :header_description,
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections :followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections,
:show_media, :show_media_replies, :show_featured
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?

View File

@ -83,7 +83,6 @@ ar:
access_denied: لقد رفض مالك المَورِدِ أو تصريح السيرفر طلبك. access_denied: لقد رفض مالك المَورِدِ أو تصريح السيرفر طلبك.
credential_flow_not_configured: فشل تدفق بيانات اعتماد كلمة سر مالك المورد بسبب عدم تهيئة Doorkeeper.configure.resource_owner_from_credentials. credential_flow_not_configured: فشل تدفق بيانات اعتماد كلمة سر مالك المورد بسبب عدم تهيئة Doorkeeper.configure.resource_owner_from_credentials.
invalid_client: فشلت المصادقة مع العميل لأنه العميل مجهول أو لغياب المصادقة ضمن العميل أو أنّ أسلوب المصادقة غير مدعومة. invalid_client: فشلت المصادقة مع العميل لأنه العميل مجهول أو لغياب المصادقة ضمن العميل أو أنّ أسلوب المصادقة غير مدعومة.
invalid_code_challenge_method: يجب أن تكون طريقة تحدي الكود S256، البسيط غير مدعوم.
invalid_grant: إنّ التصريح المقدَّم غير صالح، سواء انتهت مدة صلاحيته أو تم إلغاؤه أو أنه لا يتطابق مع عنوان إعادة التحويل في طلب التصريح أو أنّ هذا التصريح قد تم تقديمه لعميل آخر. invalid_grant: إنّ التصريح المقدَّم غير صالح، سواء انتهت مدة صلاحيته أو تم إلغاؤه أو أنه لا يتطابق مع عنوان إعادة التحويل في طلب التصريح أو أنّ هذا التصريح قد تم تقديمه لعميل آخر.
invalid_redirect_uri: إنّ عنوان إعادة التحويل غير صالح. invalid_redirect_uri: إنّ عنوان إعادة التحويل غير صالح.
invalid_request: invalid_request:

View File

@ -83,7 +83,10 @@ be:
access_denied: Уласнік рэсурсу або сэрвер аўтарызацыі адхіліў ваш запыт. access_denied: Уласнік рэсурсу або сэрвер аўтарызацыі адхіліў ваш запыт.
credential_flow_not_configured: Resource Owner Password Credentials flow не прайшоў з-за таго, што ўласцівасць Doorkeeper.configure.resource_owner_from_credentials была не вызначана. credential_flow_not_configured: Resource Owner Password Credentials flow не прайшоў з-за таго, што ўласцівасць Doorkeeper.configure.resource_owner_from_credentials была не вызначана.
invalid_client: Збой аўтэнтыфікацыі кліента з-за невядомага кліента, адсутнасці аўтэнтыфікацыі кліента або метаду аўтэнтыфікацыі, які не падтрымліваецца. invalid_client: Збой аўтэнтыфікацыі кліента з-за невядомага кліента, адсутнасці аўтэнтыфікацыі кліента або метаду аўтэнтыфікацыі, які не падтрымліваецца.
invalid_code_challenge_method: Метад праверкі кода павінен быць S256, просты тэкст не падтрымліваецца. invalid_code_challenge_method:
one: code_challenge_method павінен быць %{challenge_methods}.
other: code_challenge_method павінен быць адным з %{challenge_methods}.
zero: Сервер аўтарызацыі не падтрымлівае PKCE, паколькі няма прынятых значэнняў у code_challenge_method.
invalid_grant: Прадастаўлены дазвол на аўтарызацыю несапраўдны, пратэрмінованы, быў адкліканы, не адпавядае URI перанакіравання, які выкарыстоўваецца ў запыце аўтарызацыі або быў выдадзены іншаму кліенту. invalid_grant: Прадастаўлены дазвол на аўтарызацыю несапраўдны, пратэрмінованы, быў адкліканы, не адпавядае URI перанакіравання, які выкарыстоўваецца ў запыце аўтарызацыі або быў выдадзены іншаму кліенту.
invalid_redirect_uri: Прадстаўлены URI перанакіравання не сапраўдны. invalid_redirect_uri: Прадстаўлены URI перанакіравання не сапраўдны.
invalid_request: invalid_request:

View File

@ -83,7 +83,6 @@ bg:
access_denied: Заявката беше отказана от собственика на ресурса или от сървъра за упълномощаване. access_denied: Заявката беше отказана от собственика на ресурса или от сървъра за упълномощаване.
credential_flow_not_configured: Resource Owner Password Credentials предизвика грешка, заради това, че настройките за Doorkeeper.configure.resource_owner_from_credentials липсват. credential_flow_not_configured: Resource Owner Password Credentials предизвика грешка, заради това, че настройките за Doorkeeper.configure.resource_owner_from_credentials липсват.
invalid_client: Удостоверяването на клиента предизвика грешка, поради непознат клиент, липсващо клиентско удостоверяване, или заради това, че методът на удостоверяване не се поддържа. invalid_client: Удостоверяването на клиента предизвика грешка, поради непознат клиент, липсващо клиентско удостоверяване, или заради това, че методът на удостоверяване не се поддържа.
invalid_code_challenge_method: Методът на предизвикателството на кода трябва да е S256, обикновен не се поддържа.
invalid_grant: Предоставеното удостоверение за достъп е невалидно, изтекло, отхвърлено, не съвпада с пренасочващото URI, използвано в заявката за удостоверение, или е бил издадено от друг клиент. invalid_grant: Предоставеното удостоверение за достъп е невалидно, изтекло, отхвърлено, не съвпада с пренасочващото URI, използвано в заявката за удостоверение, или е бил издадено от друг клиент.
invalid_redirect_uri: Включеният пренасочващ Uri е невалиден. invalid_redirect_uri: Включеният пренасочващ Uri е невалиден.
invalid_request: invalid_request:

View File

@ -83,7 +83,6 @@ ca:
access_denied: El propietari del recurs o servidor d'autorizació ha denegat la petició. access_denied: El propietari del recurs o servidor d'autorizació ha denegat la petició.
credential_flow_not_configured: Les credencials de contrasenya del propietari del recurs han fallat degut a que Doorkeeper.configure.resource_owner_from_credentials està sense configurar. credential_flow_not_configured: Les credencials de contrasenya del propietari del recurs han fallat degut a que Doorkeeper.configure.resource_owner_from_credentials està sense configurar.
invalid_client: La autentificació del client ha fallat perquè és un client desconegut o no està inclòsa l'autentificació del client o el mètode d'autenticació no està confirmat. invalid_client: La autentificació del client ha fallat perquè és un client desconegut o no està inclòsa l'autentificació del client o el mètode d'autenticació no està confirmat.
invalid_code_challenge_method: El mètode de desafiament de codi ha de ser S256, no es permet que sigui pla.
invalid_grant: La concessió d'autorizació oferta és invàlida, ha vençut, s'ha revocat, no coincideix amb l'URI de redirecció utilizada en la petició d'autorizació, o fou emesa per a un altre client. invalid_grant: La concessió d'autorizació oferta és invàlida, ha vençut, s'ha revocat, no coincideix amb l'URI de redirecció utilizada en la petició d'autorizació, o fou emesa per a un altre client.
invalid_redirect_uri: L'uri de redirecció inclòsa no és vàlida. invalid_redirect_uri: L'uri de redirecció inclòsa no és vàlida.
invalid_request: invalid_request:

View File

@ -83,7 +83,6 @@ cs:
access_denied: Vlastník zdroje či autorizační server žádost zamítl. access_denied: Vlastník zdroje či autorizační server žádost zamítl.
credential_flow_not_configured: Proud Resource Owner Password Credentials selhal, protože Doorkeeper.configure.resource_owner_from_credentials nebylo nakonfigurováno. credential_flow_not_configured: Proud Resource Owner Password Credentials selhal, protože Doorkeeper.configure.resource_owner_from_credentials nebylo nakonfigurováno.
invalid_client: Ověření klienta selhalo kvůli neznámému klientovi, chybějící klientské autentizaci či nepodporované autentizační metodě. invalid_client: Ověření klienta selhalo kvůli neznámému klientovi, chybějící klientské autentizaci či nepodporované autentizační metodě.
invalid_code_challenge_method: Metoda code challenge musí být S256, plain není podporována.
invalid_grant: Poskytnuté oprávnění je neplatné, vypršela jeho platnost, bylo odvoláno, neshoduje se s URI přesměrování použitým v požadavku o autorizaci, nebo bylo uděleno jinému klientu. invalid_grant: Poskytnuté oprávnění je neplatné, vypršela jeho platnost, bylo odvoláno, neshoduje se s URI přesměrování použitým v požadavku o autorizaci, nebo bylo uděleno jinému klientu.
invalid_redirect_uri: Zahrnutá přesměrovací URI není platná. invalid_redirect_uri: Zahrnutá přesměrovací URI není platná.
invalid_request: invalid_request:

View File

@ -83,7 +83,6 @@ cy:
access_denied: Mae perchennog yr adnodd neu'r gweinydd awdurdodi wedi atal y cais. access_denied: Mae perchennog yr adnodd neu'r gweinydd awdurdodi wedi atal y cais.
credential_flow_not_configured: Llif meini prawf cyfrinair perchennog yr adnodd wedi methu achos fod Doorkeeper.configure.resource_owner_from_credentials heb ei ffurfweddu. credential_flow_not_configured: Llif meini prawf cyfrinair perchennog yr adnodd wedi methu achos fod Doorkeeper.configure.resource_owner_from_credentials heb ei ffurfweddu.
invalid_client: Methodd dilysu cleient oherwydd cleient anhysbys, dim dilysiad cleient wedi'i gynnwys, neu ddull dilysu heb ei gefnogi. invalid_client: Methodd dilysu cleient oherwydd cleient anhysbys, dim dilysiad cleient wedi'i gynnwys, neu ddull dilysu heb ei gefnogi.
invalid_code_challenge_method: Rhaid i'r dull herio cod fod yn S256, nid oes cefnogaeth i'r plaen.
invalid_grant: Mae'r grant awdurdodi ar yr amod yn annilys, wedi dod i ben, wedi'i ddirymu, nid yw'n cyfateb i'r URI ailgyfeirio a ddefnyddiwyd yn y cais am awdurdodiad, neu wedi'i roi i gleient arall. invalid_grant: Mae'r grant awdurdodi ar yr amod yn annilys, wedi dod i ben, wedi'i ddirymu, nid yw'n cyfateb i'r URI ailgyfeirio a ddefnyddiwyd yn y cais am awdurdodiad, neu wedi'i roi i gleient arall.
invalid_redirect_uri: Nid yw'r uri ailgyfeirio a gynhwysir yn ddilys. invalid_redirect_uri: Nid yw'r uri ailgyfeirio a gynhwysir yn ddilys.
invalid_request: invalid_request:

View File

@ -83,7 +83,10 @@ da:
access_denied: Ressourceejeren eller godkendelsesserveren afviste anmodningen. access_denied: Ressourceejeren eller godkendelsesserveren afviste anmodningen.
credential_flow_not_configured: Ressourceejeradgangskodeakkreditiver-flow mislykkedes grundet ikke-opsat Doorkeeper.configure.resource_owner_from_credentials. credential_flow_not_configured: Ressourceejeradgangskodeakkreditiver-flow mislykkedes grundet ikke-opsat Doorkeeper.configure.resource_owner_from_credentials.
invalid_client: Klientgodkendelse mislykkedes grundet en ukendt klient, ingen inkluderet klientgodkendelse eller uunderstøttet godkendelsesmetode. invalid_client: Klientgodkendelse mislykkedes grundet en ukendt klient, ingen inkluderet klientgodkendelse eller uunderstøttet godkendelsesmetode.
invalid_code_challenge_method: Kodeudfordringsmetoden skal være S256, simpel (plain) er uunderstøttet. invalid_code_challenge_method:
one: code_challenge_method skal være %{challenge_methods}.
other: code_challenge_method skal være en af %{challenge_methods}.
zero: Autorisationsserveren understøtter ikke PKCE, da der ikke er nogen accepterede code_challenge_method-værdier.
invalid_grant: Den leverede godkendelse er ugyldig, udløbet, ophævet, matcher ikke omdirigerings-URI'en brugt i godkendelsesanmodningen eller er udstedt til en anden klient. invalid_grant: Den leverede godkendelse er ugyldig, udløbet, ophævet, matcher ikke omdirigerings-URI'en brugt i godkendelsesanmodningen eller er udstedt til en anden klient.
invalid_redirect_uri: Inkluderede ormdirigerings-URI er ugyldig. invalid_redirect_uri: Inkluderede ormdirigerings-URI er ugyldig.
invalid_request: invalid_request:

View File

@ -83,7 +83,6 @@ de:
access_denied: Diese Anfrage wurde von den Inhaber*innen oder durch den Autorisierungsserver abgelehnt. access_denied: Diese Anfrage wurde von den Inhaber*innen oder durch den Autorisierungsserver abgelehnt.
credential_flow_not_configured: Das Konto konnte nicht gefunden werden, da Doorkeeper.configure.resource_owner_from_credentials nicht konfiguriert ist. credential_flow_not_configured: Das Konto konnte nicht gefunden werden, da Doorkeeper.configure.resource_owner_from_credentials nicht konfiguriert ist.
invalid_client: 'Client-Authentisierung ist fehlgeschlagen: Client unbekannt, keine Authentisierung mitgeliefert oder Authentisierungsmethode wird nicht unterstützt.' invalid_client: 'Client-Authentisierung ist fehlgeschlagen: Client unbekannt, keine Authentisierung mitgeliefert oder Authentisierungsmethode wird nicht unterstützt.'
invalid_code_challenge_method: Die Code-Challenge-Methode muss „S256“ sein, „plain“ wird nicht unterstützt.
invalid_grant: Die beigefügte Autorisierung ist ungültig, abgelaufen, wurde widerrufen oder einem anderen Client ausgestellt, oder der Weiterleitungs-URI stimmt nicht mit der Autorisierungs-Anfrage überein. invalid_grant: Die beigefügte Autorisierung ist ungültig, abgelaufen, wurde widerrufen oder einem anderen Client ausgestellt, oder der Weiterleitungs-URI stimmt nicht mit der Autorisierungs-Anfrage überein.
invalid_redirect_uri: Der beigefügte Weiterleitungs-URI ist ungültig. invalid_redirect_uri: Der beigefügte Weiterleitungs-URI ist ungültig.
invalid_request: invalid_request:

View File

@ -83,7 +83,10 @@ el:
access_denied: Ο ιδιοκτήτης του πόρου ή του παρόχου έγκρισης απέρριψε το αίτημα. access_denied: Ο ιδιοκτήτης του πόρου ή του παρόχου έγκρισης απέρριψε το αίτημα.
credential_flow_not_configured: Η ροή Resource Owner Password Credentials απέτυχε επειδή το Doorkeeper.configure.resource_owner_from_credentials δεν έχει ρυθμιστεί. credential_flow_not_configured: Η ροή Resource Owner Password Credentials απέτυχε επειδή το Doorkeeper.configure.resource_owner_from_credentials δεν έχει ρυθμιστεί.
invalid_client: Η ταυτοποίηση του πελάτη απέτυχε είτε λόγω άγνωστου πελάτη, είτε λόγω έλλειψης ταυτοποιημένου πελάτη ή λόγω μη υποστηριζόμενης μεθόδου ταυτοποίησης. invalid_client: Η ταυτοποίηση του πελάτη απέτυχε είτε λόγω άγνωστου πελάτη, είτε λόγω έλλειψης ταυτοποιημένου πελάτη ή λόγω μη υποστηριζόμενης μεθόδου ταυτοποίησης.
invalid_code_challenge_method: Η μέθοδος πρόκλησης κώδικα πρέπει να είναι S256, το απλό δεν υποστηρίζεται. invalid_code_challenge_method:
one: Το code_challenge_method πρέπει να είναι %{challenge_methods}.
other: Το code_challenge_method πρέπει να είναι ένα από %{challenge_methods}.
zero: Ο διακομιστής εξουσιοδότησης δεν υποστηρίζει PKCE καθώς δεν υπάρχουν αποδεκτές τιμές code_challenge_method.
invalid_grant: Η άδεια πιστοποίησης που δόθηκε είναι άκυρη, ληγμένη, έχει ανακληθεί, δεν συμφωνεί με το URI ανακατεύθυνσης που δόθηκε στο αίτημα πιστοποίησης ή εκδόθηκε προς άλλο πελάτη. invalid_grant: Η άδεια πιστοποίησης που δόθηκε είναι άκυρη, ληγμένη, έχει ανακληθεί, δεν συμφωνεί με το URI ανακατεύθυνσης που δόθηκε στο αίτημα πιστοποίησης ή εκδόθηκε προς άλλο πελάτη.
invalid_redirect_uri: Το uri ανακατεύθυνσης που δόθηκε δεν είναι έγκυρο. invalid_redirect_uri: Το uri ανακατεύθυνσης που δόθηκε δεν είναι έγκυρο.
invalid_request: invalid_request:

View File

@ -83,7 +83,10 @@ en-GB:
access_denied: The resource owner or authorisation server denied the request. access_denied: The resource owner or authorisation server denied the request.
credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured. credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method. invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
invalid_code_challenge_method: The code challenge method must be S256, plain is unsupported. invalid_code_challenge_method:
one: The code_challenge_method must be %{challenge_methods}.
other: The code_challenge_method must be one of %{challenge_methods}.
zero: The authorisation server does not support PKCE as there are no accepted code_challenge_method values.
invalid_grant: The provided authorisation grant is invalid, expired, revoked, does not match the redirection URI used in the authorisation request, or was issued to another client. invalid_grant: The provided authorisation grant is invalid, expired, revoked, does not match the redirection URI used in the authorisation request, or was issued to another client.
invalid_redirect_uri: The redirect URI included is not valid. invalid_redirect_uri: The redirect URI included is not valid.
invalid_request: invalid_request:

View File

@ -83,7 +83,6 @@ eo:
access_denied: La posedanto de la rimedo aŭ de la rajtiga servilo rifuzis vian peton. access_denied: La posedanto de la rimedo aŭ de la rajtiga servilo rifuzis vian peton.
credential_flow_not_configured: La sendado de la identigiloj de la posedanto de la rimedo malsukcesis ĉar Doorkeeper.configure.resource_owner_from_credentials ne estis agordita. credential_flow_not_configured: La sendado de la identigiloj de la posedanto de la rimedo malsukcesis ĉar Doorkeeper.configure.resource_owner_from_credentials ne estis agordita.
invalid_client: Klienta aŭtentigo malsukcesa pro nekonata kliento, neniu klienta aŭtentigo inkluzivita, aŭ nesubtenata aŭtentiga metodo. invalid_client: Klienta aŭtentigo malsukcesa pro nekonata kliento, neniu klienta aŭtentigo inkluzivita, aŭ nesubtenata aŭtentiga metodo.
invalid_code_challenge_method: La koda defia metodo devas esti S256, ebenaĵo estas nesubtenata.
invalid_grant: La rajtiga konsento ne estas valida, ne plu estas valida, estis forigita, ne kongruas kun la plusenda URI uzita en la aŭtentiga peto, aŭ estis sendita al alia kliento. invalid_grant: La rajtiga konsento ne estas valida, ne plu estas valida, estis forigita, ne kongruas kun la plusenda URI uzita en la aŭtentiga peto, aŭ estis sendita al alia kliento.
invalid_redirect_uri: La plusenda URI uzita ne estas valida. invalid_redirect_uri: La plusenda URI uzita ne estas valida.
invalid_request: invalid_request:

View File

@ -83,7 +83,6 @@ es-AR:
access_denied: El propietario del recurso o servidor de autorización denegó la petición. access_denied: El propietario del recurso o servidor de autorización denegó la petición.
credential_flow_not_configured: Las credenciales de contraseña del propietario del recurso fallaron debido a que "Doorkeeper.configure.resource_owner_from_credentials" está sin configurar. credential_flow_not_configured: Las credenciales de contraseña del propietario del recurso fallaron debido a que "Doorkeeper.configure.resource_owner_from_credentials" está sin configurar.
invalid_client: La autenticación del cliente falló debido a que es un cliente desconocido, o no está incluída la autenticación del cliente, o el método de autenticación no está soportado. invalid_client: La autenticación del cliente falló debido a que es un cliente desconocido, o no está incluída la autenticación del cliente, o el método de autenticación no está soportado.
invalid_code_challenge_method: El método de desafío de código debe ser S256, «plain» no está soportado.
invalid_grant: La concesión de autorización ofrecida no es válida, venció, se revocó, no coincide con la dirección web de redireccionamiento usada en la petición de autorización, o fue emitida para otro cliente. invalid_grant: La concesión de autorización ofrecida no es válida, venció, se revocó, no coincide con la dirección web de redireccionamiento usada en la petición de autorización, o fue emitida para otro cliente.
invalid_redirect_uri: La dirección web de redireccionamiento incluida no es válida. invalid_redirect_uri: La dirección web de redireccionamiento incluida no es válida.
invalid_request: invalid_request:

Some files were not shown because too many files have changed in this diff Show More