Merge pull request #3409 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 3df8fb8fe9
This commit is contained in:
12
.github/renovate.json5
vendored
12
.github/renovate.json5
vendored
@@ -154,9 +154,15 @@
|
||||
groupName: 'opentelemetry-ruby (non-major)',
|
||||
},
|
||||
{
|
||||
// Group Playwright Ruby & JS deps in the same PR, as they need to be in sync
|
||||
matchManagers: ['bundler', 'npm'],
|
||||
matchPackageNames: ['playwright-ruby-client', 'playwright'],
|
||||
// The ruby portion of the Playwright group
|
||||
matchManagers: ['bundler'],
|
||||
matchPackageNames: ['playwright-ruby-client'],
|
||||
groupName: 'Playwright',
|
||||
},
|
||||
{
|
||||
// The node portion of the Playwright group
|
||||
matchManagers: ['npm'],
|
||||
matchPackageNames: ['playwright'],
|
||||
groupName: 'Playwright',
|
||||
},
|
||||
// Add labels depending on package manager
|
||||
|
||||
@@ -4,3 +4,6 @@ Layout/FirstHashElementIndentation:
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 300 # Default of 120 causes a duplicate entry in generated todo file
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
EnforcedStyle: indented
|
||||
|
||||
@@ -470,7 +470,7 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0)
|
||||
nokogiri (1.19.1)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.15)
|
||||
@@ -631,7 +631,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack (3.2.5)
|
||||
rack-attack (6.8.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (3.0.0)
|
||||
@@ -741,7 +741,7 @@ GEM
|
||||
rspec-mocks (3.13.7)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (8.0.2)
|
||||
rspec-rails (8.0.3)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
@@ -755,7 +755,7 @@ GEM
|
||||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 9)
|
||||
rspec-support (3.13.7)
|
||||
rubocop (1.84.0)
|
||||
rubocop (1.84.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
|
||||
@@ -53,9 +53,9 @@ class PublicStatusesIndex < Chewy::Index
|
||||
}
|
||||
|
||||
index_scope ::Status.unscoped
|
||||
.kept
|
||||
.indexable
|
||||
.includes(:media_attachments, :preloadable_poll, :tags, preview_cards_status: :preview_card)
|
||||
.kept
|
||||
.indexable
|
||||
.includes(:media_attachments, :preloadable_poll, :tags, preview_cards_status: :preview_card)
|
||||
|
||||
root date_detection: false do
|
||||
field(:id, type: 'long')
|
||||
|
||||
@@ -5,8 +5,8 @@ class Admin::Fasp::Debug::CallbacksController < Admin::BaseController
|
||||
authorize [:admin, :fasp, :provider], :update?
|
||||
|
||||
@callbacks = Fasp::DebugCallback
|
||||
.includes(:fasp_provider)
|
||||
.order(created_at: :desc)
|
||||
.includes(:fasp_provider)
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -18,14 +18,14 @@ class Api::V1::BlocksController < Api::BaseController
|
||||
|
||||
def paginated_blocks
|
||||
@paginated_blocks ||= Block.eager_load(target_account: [:account_stat, :user])
|
||||
.joins(:target_account)
|
||||
.merge(Account.without_suspended)
|
||||
.where(account: current_account)
|
||||
.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
.joins(:target_account)
|
||||
.merge(Account.without_suspended)
|
||||
.where(account: current_account)
|
||||
.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
end
|
||||
|
||||
def next_path
|
||||
|
||||
@@ -37,20 +37,20 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||
|
||||
def paginated_conversations
|
||||
AccountConversation.where(account: current_account)
|
||||
.includes(
|
||||
account: [:account_stat, user: :role],
|
||||
last_status: [
|
||||
:media_attachments,
|
||||
:status_stat,
|
||||
:tags,
|
||||
{
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
active_mentions: :account,
|
||||
account: [:account_stat, user: :role],
|
||||
},
|
||||
]
|
||||
)
|
||||
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
.includes(
|
||||
account: [:account_stat, user: :role],
|
||||
last_status: [
|
||||
:media_attachments,
|
||||
:status_stat,
|
||||
:tags,
|
||||
{
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
active_mentions: :account,
|
||||
account: [:account_stat, user: :role],
|
||||
},
|
||||
]
|
||||
)
|
||||
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def next_path
|
||||
|
||||
@@ -18,14 +18,14 @@ class Api::V1::MutesController < Api::BaseController
|
||||
|
||||
def paginated_mutes
|
||||
@paginated_mutes ||= Mute.eager_load(target_account: [:account_stat, :user])
|
||||
.joins(:target_account)
|
||||
.merge(Account.without_suspended)
|
||||
.where(account: current_account)
|
||||
.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
.joins(:target_account)
|
||||
.merge(Account.without_suspended)
|
||||
.where(account: current_account)
|
||||
.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
end
|
||||
|
||||
def next_path
|
||||
|
||||
@@ -72,10 +72,10 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
|
||||
def set_collections
|
||||
@collections = @account.collections
|
||||
.with_tag
|
||||
.order(created_at: :desc)
|
||||
.offset(offset_param)
|
||||
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
|
||||
.with_tag
|
||||
.order(created_at: :desc)
|
||||
.offset(offset_param)
|
||||
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
|
||||
@collections = @collections.discoverable unless @account == current_account
|
||||
end
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ export interface ApiCollectionJSON {
|
||||
account_id: string;
|
||||
|
||||
id: string;
|
||||
uri: string;
|
||||
uri: string | null;
|
||||
local: boolean;
|
||||
item_count: number;
|
||||
|
||||
name: string;
|
||||
description: string;
|
||||
tag?: ApiTagJSON;
|
||||
language: string;
|
||||
tag: ApiTagJSON | null;
|
||||
language: string | null;
|
||||
sensitive: boolean;
|
||||
discoverable: boolean;
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ export const Account: React.FC<AccountProps> = ({
|
||||
>
|
||||
<div className='account__info-wrapper'>
|
||||
<Permalink
|
||||
className='account__display-name'
|
||||
className='account__display-name focusable'
|
||||
title={account?.acct}
|
||||
href={account?.url}
|
||||
to={`/@${account?.acct}`}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Avatar: React.FC<Props> = ({
|
||||
}, [setError]);
|
||||
|
||||
const avatar = (
|
||||
<div
|
||||
<span
|
||||
className={classNames(className, 'account__avatar', {
|
||||
'account__avatar--inline': inline,
|
||||
'account__avatar--loading': loading,
|
||||
@@ -68,14 +68,14 @@ export const Avatar: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
{counter && (
|
||||
<div
|
||||
<span
|
||||
className='account__avatar__counter'
|
||||
style={{ borderColor: counterBorderColor }}
|
||||
>
|
||||
{counter}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
||||
if (withLink) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { forwardRef, useRef, useImperativeHandle } from 'react';
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { scrollTop } from 'flavours/glitch/scroll';
|
||||
|
||||
export interface ColumnRef {
|
||||
@@ -12,10 +14,11 @@ interface ColumnProps {
|
||||
children?: React.ReactNode;
|
||||
label?: string;
|
||||
bindToDocument?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Column = forwardRef<ColumnRef, ColumnProps>(
|
||||
({ children, label, bindToDocument }, ref: Ref<ColumnRef>) => {
|
||||
({ children, label, bindToDocument, className }, ref: Ref<ColumnRef>) => {
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -39,7 +42,12 @@ export const Column = forwardRef<ColumnRef, ColumnProps>(
|
||||
}));
|
||||
|
||||
return (
|
||||
<div role='region' aria-label={label} className='column' ref={nodeRef}>
|
||||
<div
|
||||
role='region'
|
||||
aria-label={label}
|
||||
className={classNames('column', className)}
|
||||
ref={nodeRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface Props {
|
||||
iconComponent?: IconProp;
|
||||
active?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
pinned?: boolean;
|
||||
multiColumn?: boolean;
|
||||
extraButton?: React.ReactNode;
|
||||
@@ -91,6 +92,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
iconComponent,
|
||||
active,
|
||||
children,
|
||||
className,
|
||||
pinned,
|
||||
multiColumn,
|
||||
extraButton,
|
||||
@@ -141,7 +143,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
onPin?.();
|
||||
}, [history, pinned, onPin]);
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
const wrapperClassName = classNames('column-header__wrapper', className, {
|
||||
active,
|
||||
});
|
||||
|
||||
@@ -256,7 +258,8 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
const hasIcon = icon && iconComponent;
|
||||
const hasTitle = hasIcon && title;
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const hasTitle = (hasIcon || backButton) && title;
|
||||
|
||||
const component = (
|
||||
<div className={wrapperClassName}>
|
||||
@@ -270,7 +273,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
className='column-header__title'
|
||||
type='button'
|
||||
>
|
||||
{!backButton && (
|
||||
{!backButton && hasIcon && (
|
||||
<Icon
|
||||
id={icon}
|
||||
icon={iconComponent}
|
||||
|
||||
@@ -311,6 +311,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
|
||||
status?: ImmutableMap<string, unknown>;
|
||||
needsStatusRefresh?: boolean;
|
||||
forceDropdown?: boolean;
|
||||
className?: string;
|
||||
renderItem?: RenderItemFn<Item>;
|
||||
renderHeader?: RenderHeaderFn<Item>;
|
||||
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,
|
||||
needsStatusRefresh,
|
||||
forceDropdown = false,
|
||||
className,
|
||||
renderItem,
|
||||
renderHeader,
|
||||
onOpen,
|
||||
@@ -434,6 +436,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
modalProps: {
|
||||
actions: items,
|
||||
onClick: handleItemClick,
|
||||
className,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -462,6 +465,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
handleClose,
|
||||
statusId,
|
||||
needsStatusRefresh,
|
||||
className,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -515,7 +519,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
popperConfig={popperConfig}
|
||||
>
|
||||
{({ props, arrowProps, placement }) => (
|
||||
<div {...props} id={menuId}>
|
||||
<div {...props} className={className} id={menuId}>
|
||||
<div className={`dropdown-animation dropdown-menu ${placement}`}>
|
||||
<div
|
||||
className={`dropdown-menu__arrow ${placement}`}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { useCallback, useEffect } from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useIdentity } from '@/flavours/glitch/identity_context';
|
||||
import { isClientFeatureEnabled } from '@/flavours/glitch/utils/environment';
|
||||
import {
|
||||
fetchRelationships,
|
||||
followAccount,
|
||||
@@ -158,14 +160,24 @@ export const FollowButton: React.FC<{
|
||||
}
|
||||
|
||||
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 (
|
||||
<a
|
||||
href='/settings/profile'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className={classNames(className, 'button button-secondary', {
|
||||
'button--compact': compact,
|
||||
})}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,10 @@ export const AccountHeader: React.FC<{
|
||||
<>
|
||||
<AccountBio
|
||||
accountId={accountId}
|
||||
className='account__header__content'
|
||||
className={classNames(
|
||||
'account__header__content',
|
||||
isRedesign && redesignClasses.bio,
|
||||
)}
|
||||
/>
|
||||
<AccountHeaderFields accountId={accountId} />
|
||||
</>
|
||||
|
||||
@@ -14,8 +14,10 @@ import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_dat
|
||||
import { Icon } from '@/flavours/glitch/components/icon';
|
||||
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
|
||||
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||
import type { Account } from '@/flavours/glitch/models/account';
|
||||
import { isValidUrl } from '@/flavours/glitch/utils/checks';
|
||||
import type {
|
||||
Account,
|
||||
AccountFieldShape,
|
||||
} from '@/flavours/glitch/models/account';
|
||||
import type { OnElementHandler } from '@/flavours/glitch/utils/html';
|
||||
import IconVerified from '@/images/icons/icon_verified.svg?react';
|
||||
|
||||
@@ -76,8 +78,8 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
|
||||
[account.emojis],
|
||||
);
|
||||
const textHasCustomEmoji = useCallback(
|
||||
(text: string) => {
|
||||
if (!emojis) {
|
||||
(text?: string | null) => {
|
||||
if (!emojis || !text) {
|
||||
return false;
|
||||
}
|
||||
for (const emoji of Object.keys(emojis)) {
|
||||
@@ -92,62 +94,96 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
|
||||
const htmlHandlers = useElementHandledLink({
|
||||
hashtagAccountId: account.id,
|
||||
});
|
||||
const intl = useIntl();
|
||||
|
||||
if (account.fields.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomEmojiProvider emojis={emojis}>
|
||||
<dl className={classes.fieldList}>
|
||||
{account.fields.map(
|
||||
(
|
||||
{ name, name_emojified, value_emojified, value_plain, verified_at },
|
||||
key,
|
||||
) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(
|
||||
classes.fieldRow,
|
||||
verified_at && classes.fieldVerified,
|
||||
)}
|
||||
>
|
||||
<FieldHTML
|
||||
as='dt'
|
||||
text={name}
|
||||
textEmojified={name_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(name)}
|
||||
titleLength={50}
|
||||
className='translate'
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
<FieldHTML
|
||||
as='dd'
|
||||
text={value_plain ?? ''}
|
||||
textEmojified={value_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
|
||||
titleLength={120}
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
{verified_at && (
|
||||
<Icon
|
||||
id='verified'
|
||||
icon={IconVerified}
|
||||
className={classes.fieldVerifiedIcon}
|
||||
aria-label={intl.formatMessage(verifyMessage, {
|
||||
date: intl.formatDate(verified_at, dateFormatOptions),
|
||||
})}
|
||||
noFill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{account.fields.map((field, key) => (
|
||||
<FieldRow
|
||||
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(
|
||||
classes.fieldRow,
|
||||
verified_at && classes.fieldVerified,
|
||||
showAll && classes.fieldShowAll,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
/* eslint-enable */
|
||||
>
|
||||
<FieldHTML
|
||||
as='dt'
|
||||
text={name}
|
||||
textEmojified={name_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(name)}
|
||||
titleLength={50}
|
||||
className='translate'
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
<dd>
|
||||
<FieldHTML
|
||||
as='span'
|
||||
text={value_plain ?? ''}
|
||||
textEmojified={value_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
|
||||
titleLength={120}
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
|
||||
{verified_at && (
|
||||
<Icon
|
||||
id='verified'
|
||||
icon={IconVerified}
|
||||
className={classes.fieldVerifiedIcon}
|
||||
aria-label={intl.formatMessage(verifyMessage, {
|
||||
date: intl.formatDate(verified_at, dateFormatOptions),
|
||||
})}
|
||||
noFill
|
||||
/>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldHTML: FC<
|
||||
{
|
||||
as: 'dd' | 'dt';
|
||||
as?: 'span' | 'dt';
|
||||
text: string;
|
||||
textEmojified: string;
|
||||
textHasCustomEmoji: boolean;
|
||||
@@ -164,11 +200,6 @@ const FieldHTML: FC<
|
||||
onElement,
|
||||
...props
|
||||
}) => {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const handleClick = useCallback(() => {
|
||||
setShowAll((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleElement: OnElementHandler = useCallback(
|
||||
(element, props, children, extra) => {
|
||||
if (element instanceof HTMLAnchorElement) {
|
||||
@@ -186,17 +217,13 @@ const FieldHTML: FC<
|
||||
},
|
||||
[onElement, textHasCustomEmoji],
|
||||
);
|
||||
|
||||
return (
|
||||
<EmojiHTML
|
||||
as={as}
|
||||
htmlString={textEmojified}
|
||||
title={showTitleOnLength(text, titleLength)}
|
||||
className={classNames(
|
||||
className,
|
||||
text && isValidUrl(text) && classes.fieldLink,
|
||||
showAll && classes.fieldShowAll,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
onElement={handleElement}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -45,6 +45,8 @@ import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
|
||||
import { isRedesignEnabled } from '../common';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const { signedIn, permissions } = useIdentity();
|
||||
@@ -89,6 +91,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
items={menuItems}
|
||||
icon='ellipsis-v'
|
||||
iconComponent={MoreHorizIcon}
|
||||
className={classes.buttonMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,11 +24,10 @@
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
font-size: 22px;
|
||||
white-space: initial;
|
||||
line-height: normal;
|
||||
|
||||
> h1 {
|
||||
font-size: 22px;
|
||||
line-height: normal;
|
||||
white-space: initial;
|
||||
}
|
||||
}
|
||||
@@ -149,11 +148,33 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
|
||||
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 {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
padding: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
:global(.account__header__badges) > & {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-weight: unset;
|
||||
@@ -194,12 +215,13 @@ svg.badgeIcon {
|
||||
|
||||
.fieldList {
|
||||
display: grid;
|
||||
grid-template-columns: 160px 1fr min-content;
|
||||
grid-template-columns: 160px 1fr;
|
||||
column-gap: 12px;
|
||||
margin: 4px 0 16px;
|
||||
margin: 16px 0;
|
||||
border-top: 0.5px solid var(--color-border-primary);
|
||||
|
||||
@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;
|
||||
align-items: start;
|
||||
grid-template-columns: subgrid;
|
||||
padding: 0 4px;
|
||||
padding: 8px;
|
||||
border-bottom: 0.5px solid var(--color-border-primary);
|
||||
|
||||
> :is(dt, dd) {
|
||||
margin: 8px 0;
|
||||
|
||||
&:not(.fieldShowAll) {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -227,43 +248,34 @@ svg.badgeIcon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&:not(.fieldVerified) > dd {
|
||||
grid-column: span 2;
|
||||
> dd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-brand);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: 0.2s ease-in-out;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--color-text-brand-soft);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fieldVerified {
|
||||
background-color: var(--color-bg-brand-softer);
|
||||
}
|
||||
|
||||
.fieldLink:is(dd, dt) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fieldLink > a {
|
||||
display: block;
|
||||
padding: 8px 0;
|
||||
background-color: var(--color-bg-success-softer);
|
||||
}
|
||||
|
||||
.fieldVerifiedIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fieldNumbersWrapper {
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
@@ -323,10 +335,15 @@ svg.badgeIcon {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 12px;
|
||||
padding: 0 24px;
|
||||
|
||||
@container (width >= 500px) {
|
||||
padding: 0 24px;
|
||||
@container (width < 500px) {
|
||||
padding: 0 12px;
|
||||
|
||||
a {
|
||||
flex: 1 1 0px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.collectionItemWrapper {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@@ -7,13 +7,13 @@
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.collectionItemContent {
|
||||
.content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
padding: 15px 5px;
|
||||
}
|
||||
|
||||
.collectionItemLink {
|
||||
.link {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
font-size: 15px;
|
||||
@@ -33,16 +33,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.collectionItemInfo {
|
||||
.info {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.metaList {
|
||||
--gap: 0.75ch;
|
||||
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
& > li:not(:first-child)::before {
|
||||
& > li:not(:last-child)::after {
|
||||
content: '·';
|
||||
margin-inline-end: var(--gap);
|
||||
margin-inline-start: var(--gap);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export const CollectionDetails: React.FC<{
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
history.push(`/collections/${id}`);
|
||||
});
|
||||
} else {
|
||||
const payload: Partial<ApiCreateCollectionPayload> = {
|
||||
|
||||
@@ -68,7 +68,7 @@ export const CollectionSettings: React.FC<{
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
history.push(`/collections/${id}`);
|
||||
});
|
||||
} else {
|
||||
const payload: ApiCreateCollectionPayload = {
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { useEffect, useMemo, useCallback, useId } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.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 { openModal } from 'flavours/glitch/actions/modal';
|
||||
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||
import { Column } from 'flavours/glitch/components/column';
|
||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||
import { Dropdown } from 'flavours/glitch/components/dropdown_menu';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
import {
|
||||
fetchAccountCollections,
|
||||
@@ -24,119 +18,13 @@ import {
|
||||
} from 'flavours/glitch/reducers/slices/collections';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
import { CollectionListItem } from './detail/collection_list_item';
|
||||
import { messages as editorMessages } from './editor';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
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<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
@@ -202,7 +90,7 @@ export const Collections: React.FC<{
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{collections.map((item) => (
|
||||
<CollectionItem key={item.id} collection={item} />
|
||||
<CollectionListItem key={item.id} collection={item} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
export const ActionsModal: React.FC<{
|
||||
actions: MenuItem[];
|
||||
onClick: React.MouseEventHandler;
|
||||
}> = ({ actions, onClick }) => (
|
||||
<div className='modal-root__modal actions-modal'>
|
||||
className?: string;
|
||||
}> = ({ actions, onClick, className }) => (
|
||||
<div className={classNames('modal-root__modal actions-modal', className)}>
|
||||
<ul>
|
||||
{actions.map((option, i: number) => {
|
||||
if (option === null) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
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 { clearHeight } from '../../actions/height_cache';
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
ListEdit,
|
||||
ListMembers,
|
||||
Collections,
|
||||
CollectionDetail,
|
||||
CollectionsEditor,
|
||||
Blocks,
|
||||
DomainBlocks,
|
||||
@@ -83,6 +84,7 @@ import {
|
||||
TermsOfService,
|
||||
AccountFeatured,
|
||||
AccountAbout,
|
||||
AccountEdit,
|
||||
Quotes,
|
||||
} from './util/async-components';
|
||||
import { ColumnsContextProvider } from './util/columns_context';
|
||||
@@ -240,6 +242,8 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} 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/follows' component={OnboardingFollows} 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='/lists' component={Lists} content={children} />
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />
|
||||
[
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />,
|
||||
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} />,
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||
]
|
||||
}
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||
}
|
||||
|
||||
<Route component={BundleColumnError} />
|
||||
</WrappedSwitch>
|
||||
</ColumnsAreaContainer>
|
||||
|
||||
@@ -44,13 +44,19 @@ export function DirectTimeline() {
|
||||
return import('../../direct_timeline');
|
||||
}
|
||||
|
||||
export function Collections () {
|
||||
export function Collections() {
|
||||
return import('../../collections').then(
|
||||
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(
|
||||
module => ({default: module.CollectionEditorPage})
|
||||
);
|
||||
@@ -92,6 +98,11 @@ export function AccountAbout() {
|
||||
.then((module) => ({ default: module.AccountAbout }));
|
||||
}
|
||||
|
||||
export function AccountEdit() {
|
||||
return import('../../account_edit')
|
||||
.then((module) => ({ default: module.AccountEdit }));
|
||||
}
|
||||
|
||||
export function Followers () {
|
||||
return import('../../followers');
|
||||
}
|
||||
|
||||
@@ -55,3 +55,7 @@ export function useAccountId() {
|
||||
|
||||
return accountId satisfies AccountId;
|
||||
}
|
||||
|
||||
export function useCurrentAccountId() {
|
||||
return useAppSelector((state) => state.meta.get('me', null) as string | null);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { CustomEmojiFactory } from './custom_emoji';
|
||||
import type { CustomEmoji } from './custom_emoji';
|
||||
|
||||
// AccountField
|
||||
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||
export interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||
name_emojified: string;
|
||||
value_emojified: string;
|
||||
value_plain: string | null;
|
||||
|
||||
@@ -4280,7 +4280,8 @@ input.glitch-setting-text {
|
||||
cursor: pointer;
|
||||
|
||||
& > 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;
|
||||
padding: 12px 9px;
|
||||
backdrop-filter: $backdrop-blur-filter;
|
||||
@@ -4293,7 +4294,7 @@ input.glitch-setting-text {
|
||||
button,
|
||||
a {
|
||||
display: inline;
|
||||
color: var(--color-text-primary);
|
||||
color: var(--color-text-on-media);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0 8px;
|
||||
@@ -4304,7 +4305,7 @@ input.glitch-setting-text {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: var(--color-text-primary);
|
||||
color: var(--color-text-on-media);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function isServerFeatureEnabled(feature: ServerFeatures) {
|
||||
return initialState?.features.includes(feature) ?? false;
|
||||
}
|
||||
|
||||
type ClientFeatures = 'collections';
|
||||
type ClientFeatures = 'collections' | 'profile_editing';
|
||||
|
||||
export function isClientFeatureEnabled(feature: ClientFeatures) {
|
||||
try {
|
||||
|
||||
@@ -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 stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="m6 8 1.333 1.333L10 6.667"/>
|
||||
<defs>
|
||||
<linearGradient id="VerifiedGradient" x1="-.966" x2="12.162" y1="2.629" y2="17.493" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".13" stop-color="#5638CC"/>
|
||||
<stop offset=".995" stop-color="#DC03F0"/>
|
||||
<linearGradient id="VerifiedGradient" x1="7.99512" y1="0.836914" x2="7.99512" y2="15.1689" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00BC7D"/>
|
||||
<stop offset="1" stop-color="#00BBA7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 921 B |
@@ -11,14 +11,14 @@ export interface ApiCollectionJSON {
|
||||
account_id: string;
|
||||
|
||||
id: string;
|
||||
uri: string;
|
||||
uri: string | null;
|
||||
local: boolean;
|
||||
item_count: number;
|
||||
|
||||
name: string;
|
||||
description: string;
|
||||
tag?: ApiTagJSON;
|
||||
language: string;
|
||||
tag: ApiTagJSON | null;
|
||||
language: string | null;
|
||||
sensitive: boolean;
|
||||
discoverable: boolean;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
|
||||
<div
|
||||
<span
|
||||
className="account__avatar account__avatar--loading"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
@@ -18,11 +18,11 @@ exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
|
||||
onLoad={[Function]}
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`<Avatar /> > Still > renders a still avatar 1`] = `
|
||||
<div
|
||||
<span
|
||||
className="account__avatar account__avatar--loading"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
@@ -39,5 +39,5 @@ exports[`<Avatar /> > Still > renders a still avatar 1`] = `
|
||||
onLoad={[Function]}
|
||||
src="/static/alice.jpg"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@@ -297,7 +297,7 @@ export const Account: React.FC<AccountProps> = ({
|
||||
>
|
||||
<div className='account__info-wrapper'>
|
||||
<Link
|
||||
className='account__display-name'
|
||||
className='account__display-name focusable'
|
||||
title={account?.acct}
|
||||
to={`/@${account?.acct}`}
|
||||
data-hover-card-account={id}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Avatar: React.FC<Props> = ({
|
||||
}, [setError]);
|
||||
|
||||
const avatar = (
|
||||
<div
|
||||
<span
|
||||
className={classNames(className, 'account__avatar', {
|
||||
'account__avatar--inline': inline,
|
||||
'account__avatar--loading': loading,
|
||||
@@ -67,14 +67,14 @@ export const Avatar: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
{counter && (
|
||||
<div
|
||||
<span
|
||||
className='account__avatar__counter'
|
||||
style={{ borderColor: counterBorderColor }}
|
||||
>
|
||||
{counter}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
||||
if (withLink) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { forwardRef, useRef, useImperativeHandle } from 'react';
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { scrollTop } from 'mastodon/scroll';
|
||||
|
||||
export interface ColumnRef {
|
||||
@@ -12,10 +14,11 @@ interface ColumnProps {
|
||||
children?: React.ReactNode;
|
||||
label?: string;
|
||||
bindToDocument?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Column = forwardRef<ColumnRef, ColumnProps>(
|
||||
({ children, label, bindToDocument }, ref: Ref<ColumnRef>) => {
|
||||
({ children, label, bindToDocument, className }, ref: Ref<ColumnRef>) => {
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -39,7 +42,12 @@ export const Column = forwardRef<ColumnRef, ColumnProps>(
|
||||
}));
|
||||
|
||||
return (
|
||||
<div role='region' aria-label={label} className='column' ref={nodeRef}>
|
||||
<div
|
||||
role='region'
|
||||
aria-label={label}
|
||||
className={classNames('column', className)}
|
||||
ref={nodeRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface Props {
|
||||
iconComponent?: IconProp;
|
||||
active?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
pinned?: boolean;
|
||||
multiColumn?: boolean;
|
||||
extraButton?: React.ReactNode;
|
||||
@@ -91,6 +92,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
iconComponent,
|
||||
active,
|
||||
children,
|
||||
className,
|
||||
pinned,
|
||||
multiColumn,
|
||||
extraButton,
|
||||
@@ -141,7 +143,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
onPin?.();
|
||||
}, [history, pinned, onPin]);
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
const wrapperClassName = classNames('column-header__wrapper', className, {
|
||||
active,
|
||||
});
|
||||
|
||||
@@ -256,7 +258,8 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
const hasIcon = icon && iconComponent;
|
||||
const hasTitle = hasIcon && title;
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const hasTitle = (hasIcon || backButton) && title;
|
||||
|
||||
const component = (
|
||||
<div className={wrapperClassName}>
|
||||
@@ -270,7 +273,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
className='column-header__title'
|
||||
type='button'
|
||||
>
|
||||
{!backButton && (
|
||||
{!backButton && hasIcon && (
|
||||
<Icon
|
||||
id={icon}
|
||||
icon={iconComponent}
|
||||
|
||||
@@ -311,6 +311,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
|
||||
status?: ImmutableMap<string, unknown>;
|
||||
needsStatusRefresh?: boolean;
|
||||
forceDropdown?: boolean;
|
||||
className?: string;
|
||||
renderItem?: RenderItemFn<Item>;
|
||||
renderHeader?: RenderHeaderFn<Item>;
|
||||
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,
|
||||
needsStatusRefresh,
|
||||
forceDropdown = false,
|
||||
className,
|
||||
renderItem,
|
||||
renderHeader,
|
||||
onOpen,
|
||||
@@ -434,6 +436,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
modalProps: {
|
||||
actions: items,
|
||||
onClick: handleItemClick,
|
||||
className,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -462,6 +465,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
handleClose,
|
||||
statusId,
|
||||
needsStatusRefresh,
|
||||
className,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -515,7 +519,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
popperConfig={popperConfig}
|
||||
>
|
||||
{({ props, arrowProps, placement }) => (
|
||||
<div {...props} id={menuId}>
|
||||
<div {...props} className={className} id={menuId}>
|
||||
<div className={`dropdown-animation dropdown-menu ${placement}`}>
|
||||
<div
|
||||
className={`dropdown-menu__arrow ${placement}`}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { useCallback, useEffect } from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useIdentity } from '@/mastodon/identity_context';
|
||||
import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
|
||||
import {
|
||||
fetchRelationships,
|
||||
followAccount,
|
||||
@@ -158,14 +160,24 @@ export const FollowButton: React.FC<{
|
||||
}
|
||||
|
||||
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 (
|
||||
<a
|
||||
href='/settings/profile'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className={classNames(className, 'button button-secondary', {
|
||||
'button--compact': compact,
|
||||
})}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
|
||||
53
app/javascript/mastodon/features/account_edit/index.tsx
Normal file
53
app/javascript/mastodon/features/account_edit/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,10 @@ export const AccountHeader: React.FC<{
|
||||
<>
|
||||
<AccountBio
|
||||
accountId={accountId}
|
||||
className='account__header__content'
|
||||
className={classNames(
|
||||
'account__header__content',
|
||||
isRedesign && redesignClasses.bio,
|
||||
)}
|
||||
/>
|
||||
<AccountHeaderFields accountId={accountId} />
|
||||
</>
|
||||
|
||||
@@ -15,8 +15,7 @@ import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import { isValidUrl } from '@/mastodon/utils/checks';
|
||||
import type { Account, AccountFieldShape } from '@/mastodon/models/account';
|
||||
import type { OnElementHandler } from '@/mastodon/utils/html';
|
||||
|
||||
import { cleanExtraEmojis } from '../../emoji/normalize';
|
||||
@@ -76,8 +75,8 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
|
||||
[account.emojis],
|
||||
);
|
||||
const textHasCustomEmoji = useCallback(
|
||||
(text: string) => {
|
||||
if (!emojis) {
|
||||
(text?: string | null) => {
|
||||
if (!emojis || !text) {
|
||||
return false;
|
||||
}
|
||||
for (const emoji of Object.keys(emojis)) {
|
||||
@@ -92,62 +91,96 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
|
||||
const htmlHandlers = useElementHandledLink({
|
||||
hashtagAccountId: account.id,
|
||||
});
|
||||
const intl = useIntl();
|
||||
|
||||
if (account.fields.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomEmojiProvider emojis={emojis}>
|
||||
<dl className={classes.fieldList}>
|
||||
{account.fields.map(
|
||||
(
|
||||
{ name, name_emojified, value_emojified, value_plain, verified_at },
|
||||
key,
|
||||
) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(
|
||||
classes.fieldRow,
|
||||
verified_at && classes.fieldVerified,
|
||||
)}
|
||||
>
|
||||
<FieldHTML
|
||||
as='dt'
|
||||
text={name}
|
||||
textEmojified={name_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(name)}
|
||||
titleLength={50}
|
||||
className='translate'
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
<FieldHTML
|
||||
as='dd'
|
||||
text={value_plain ?? ''}
|
||||
textEmojified={value_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
|
||||
titleLength={120}
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
{verified_at && (
|
||||
<Icon
|
||||
id='verified'
|
||||
icon={IconVerified}
|
||||
className={classes.fieldVerifiedIcon}
|
||||
aria-label={intl.formatMessage(verifyMessage, {
|
||||
date: intl.formatDate(verified_at, dateFormatOptions),
|
||||
})}
|
||||
noFill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{account.fields.map((field, key) => (
|
||||
<FieldRow
|
||||
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(
|
||||
classes.fieldRow,
|
||||
verified_at && classes.fieldVerified,
|
||||
showAll && classes.fieldShowAll,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
/* eslint-enable */
|
||||
>
|
||||
<FieldHTML
|
||||
as='dt'
|
||||
text={name}
|
||||
textEmojified={name_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(name)}
|
||||
titleLength={50}
|
||||
className='translate'
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
<dd>
|
||||
<FieldHTML
|
||||
as='span'
|
||||
text={value_plain ?? ''}
|
||||
textEmojified={value_emojified}
|
||||
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
|
||||
titleLength={120}
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
|
||||
{verified_at && (
|
||||
<Icon
|
||||
id='verified'
|
||||
icon={IconVerified}
|
||||
className={classes.fieldVerifiedIcon}
|
||||
aria-label={intl.formatMessage(verifyMessage, {
|
||||
date: intl.formatDate(verified_at, dateFormatOptions),
|
||||
})}
|
||||
noFill
|
||||
/>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldHTML: FC<
|
||||
{
|
||||
as: 'dd' | 'dt';
|
||||
as?: 'span' | 'dt';
|
||||
text: string;
|
||||
textEmojified: string;
|
||||
textHasCustomEmoji: boolean;
|
||||
@@ -164,11 +197,6 @@ const FieldHTML: FC<
|
||||
onElement,
|
||||
...props
|
||||
}) => {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const handleClick = useCallback(() => {
|
||||
setShowAll((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleElement: OnElementHandler = useCallback(
|
||||
(element, props, children, extra) => {
|
||||
if (element instanceof HTMLAnchorElement) {
|
||||
@@ -186,17 +214,13 @@ const FieldHTML: FC<
|
||||
},
|
||||
[onElement, textHasCustomEmoji],
|
||||
);
|
||||
|
||||
return (
|
||||
<EmojiHTML
|
||||
as={as}
|
||||
htmlString={textEmojified}
|
||||
title={showTitleOnLength(text, titleLength)}
|
||||
className={classNames(
|
||||
className,
|
||||
text && isValidUrl(text) && classes.fieldLink,
|
||||
showAll && classes.fieldShowAll,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
onElement={handleElement}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -42,6 +42,8 @@ import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
|
||||
import { isRedesignEnabled } from '../common';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const { signedIn, permissions } = useIdentity();
|
||||
@@ -86,6 +88,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
items={menuItems}
|
||||
icon='ellipsis-v'
|
||||
iconComponent={MoreHorizIcon}
|
||||
className={classes.buttonMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,11 +24,10 @@
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
font-size: 22px;
|
||||
white-space: initial;
|
||||
line-height: normal;
|
||||
|
||||
> h1 {
|
||||
font-size: 22px;
|
||||
line-height: normal;
|
||||
white-space: initial;
|
||||
}
|
||||
}
|
||||
@@ -149,11 +148,33 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
|
||||
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 {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
padding: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
:global(.account__header__badges) > & {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-weight: unset;
|
||||
@@ -194,12 +215,13 @@ svg.badgeIcon {
|
||||
|
||||
.fieldList {
|
||||
display: grid;
|
||||
grid-template-columns: 160px 1fr min-content;
|
||||
grid-template-columns: 160px 1fr;
|
||||
column-gap: 12px;
|
||||
margin: 4px 0 16px;
|
||||
margin: 16px 0;
|
||||
border-top: 0.5px solid var(--color-border-primary);
|
||||
|
||||
@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;
|
||||
align-items: start;
|
||||
grid-template-columns: subgrid;
|
||||
padding: 0 4px;
|
||||
padding: 8px;
|
||||
border-bottom: 0.5px solid var(--color-border-primary);
|
||||
|
||||
> :is(dt, dd) {
|
||||
margin: 8px 0;
|
||||
|
||||
&:not(.fieldShowAll) {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -227,43 +248,34 @@ svg.badgeIcon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&:not(.fieldVerified) > dd {
|
||||
grid-column: span 2;
|
||||
> dd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-brand);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: 0.2s ease-in-out;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--color-text-brand-soft);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fieldVerified {
|
||||
background-color: var(--color-bg-brand-softer);
|
||||
}
|
||||
|
||||
.fieldLink:is(dd, dt) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fieldLink > a {
|
||||
display: block;
|
||||
padding: 8px 0;
|
||||
background-color: var(--color-bg-success-softer);
|
||||
}
|
||||
|
||||
.fieldVerifiedIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fieldNumbersWrapper {
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
@@ -323,10 +335,15 @@ svg.badgeIcon {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 0 12px;
|
||||
padding: 0 24px;
|
||||
|
||||
@container (width >= 500px) {
|
||||
padding: 0 24px;
|
||||
@container (width < 500px) {
|
||||
padding: 0 12px;
|
||||
|
||||
a {
|
||||
flex: 1 1 0px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.collectionItemWrapper {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@@ -7,13 +7,13 @@
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.collectionItemContent {
|
||||
.content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
padding: 15px 5px;
|
||||
}
|
||||
|
||||
.collectionItemLink {
|
||||
.link {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
font-size: 15px;
|
||||
@@ -33,16 +33,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.collectionItemInfo {
|
||||
.info {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.metaList {
|
||||
--gap: 0.75ch;
|
||||
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
& > li:not(:first-child)::before {
|
||||
& > li:not(:last-child)::after {
|
||||
content: '·';
|
||||
margin-inline-end: var(--gap);
|
||||
margin-inline-start: var(--gap);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
178
app/javascript/mastodon/features/collections/detail/index.tsx
Normal file
178
app/javascript/mastodon/features/collections/detail/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export const CollectionDetails: React.FC<{
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
history.push(`/collections/${id}`);
|
||||
});
|
||||
} else {
|
||||
const payload: Partial<ApiCreateCollectionPayload> = {
|
||||
|
||||
@@ -68,7 +68,7 @@ export const CollectionSettings: React.FC<{
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
history.push(`/collections/${id}`);
|
||||
});
|
||||
} else {
|
||||
const payload: ApiCreateCollectionPayload = {
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { useEffect, useMemo, useCallback, useId } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.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 { openModal } from 'mastodon/actions/modal';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import {
|
||||
fetchAccountCollections,
|
||||
@@ -24,119 +18,13 @@ import {
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { CollectionListItem } from './detail/collection_list_item';
|
||||
import { messages as editorMessages } from './editor';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
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<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
@@ -202,7 +90,7 @@ export const Collections: React.FC<{
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{collections.map((item) => (
|
||||
<CollectionItem key={item.id} collection={item} />
|
||||
<CollectionListItem key={item.id} collection={item} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
export const ActionsModal: React.FC<{
|
||||
actions: MenuItem[];
|
||||
onClick: React.MouseEventHandler;
|
||||
}> = ({ actions, onClick }) => (
|
||||
<div className='modal-root__modal actions-modal'>
|
||||
className?: string;
|
||||
}> = ({ actions, onClick, className }) => (
|
||||
<div className={classNames('modal-root__modal actions-modal', className)}>
|
||||
<ul>
|
||||
{actions.map((option, i: number) => {
|
||||
if (option === null) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
|
||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
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 { clearHeight } from '../../actions/height_cache';
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
ListEdit,
|
||||
ListMembers,
|
||||
Collections,
|
||||
CollectionDetail,
|
||||
CollectionsEditor,
|
||||
Blocks,
|
||||
DomainBlocks,
|
||||
@@ -80,6 +81,7 @@ import {
|
||||
TermsOfService,
|
||||
AccountFeatured,
|
||||
AccountAbout,
|
||||
AccountEdit,
|
||||
Quotes,
|
||||
} from './util/async-components';
|
||||
import { ColumnsContextProvider } from './util/columns_context';
|
||||
@@ -232,6 +234,8 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} 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/follows' component={OnboardingFollows} 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='/lists' component={Lists} content={children} />
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />
|
||||
[
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />,
|
||||
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} />,
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||
]
|
||||
}
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||
}
|
||||
|
||||
<Route component={BundleColumnError} />
|
||||
</WrappedSwitch>
|
||||
</ColumnsAreaContainer>
|
||||
|
||||
@@ -44,13 +44,19 @@ export function Lists () {
|
||||
return import('../../lists');
|
||||
}
|
||||
|
||||
export function Collections () {
|
||||
export function Collections() {
|
||||
return import('../../collections').then(
|
||||
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(
|
||||
module => ({default: module.CollectionEditorPage})
|
||||
);
|
||||
@@ -92,6 +98,11 @@ export function AccountAbout() {
|
||||
.then((module) => ({ default: module.AccountAbout }));
|
||||
}
|
||||
|
||||
export function AccountEdit() {
|
||||
return import('../../account_edit')
|
||||
.then((module) => ({ default: module.AccountEdit }));
|
||||
}
|
||||
|
||||
export function Followers () {
|
||||
return import('../../followers');
|
||||
}
|
||||
|
||||
@@ -55,3 +55,7 @@ export function useAccountId() {
|
||||
|
||||
return accountId satisfies AccountId;
|
||||
}
|
||||
|
||||
export function useCurrentAccountId() {
|
||||
return useAppSelector((state) => state.meta.get('me', null) as string | null);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Не ігнараваць @{name}",
|
||||
"account.unmute_notifications_short": "Апавяшчаць",
|
||||
"account.unmute_short": "Не ігнараваць",
|
||||
"account_edit.column_button": "Гатова",
|
||||
"account_edit.column_title": "Рэдагаваць профіль",
|
||||
"account_note.placeholder": "Націсніце, каб дадаць нататку",
|
||||
"admin.dashboard.daily_retention": "Штодзённы паказчык утрымання карыстальнікаў пасля рэгістрацыі",
|
||||
"admin.dashboard.monthly_retention": "Штомесячны паказчык утрымання карыстальнікаў пасля рэгістрацыі",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"about.not_available": "Aquesta informació no és disponible en aquest servidor.",
|
||||
"about.powered_by": "Xarxa social descentralitzada impulsada per {mastodon}",
|
||||
"about.rules": "Normes del servidor",
|
||||
"account.about": "Quant a",
|
||||
"account.account_note_header": "Nota personal",
|
||||
"account.activity": "Activitat",
|
||||
"account.add_note": "Afegeix una nota personal",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Vis @{name} igen",
|
||||
"account.unmute_notifications_short": "Vis notifikationer 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",
|
||||
"admin.dashboard.daily_retention": "Brugerfastholdelsesrate pr. dag efter tilmelding",
|
||||
"admin.dashboard.monthly_retention": "Brugerfastholdelsesrate pr. måned efter tilmelding",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Stummschaltung von @{name} aufheben",
|
||||
"account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen 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",
|
||||
"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",
|
||||
@@ -264,7 +266,7 @@
|
||||
"collections.edit_details": "Allgemeine Informationen bearbeiten",
|
||||
"collections.edit_settings": "Einstellungen bearbeiten",
|
||||
"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.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}",
|
||||
@@ -276,7 +278,7 @@
|
||||
"collections.new_collection": "Neue Sammlung",
|
||||
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
|
||||
"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.topic_hint": "Ein Hashtag für diese Sammlung kann anderen dabei helfen, dein Anliegen besser einordnen zu können.",
|
||||
"collections.view_collection": "Sammlungen anzeigen",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Άρση σίγασης @{name}",
|
||||
"account.unmute_notifications_short": "Σίγαση ειδοποιήσεων",
|
||||
"account.unmute_short": "Κατάργηση σίγασης",
|
||||
"account_edit.column_button": "Έγινε",
|
||||
"account_edit.column_title": "Επεξεργασία Προφίλ",
|
||||
"account_note.placeholder": "Κάνε κλικ για να προσθέσεις σημείωση",
|
||||
"admin.dashboard.daily_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά ημέρα",
|
||||
"admin.dashboard.monthly_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά μήνα",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Unmute @{name}",
|
||||
"account.unmute_notifications_short": "Unmute notifications",
|
||||
"account.unmute_short": "Unmute",
|
||||
"account_edit.column_button": "Done",
|
||||
"account_edit.column_title": "Edit Profile",
|
||||
"account_note.placeholder": "Click to add note",
|
||||
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Unmute @{name}",
|
||||
"account.unmute_notifications_short": "Unmute notifications",
|
||||
"account.unmute_short": "Unmute",
|
||||
"account_edit.column_button": "Done",
|
||||
"account_edit.column_title": "Edit Profile",
|
||||
"account_note.placeholder": "Click to add note",
|
||||
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
|
||||
@@ -261,6 +263,11 @@
|
||||
"collections.create_collection": "Create collection",
|
||||
"collections.delete_collection": "Delete collection",
|
||||
"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_settings": "Edit settings",
|
||||
"collections.error_loading_collections": "There was an error when trying to load your collections.",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Dejar de silenciar a @{name}",
|
||||
"account.unmute_notifications_short": "Dejar de silenciar notificaciones",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
@@ -72,11 +72,11 @@
|
||||
"account.go_to_profile": "Ir al perfil",
|
||||
"account.hide_reblogs": "Ocultar impulsos de @{name}",
|
||||
"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.languages": "Cambiar idiomas suscritos",
|
||||
"account.link_verified_on": "El proprietario de este enlace fue comprobado 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.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 propietario revisa manualmente quién puede seguirlo.",
|
||||
"account.media": "Multimedia",
|
||||
"account.mention": "Mencionar a @{name}",
|
||||
"account.menu.add_to_list": "Añadir a lista…",
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Dejar de silenciar a @{name}",
|
||||
"account.unmute_notifications_short": "Dejar de silenciar notificaciones",
|
||||
"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",
|
||||
"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",
|
||||
@@ -296,9 +298,9 @@
|
||||
"column.domain_blocks": "Dominios ocultados",
|
||||
"column.edit_list": "Editar lista",
|
||||
"column.favourites": "Favoritos",
|
||||
"column.firehose": "Feeds en vivo",
|
||||
"column.firehose_local": "Feed en vivo para este servidor",
|
||||
"column.firehose_singular": "Feed en vivo",
|
||||
"column.firehose": "Cronologías en vivo",
|
||||
"column.firehose_local": "Cronología en vivo para este servidor",
|
||||
"column.firehose_singular": "Cronología en vivo",
|
||||
"column.follow_requests": "Solicitudes de seguimiento",
|
||||
"column.home": "Inicio",
|
||||
"column.list_members": "Administrar miembros de la lista",
|
||||
@@ -313,7 +315,7 @@
|
||||
"column_header.moveRight_settings": "Mover columna a la derecha",
|
||||
"column_header.pin": "Fijar",
|
||||
"column_header.show_settings": "Mostrar ajustes",
|
||||
"column_header.unpin": "Desfijar",
|
||||
"column_header.unpin": "Dejar de fijar",
|
||||
"column_search.cancel": "Cancelar",
|
||||
"combobox.close_results": "Cerrar resultados",
|
||||
"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_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_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.server": "Servidor",
|
||||
"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.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.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.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á.",
|
||||
@@ -506,9 +508,9 @@
|
||||
"featured_carousel.header": "{count, plural,one {Publicación fijada}other {Publicaciones fijadas}}",
|
||||
"featured_carousel.slide": "Publicación {current, number} de {max, number}",
|
||||
"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.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.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",
|
||||
@@ -746,7 +748,7 @@
|
||||
"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_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.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",
|
||||
@@ -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.search": "Buscar",
|
||||
"onboarding.follows.title": "Sigue personas para comenzar",
|
||||
"onboarding.profile.discoverable": "Make my profile discoverable",
|
||||
"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.display_name": "Nombre a mostrar",
|
||||
"onboarding.profile.discoverable": "Hacer que mi perfil aparezca en búsquedas",
|
||||
"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 para mostrar",
|
||||
"onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…",
|
||||
"onboarding.profile.note": "Biografía",
|
||||
"onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…",
|
||||
"onboarding.profile.save_and_continue": "Guardar y continuar",
|
||||
"onboarding.profile.title": "Configuración del perfil",
|
||||
"onboarding.profile.upload_avatar": "Subir foto de perfil",
|
||||
"onboarding.profile.upload_header": "Subir foto de cabecera",
|
||||
"password_confirmation.exceeds_maxlength": "La contraseña de confirmación excede la longitud máxima de la contraseña",
|
||||
"onboarding.profile.upload_header": "Subir encabezado de perfil",
|
||||
"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",
|
||||
"picture_in_picture.restore": "Restaurar",
|
||||
"poll.closed": "Cerrada",
|
||||
@@ -892,14 +894,14 @@
|
||||
"privacy.change": "Ajustar privacidad",
|
||||
"privacy.direct.long": "Todos los mencionados en la publicación",
|
||||
"privacy.direct.short": "Mención privada",
|
||||
"privacy.private.long": "Sólo tus seguidores",
|
||||
"privacy.private.long": "Solo tus seguidores",
|
||||
"privacy.private.short": "Seguidores",
|
||||
"privacy.public.long": "Cualquiera dentro y fuera de Mastodon",
|
||||
"privacy.public.short": "Público",
|
||||
"privacy.quote.anyone": "{visibility}, citas permitidas",
|
||||
"privacy.quote.disabled": "{visibility}, citas desactivadas",
|
||||
"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.short": "Pública, pero discreta",
|
||||
"privacy_policy.last_updated": "Actualizado por última vez {date}",
|
||||
@@ -917,7 +919,7 @@
|
||||
"relative_time.days": "{number} d",
|
||||
"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.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.seconds": "Hace {number, plural, one {# second} other {# segundos}}",
|
||||
"relative_time.hours": "{number} h",
|
||||
@@ -932,7 +934,7 @@
|
||||
"reply_indicator.cancel": "Cancelar",
|
||||
"reply_indicator.poll": "Encuesta",
|
||||
"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.other": "Otro",
|
||||
"report.categories.spam": "Spam",
|
||||
@@ -944,7 +946,7 @@
|
||||
"report.close": "Realizado",
|
||||
"report.comment.title": "¿Hay algo más que creas que deberíamos saber?",
|
||||
"report.forward": "Reenviar a {target}",
|
||||
"report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?",
|
||||
"report.forward_hint": "La cuenta es de otro servidor. ¿Enviar también una copia anónima del informe allí?",
|
||||
"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.next": "Siguiente",
|
||||
@@ -970,7 +972,7 @@
|
||||
"report.thanks.title": "¿No quieres ver esto?",
|
||||
"report.thanks.title_actionable": "Gracias por denunciar, revisaremos esto.",
|
||||
"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.categories.legal": "Legal",
|
||||
"report_notification.categories.legal_sentence": "contenido ilegal",
|
||||
@@ -991,7 +993,7 @@
|
||||
"search.quick_action.status_search": "Publicaciones que coinciden con {x}",
|
||||
"search.search_or_paste": "Buscar o pegar URL",
|
||||
"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.options": "Opciones de búsqueda",
|
||||
"search_popout.quick_actions": "Acciones rápidas",
|
||||
@@ -1049,8 +1051,8 @@
|
||||
"status.history.created": "{name} creó {date}",
|
||||
"status.history.edited": "{name} editado {date}",
|
||||
"status.load_more": "Cargar más",
|
||||
"status.media.open": "Click para abrir",
|
||||
"status.media.show": "Click para mostrar",
|
||||
"status.media.open": "Haz clic para abrir",
|
||||
"status.media.show": "Haz clic para mostrar",
|
||||
"status.media_hidden": "Contenido multimedia oculto",
|
||||
"status.mention": "Mencionar @{name}",
|
||||
"status.more": "Más",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Kumoa käyttäjän @{name} mykistys",
|
||||
"account.unmute_notifications_short": "Kumoa ilmoitusten mykistys",
|
||||
"account.unmute_short": "Kumoa mykistys",
|
||||
"account_edit.column_button": "Valmis",
|
||||
"account_edit.column_title": "Muokkaa profiilia",
|
||||
"account_note.placeholder": "Lisää muistiinpano napsauttamalla",
|
||||
"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",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Doyv ikki @{name}",
|
||||
"account.unmute_notifications_short": "Tendra fráboðanir",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Deixar de silenciar a @{name}",
|
||||
"account.unmute_notifications_short": "Reactivar notificacións",
|
||||
"account.unmute_short": "Non silenciar",
|
||||
"account_edit.column_button": "Feito",
|
||||
"account_edit.column_title": "Editar perfil",
|
||||
"account_note.placeholder": "Preme para engadir nota",
|
||||
"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",
|
||||
@@ -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.title": "Crear conta en Mastodon",
|
||||
"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_name": "Nome",
|
||||
"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.continue": "Continuar",
|
||||
"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_settings": "Editar axustes",
|
||||
"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.manage_accounts": "Xestionar contas",
|
||||
"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.new_collection": "Nova colección",
|
||||
"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.view_collection": "Ver colección",
|
||||
"collections.visibility_public": "Pública",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "הפסקת השתקת @{name}",
|
||||
"account.unmute_notifications_short": "הפעלת הודעות",
|
||||
"account.unmute_short": "ביטול השתקה",
|
||||
"account_edit.column_button": "סיום",
|
||||
"account_edit.column_title": "עריכת הפרופיל",
|
||||
"account_note.placeholder": "יש ללחוץ כדי להוסיף הערות",
|
||||
"admin.dashboard.daily_retention": "קצב שימור משתמשים יומי אחרי ההרשמה",
|
||||
"admin.dashboard.monthly_retention": "קצב שימור משתמשים (פר חודש) אחרי ההרשמה",
|
||||
@@ -244,9 +246,12 @@
|
||||
"closed_registrations_modal.preamble": "מסטודון הוא רשת מבוזרת, כך שלא משנה היכן החשבון שלך, קיימת האפשרות לעקוב ולתקשר עם משתמשים בשרת הזה. אפשר אפילו להריץ שרת בעצמך!",
|
||||
"closed_registrations_modal.title": "להרשם למסטודון",
|
||||
"collections.account_count": "{count, plural, one {חשבון אחד} other {# חשבונות}}",
|
||||
"collections.accounts.empty_description": "להוסיף עד ל־{count} חשבונות שאתם עוקבים אחריהם",
|
||||
"collections.accounts.empty_title": "האוסף הזה ריק",
|
||||
"collections.collection_description": "תיאור",
|
||||
"collections.collection_name": "כינוי",
|
||||
"collections.collection_topic": "נושא",
|
||||
"collections.confirm_account_removal": "בוודאות להסיר חשבון זה מהאוסף?",
|
||||
"collections.content_warning": "אזהרת תוכן",
|
||||
"collections.continue": "המשך",
|
||||
"collections.create.accounts_subtitle": "רק חשבונות נעקבים שבחרו להופיע ב\"תגליות\" ניתנים להוספה.",
|
||||
@@ -261,6 +266,9 @@
|
||||
"collections.edit_details": "עריכת פרטים בסיסיים",
|
||||
"collections.edit_settings": "עריכת הגדרות",
|
||||
"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.manage_accounts": "ניהול חשבונות",
|
||||
"collections.manage_accounts_in_collection": "ניהול החשבונות שבאוסף זה",
|
||||
@@ -269,6 +277,9 @@
|
||||
"collections.name_length_hint": "מגבלה של 100 תווים",
|
||||
"collections.new_collection": "אוסף חדש",
|
||||
"collections.no_collections_yet": "עוד אין אוספים.",
|
||||
"collections.remove_account": "הסר חשבון זה",
|
||||
"collections.search_accounts_label": "לחפש חשבונות להוספה…",
|
||||
"collections.search_accounts_max_reached": "הגעת למספר החשבונות המירבי",
|
||||
"collections.topic_hint": "הוספת תגית שמסייעת לאחרים להבין את הנושא הראשי של האוסף.",
|
||||
"collections.view_collection": "צפיה באוסף",
|
||||
"collections.visibility_public": "פומבי",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Hætta að þagga niður í @{name}",
|
||||
"account.unmute_notifications_short": "Hætta að þagga í tilkynningum",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Riattiva @{name}",
|
||||
"account.unmute_notifications_short": "Riattiva notifiche",
|
||||
"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",
|
||||
"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",
|
||||
@@ -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.title": "Registrazione su Mastodon",
|
||||
"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_name": "Nome",
|
||||
"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.continue": "Continua",
|
||||
"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_settings": "Modifica impostazioni",
|
||||
"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.manage_accounts": "Gestisci account",
|
||||
"collections.manage_accounts_in_collection": "Gestisci gli account in questa collezione",
|
||||
@@ -269,6 +277,9 @@
|
||||
"collections.name_length_hint": "Limite di 100 caratteri",
|
||||
"collections.new_collection": "Nuova collezione",
|
||||
"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.view_collection": "Visualizza la collezione",
|
||||
"collections.visibility_public": "Pubblica",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"about.not_available": "Denne informasjonen er ikkje gjort tilgjengeleg på denne tenaren.",
|
||||
"about.powered_by": "Desentraliserte sosiale medium drive av {mastodon}",
|
||||
"about.rules": "Tenarreglar",
|
||||
"account.about": "Om",
|
||||
"account.account_note_header": "Personleg notat",
|
||||
"account.activity": "Aktivitet",
|
||||
"account.add_note": "Legg til eit personleg notat",
|
||||
@@ -87,6 +88,7 @@
|
||||
"account.menu.hide_reblogs": "Gøym framhevingar på tidslina",
|
||||
"account.menu.mention": "Omtale",
|
||||
"account.menu.mute": "Demp konto",
|
||||
"account.menu.note.description": "Berre synleg for deg",
|
||||
"account.menu.open_original_page": "Vis på {domain}",
|
||||
"account.menu.remove_follower": "Fjern fylgjar",
|
||||
"account.menu.report": "Rapporter kontoen",
|
||||
@@ -102,6 +104,13 @@
|
||||
"account.muted": "Målbunden",
|
||||
"account.muting": "Dempa",
|
||||
"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.node_modal.callout": "Berre du kan sjå personlege notat.",
|
||||
"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.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",
|
||||
"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_name": "Namn",
|
||||
"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.continue": "Hald fram",
|
||||
"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_settings": "Rediger innstillingar",
|
||||
"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_in_collection": "Handter kontoar i denne samlinga",
|
||||
"collections.mark_as_sensitive": "Merk som ømtolig",
|
||||
@@ -258,6 +275,9 @@
|
||||
"collections.name_length_hint": "Maks 100 teikn",
|
||||
"collections.new_collection": "Ny samling",
|
||||
"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.view_collection": "Sjå samlinga",
|
||||
"collections.visibility_public": "Offentleg",
|
||||
@@ -442,6 +462,8 @@
|
||||
"emoji_button.search_results": "Søkeresultat",
|
||||
"emoji_button.symbols": "Symbol",
|
||||
"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.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.",
|
||||
@@ -525,6 +547,8 @@
|
||||
"follow_suggestions.view_all": "Vis alle",
|
||||
"follow_suggestions.who_to_follow": "Kven du kan fylgja",
|
||||
"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_mastodon": "Om Mastodon",
|
||||
"footer.about_server": "Om {domain}",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Ktheji zërin @{name}",
|
||||
"account.unmute_notifications_short": "Shfaqi njoftimet",
|
||||
"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",
|
||||
"admin.dashboard.daily_retention": "Shkallë mbajtjeje përdoruesi, në ditë, pas regjistrimit",
|
||||
"admin.dashboard.monthly_retention": "Shkallë mbajtjeje përdoruesi, në muaj, pas regjistrimit",
|
||||
|
||||
@@ -92,6 +92,8 @@
|
||||
"account.unmute": "Sluta tysta @{name}",
|
||||
"account.unmute_notifications_short": "Aktivera aviseringsljud",
|
||||
"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",
|
||||
"admin.dashboard.daily_retention": "Användarlojalitet per dag efter registrering",
|
||||
"admin.dashboard.monthly_retention": "Användarlojalitet per månad efter registrering",
|
||||
|
||||
@@ -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.title": "Mastodon'a kayıt olmak",
|
||||
"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_name": "Ad",
|
||||
"collections.collection_topic": "Konu",
|
||||
"collections.confirm_account_removal": "Bu hesabı bu koleksiyondan çıkarmak istediğinizden emin misiniz?",
|
||||
"collections.content_warning": "İçerik uyarısı",
|
||||
"collections.continue": "Devam et",
|
||||
"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_settings": "Ayarları düzenle",
|
||||
"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.manage_accounts": "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.new_collection": "Yeni koleksiyon",
|
||||
"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.view_collection": "Koleksiyonu görüntüle",
|
||||
"collections.visibility_public": "Herkese açık",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "Bỏ phớt lờ @{name}",
|
||||
"account.unmute_notifications_short": "Bỏ phớt lờ thông báo",
|
||||
"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",
|
||||
"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ý",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
"account.unmute": "解除靜音 @{name}",
|
||||
"account.unmute_notifications_short": "解除靜音推播通知",
|
||||
"account.unmute_short": "解除靜音",
|
||||
"account_edit.column_button": "完成",
|
||||
"account_edit.column_title": "編輯個人檔案",
|
||||
"account_note.placeholder": "點擊以新增備註",
|
||||
"admin.dashboard.daily_retention": "註冊後使用者存留率(日)",
|
||||
"admin.dashboard.monthly_retention": "註冊後使用者存留率(月)",
|
||||
@@ -245,7 +247,7 @@
|
||||
"closed_registrations_modal.title": "註冊 Mastodon",
|
||||
"collections.account_count": "{count, plural, other {# 個帳號}}",
|
||||
"collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號",
|
||||
"collections.accounts.empty_title": "此收藏是名單空的",
|
||||
"collections.accounts.empty_title": "此收藏名單是空的",
|
||||
"collections.collection_description": "說明",
|
||||
"collections.collection_name": "名稱",
|
||||
"collections.collection_topic": "主題",
|
||||
|
||||
@@ -14,7 +14,7 @@ import { CustomEmojiFactory } from './custom_emoji';
|
||||
import type { CustomEmoji } from './custom_emoji';
|
||||
|
||||
// AccountField
|
||||
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||
export interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||
name_emojified: string;
|
||||
value_emojified: string;
|
||||
value_plain: string | null;
|
||||
|
||||
@@ -18,7 +18,7 @@ export function isServerFeatureEnabled(feature: ServerFeatures) {
|
||||
return initialState?.features.includes(feature) ?? false;
|
||||
}
|
||||
|
||||
type ClientFeatures = 'collections';
|
||||
type ClientFeatures = 'collections' | 'profile_editing';
|
||||
|
||||
export function isClientFeatureEnabled(feature: ClientFeatures) {
|
||||
try {
|
||||
|
||||
@@ -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 |
1
app/javascript/material-icons/400-24px/more_vert.svg
Normal file
1
app/javascript/material-icons/400-24px/more_vert.svg
Normal 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 |
@@ -4192,7 +4192,8 @@ a.account__display-name {
|
||||
cursor: pointer;
|
||||
|
||||
& > 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;
|
||||
padding: 12px 9px;
|
||||
backdrop-filter: $backdrop-blur-filter;
|
||||
@@ -4205,7 +4206,7 @@ a.account__display-name {
|
||||
button,
|
||||
a {
|
||||
display: inline;
|
||||
color: var(--color-text-primary);
|
||||
color: var(--color-text-on-media);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0 8px;
|
||||
@@ -4216,7 +4217,7 @@ a.account__display-name {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: var(--color-text-primary);
|
||||
color: var(--color-text-on-media);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ class Fasp::Request
|
||||
headers = request_headers(verb, url, body)
|
||||
key = Linzer.new_ed25519_key(@provider.server_private_key_pem, @provider.remote_identifier)
|
||||
response = HTTP
|
||||
.headers(headers)
|
||||
.use(http_signature: { key:, covered_components: COVERED_COMPONENTS })
|
||||
.send(verb, url, body:)
|
||||
.headers(headers)
|
||||
.use(http_signature: { key:, covered_components: COVERED_COMPONENTS })
|
||||
.send(verb, url, body:)
|
||||
|
||||
validate!(response)
|
||||
@provider.delivery_failure_tracker.track_success!
|
||||
|
||||
@@ -239,10 +239,10 @@ class FeedManager
|
||||
# This is a bit tricky because we need posts tagged with this hashtag that are not
|
||||
# also tagged with another followed hashtag or from a followed user
|
||||
scope = from_tag.statuses
|
||||
.where(id: timeline_status_ids)
|
||||
.where.not(account: into_account)
|
||||
.where.not(account: into_account.following)
|
||||
.tagged_with_none(TagFollow.where(account: into_account).pluck(:tag_id))
|
||||
.where(id: timeline_status_ids)
|
||||
.where.not(account: into_account)
|
||||
.where.not(account: into_account.following)
|
||||
.tagged_with_none(TagFollow.where(account: into_account).pluck(:tag_id))
|
||||
|
||||
scope.select(:id, :reblog_of_id).reorder(nil).find_each do |status|
|
||||
remove_from_feed(:home, into_account.id, status, aggregate_reblogs: into_account.user&.aggregates_reblogs?)
|
||||
|
||||
@@ -18,8 +18,8 @@ class Vacuum::StatusesVacuum
|
||||
# Side-effects not covered by foreign keys, such
|
||||
# as the search index, must be handled first.
|
||||
statuses.direct_visibility
|
||||
.includes(mentions: :account)
|
||||
.find_each(&:unlink_from_conversations!)
|
||||
.includes(mentions: :account)
|
||||
.find_each(&:unlink_from_conversations!)
|
||||
if Chewy.enabled?
|
||||
remove_from_index(statuses.ids, 'chewy:queue:StatusesIndex')
|
||||
remove_from_index(statuses.ids, 'chewy:queue:PublicStatusesIndex')
|
||||
@@ -33,8 +33,8 @@ class Vacuum::StatusesVacuum
|
||||
|
||||
def statuses_scope
|
||||
Status.unscoped.kept
|
||||
.joins(:account).merge(Account.remote)
|
||||
.where(statuses: { id: ...retention_period_as_id })
|
||||
.joins(:account).merge(Account.remote)
|
||||
.where(statuses: { id: ...retention_period_as_id })
|
||||
end
|
||||
|
||||
def retention_period_as_id
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
# reviewed_at :datetime
|
||||
# sensitized_at :datetime
|
||||
# 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
|
||||
# suspended_at :datetime
|
||||
# suspension_origin :integer
|
||||
|
||||
@@ -45,7 +45,7 @@ module Account::Interactions
|
||||
|
||||
def follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
|
||||
rel = active_relationships.create_with(show_reblogs: reblogs.nil? || reblogs, notify: notify.nil? ? false : notify, languages: languages, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||
rel.notify = notify unless notify.nil?
|
||||
@@ -58,7 +58,7 @@ module Account::Interactions
|
||||
|
||||
def request_follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
|
||||
rel = follow_requests.create_with(show_reblogs: reblogs.nil? || reblogs, notify: notify.nil? ? false : notify, uri: uri, languages: languages, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||
rel.notify = notify unless notify.nil?
|
||||
@@ -71,7 +71,7 @@ module Account::Interactions
|
||||
|
||||
def block!(other_account, uri: nil)
|
||||
block_relationships.create_with(uri: uri)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
end
|
||||
|
||||
def mute!(other_account, notifications: nil, duration: 0)
|
||||
@@ -215,14 +215,14 @@ module Account::Interactions
|
||||
|
||||
def followers_for_local_distribution
|
||||
followers.local
|
||||
.joins(:user)
|
||||
.merge(User.signed_in_recently)
|
||||
.joins(:user)
|
||||
.merge(User.signed_in_recently)
|
||||
end
|
||||
|
||||
def lists_for_local_distribution
|
||||
scope = lists.joins(account: :user)
|
||||
scope.where.not(list_accounts: { follow_id: nil }).or(scope.where(account_id: id))
|
||||
.merge(User.signed_in_recently)
|
||||
.merge(User.signed_in_recently)
|
||||
end
|
||||
|
||||
def remote_followers_hash(url)
|
||||
|
||||
@@ -5,6 +5,6 @@ module Fasp::Provider::DebugConcern
|
||||
|
||||
def perform_debug_call
|
||||
Fasp::Request.new(self)
|
||||
.post('/debug/v0/callback/logs', body: { hello: 'world' })
|
||||
.post('/debug/v0/callback/logs', body: { hello: 'world' })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -216,11 +216,11 @@ class MediaAttachment < ApplicationRecord
|
||||
scope :updated_before, ->(value) { where(arel_table[:updated_at].lt(value)) }
|
||||
scope :without_local_interaction, lambda {
|
||||
where.not(Favourite.joins(:account).merge(Account.local).where(Favourite.arel_table[:status_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Bookmark.where(Bookmark.arel_table[:status_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Status.local.where(Status.arel_table[:in_reply_to_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Status.local.where(Status.arel_table[:reblog_of_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Quote.joins(:status).merge(Status.local).where(Quote.arel_table[:quoted_status_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Quote.joins(:quoted_status).merge(Status.local).where(Quote.arel_table[:status_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Bookmark.where(Bookmark.arel_table[:status_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Status.local.where(Status.arel_table[:in_reply_to_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Status.local.where(Status.arel_table[:reblog_of_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Quote.joins(:status).merge(Status.local).where(Quote.arel_table[:quoted_status_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
.where.not(Quote.joins(:quoted_status).merge(Status.local).where(Quote.arel_table[:status_id].eq(MediaAttachment.arel_table[:status_id])).select(1).arel.exists)
|
||||
}
|
||||
|
||||
attr_accessor :skip_download
|
||||
|
||||
@@ -375,19 +375,19 @@ class Status < ApplicationRecord
|
||||
|
||||
# _from_me part does not require any timeline filters
|
||||
query_from_me = where(account_id: account.id)
|
||||
.direct_visibility
|
||||
.limit(limit)
|
||||
.order(id: :desc)
|
||||
.direct_visibility
|
||||
.limit(limit)
|
||||
.order(id: :desc)
|
||||
|
||||
# _to_me part requires mute and block filter.
|
||||
# FIXME: may we check mutes.hide_notifications?
|
||||
query_to_me = Status
|
||||
.direct_visibility
|
||||
.joins(:mentions)
|
||||
.where(mentions: { account_id: account.id })
|
||||
.limit(limit)
|
||||
.order('mentions.status_id DESC')
|
||||
.not_excluded_by_account(account)
|
||||
.direct_visibility
|
||||
.joins(:mentions)
|
||||
.where(mentions: { account_id: account.id })
|
||||
.limit(limit)
|
||||
.order('mentions.status_id DESC')
|
||||
.not_excluded_by_account(account)
|
||||
|
||||
if max_id.present?
|
||||
query_from_me = query_from_me.where(id: ...max_id)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user