Merge remote-tracking branch 'parent/main' into upstream-20241203

This commit is contained in:
KMY 2024-12-04 11:56:14 +09:00
commit 0a0b0d46ea
52 changed files with 599 additions and 451 deletions

View file

@ -399,7 +399,7 @@ The following changelog entries focus on changes visible to users, administrator
- Fix empty environment variables not using default nil value (#27400 by @renchap)
- Fix language sorting in settings (#27158 by @gunchleoc)
## |4.2.11] - 2024-08-16
## [4.2.11] - 2024-08-16
### Added

View file

@ -426,7 +426,7 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.16.7)
nokogiri (1.16.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.7)
@ -632,9 +632,9 @@ GEM
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.1)
loofah (~> 2.21)
nokogiri (~> 1.14)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (7.0.10)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)

View file

@ -149,6 +149,7 @@ module ApplicationHelper
output << content_for(:body_classes)
output << "theme-#{current_theme.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
output << 'rtl' if locale_direction == 'rtl'
output << "content-font-size__#{current_account&.user&.setting_content_font_size}"

View file

@ -1,66 +0,0 @@
import { defineMessages } from 'react-intl';
import { AxiosError } from 'axios';
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' },
rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' },
});
export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
export const dismissAlert = alert => ({
type: ALERT_DISMISS,
alert,
});
export const clearAlert = () => ({
type: ALERT_CLEAR,
});
export const showAlert = alert => ({
type: ALERT_SHOW,
alert,
});
export const showAlertForError = (error, skipNotFound = false) => {
if (error.response) {
const { data, status, statusText, headers } = error.response;
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
return { type: ALERT_NOOP };
}
// Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) {
return showAlert({
title: messages.rateLimitedTitle,
message: messages.rateLimitedMessage,
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
});
}
return showAlert({
title: `${status}`,
message: data.error || statusText,
});
}
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
};

View file

@ -0,0 +1,90 @@
import { defineMessages } from 'react-intl';
import type { MessageDescriptor } from 'react-intl';
import { AxiosError } from 'axios';
import type { AxiosResponse } from 'axios';
interface Alert {
title: string | MessageDescriptor;
message: string | MessageDescriptor;
values?: Record<string, string | number | Date>;
}
interface ApiErrorResponse {
error?: string;
}
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: {
id: 'alert.unexpected.message',
defaultMessage: 'An unexpected error occurred.',
},
rateLimitedTitle: {
id: 'alert.rate_limited.title',
defaultMessage: 'Rate limited',
},
rateLimitedMessage: {
id: 'alert.rate_limited.message',
defaultMessage: 'Please retry after {retry_time, time, medium}.',
},
});
export const ALERT_SHOW = 'ALERT_SHOW';
export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
export const dismissAlert = (alert: Alert) => ({
type: ALERT_DISMISS,
alert,
});
export const clearAlert = () => ({
type: ALERT_CLEAR,
});
export const showAlert = (alert: Alert) => ({
type: ALERT_SHOW,
alert,
});
export const showAlertForError = (error: unknown, skipNotFound = false) => {
if (error instanceof AxiosError && error.response) {
const { status, statusText, headers } = error.response;
const { data } = error.response as AxiosResponse<ApiErrorResponse>;
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
return { type: ALERT_NOOP };
}
// Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) {
return showAlert({
title: messages.rateLimitedTitle,
message: messages.rateLimitedMessage,
values: {
retry_time: new Date(headers['x-ratelimit-reset'] as string),
},
});
}
return showAlert({
title: `${status}`,
message: data.error ?? statusText,
});
}
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
};

View file

@ -5,3 +5,16 @@ export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
comment: value,
});
export const apiFollowAccount = (
id: string,
params?: {
reblogs: boolean;
},
) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/follow`, {
...params,
});
export const apiUnfollowAccount = (id: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/unfollow`);

View file

@ -1,188 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { EmptyAccount } from 'mastodon/components/empty_account';
import { FollowButton } from 'mastodon/components/follow_button';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { me } from '../initial_state';
import { Avatar } from './avatar';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
});
const Account = ({ size = 46, account, onBlock, onMute, onMuteNotifications, hidden, hideButtons, minimal, defaultAction, children, withBio }) => {
const intl = useIntl();
const handleBlock = useCallback(() => {
onBlock(account);
}, [onBlock, account]);
const handleMute = useCallback(() => {
onMute(account);
}, [onMute, account]);
const handleMuteNotifications = useCallback(() => {
onMuteNotifications(account, true);
}, [onMuteNotifications, account]);
const handleUnmuteNotifications = useCallback(() => {
onMuteNotifications(account, false);
}, [onMuteNotifications, account]);
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
}
if (hidden) {
return (
<>
{account.get('display_name')}
{account.get('username')}
</>
);
}
let buttons;
if (!hideButtons && account.get('id') !== me && account.get('relationship', null) !== null) {
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <FollowButton accountId={account.get('id')} />;
} else if (blocking) {
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
} else if (muting) {
let menu;
if (account.getIn(['relationship', 'muting_notifications'])) {
menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
} else {
menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
}
buttons = (
<>
<DropdownMenuContainer
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<Button text={intl.formatMessage(messages.unmute)} onClick={handleMute} />
</>
);
} else if (defaultAction === 'mute') {
buttons = <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
} else {
buttons = <FollowButton accountId={account.get('id')} />;
}
} else if (!hideButtons) {
buttons = <FollowButton accountId={account.get('id')} />;
}
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`} data-hover-card-account={account.get('id')}>
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter}
isHide={account.getIn(['other_settings', 'hide_followers_count'])} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
</Link>
{!minimal && children && (
<div>
<div>
{children}
</div>
<div className='account__relationship'>
{buttons}
</div>
</div>
)}
{!minimal && !children && (
<div className='account__relationship'>
{buttons}
</div>
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
);
};
Account.propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.record,
onBlock: PropTypes.func,
onMute: PropTypes.func,
onMuteNotifications: PropTypes.func,
hidden: PropTypes.bool,
hideButtons: PropTypes.bool,
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
withBio: PropTypes.bool,
children: PropTypes.any,
};
export default Account;

View file

@ -0,0 +1,260 @@
import type { ReactNode } from 'react';
import React, { useCallback } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import {
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { initMuteModal } from 'mastodon/actions/mutes';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
import { me } from 'mastodon/initial_state';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
cancel_follow_request: {
id: 'account.cancel_follow_request',
defaultMessage: 'Withdraw follow request',
},
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
mute_notifications: {
id: 'account.mute_notifications_short',
defaultMessage: 'Mute notifications',
},
unmute_notifications: {
id: 'account.unmute_notifications_short',
defaultMessage: 'Unmute notifications',
},
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
});
export const Account: React.FC<{
size?: number;
id: string;
hidden?: boolean;
minimal?: boolean;
defaultAction?: 'block' | 'mute';
withBio?: boolean;
hideButtons?: boolean;
children?: ReactNode;
}> = ({
id,
size = 46,
hidden,
minimal,
defaultAction,
withBio,
hideButtons,
children,
}) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(id));
const relationship = useAppSelector((state) => state.relationships.get(id));
const dispatch = useAppDispatch();
const handleBlock = useCallback(() => {
if (relationship?.blocking) {
dispatch(unblockAccount(id));
} else {
dispatch(blockAccount(id));
}
}, [dispatch, id, relationship]);
const handleMute = useCallback(() => {
if (relationship?.muting) {
dispatch(unmuteAccount(id));
} else {
dispatch(initMuteModal(account));
}
}, [dispatch, id, account, relationship]);
const handleMuteNotifications = useCallback(() => {
dispatch(muteAccount(id, true));
}, [dispatch, id]);
const handleUnmuteNotifications = useCallback(() => {
dispatch(muteAccount(id, false));
}, [dispatch, id]);
if (hidden) {
return (
<>
{account?.display_name}
{account?.username}
</>
);
}
let buttons;
if (account && account.id !== me && relationship) {
const { requested, blocking, muting } = relationship;
if (requested) {
buttons = <FollowButton accountId={id} />;
} else if (blocking) {
buttons = (
<Button
text={intl.formatMessage(messages.unblock)}
onClick={handleBlock}
/>
);
} else if (muting) {
const menu = [
{
text: intl.formatMessage(
relationship.muting_notifications
? messages.unmute_notifications
: messages.mute_notifications,
),
action: relationship.muting_notifications
? handleUnmuteNotifications
: handleMuteNotifications,
},
];
buttons = (
<>
<DropdownMenu
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<Button
text={intl.formatMessage(messages.unmute)}
onClick={handleMute}
/>
</>
);
} else if (defaultAction === 'mute') {
buttons = (
<Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />
);
} else if (defaultAction === 'block') {
buttons = (
<Button
text={intl.formatMessage(messages.block)}
onClick={handleBlock}
/>
);
} else {
buttons = <FollowButton accountId={id} />;
}
} else {
buttons = <FollowButton accountId={id} />;
}
if (hideButtons) {
buttons = null;
}
let muteTimeRemaining;
if (account?.mute_expires_at) {
muteTimeRemaining = (
<>
· <RelativeTimestamp timestamp={account.mute_expires_at} futureDate />
</>
);
}
let verification;
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.value} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link
className='account__display-name'
title={account?.acct}
to={`/@${account?.acct}`}
data-hover-card-account={id}
>
<div className='account__avatar-wrapper'>
{account ? (
<Avatar account={account} size={size} />
) : (
<Skeleton width={size} height={size} />
)}
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
{account ? (
<>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
isHide={account.other_settings.hide_followers_count}
/>{' '}
{verification} {muteTimeRemaining}
</>
) : (
<Skeleton width='7ch' />
)}
</div>
)}
</div>
</Link>
{!minimal && children && (
<div>
<div>{children}</div>
<div className='account__relationship'>{buttons}</div>
</div>
)}
{!minimal && !children && (
<div className='account__relationship'>{buttons}</div>
)}
</div>
{account &&
withBio &&
(account.note.length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
/>
) : (
<div className='account__note account__note--missing'>
<FormattedMessage
id='account.no_bio'
defaultMessage='No description provided.'
/>
</div>
))}
</div>
);
};

View file

@ -1,33 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { DisplayName } from 'mastodon/components/display_name';
import { Skeleton } from 'mastodon/components/skeleton';
interface Props {
size?: number;
minimal?: boolean;
}
export const EmptyAccount: React.FC<Props> = ({
size = 46,
minimal = false,
}) => {
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'>
<Skeleton width={size} height={size} />
</div>
<div>
<DisplayName />
<Skeleton width='7ch' />
</div>
</div>
</div>
</div>
);
};

View file

@ -8,10 +8,10 @@ import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { fetchServer } from 'mastodon/actions/server';
import { Account } from 'mastodon/components/account';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state';
const messages = defineMessages({

View file

@ -1,60 +0,0 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import {
followAccount,
blockAccount,
unblockAccount,
muteAccount,
unmuteAccount,
} from '../actions/accounts';
import { initMuteModal } from '../actions/mutes';
import Account from '../components/account';
import { makeGetAccount } from '../selectors';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
} else {
dispatch(followAccount(account.get('id')));
}
},
onBlock (account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(blockAccount(account.get('id')));
}
},
onMute (account) {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},
onMuteNotifications (account, notifications) {
dispatch(muteAccount(account.get('id'), notifications));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));

View file

@ -15,11 +15,11 @@ import DisabledIcon from '@/material-icons/400-24px/close-fill.svg?react';
import EnabledIcon from '@/material-icons/400-24px/done-fill.svg?react';
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
import { Account } from 'mastodon/components/account';
import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
const messages = defineMessages({

View file

@ -1,6 +1,7 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { FormattedMessage, FormattedNumber } from 'react-intl';
import { domain } from 'mastodon/initial_state';
import type { Percentiles } from 'mastodon/models/annual_report';
export const Percentile: React.FC<{
@ -12,7 +13,7 @@ export const Percentile: React.FC<{
<div className='annual-report__bento__box annual-report__summary__percentile'>
<FormattedMessage
id='annual_report.summary.percentile.text'
defaultMessage='<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>'
defaultMessage='<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>'
values={{
topLabel: (str) => (
<div className='annual-report__summary__percentile__label'>
@ -44,6 +45,8 @@ export const Percentile: React.FC<{
)}
</div>
),
domain,
}}
>
{(message) => <>{message}</>}

View file

@ -9,11 +9,11 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
import { Account } from 'mastodon/components/account';
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
const messages = defineMessages({
@ -70,7 +70,7 @@ class Blocks extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} defaultAction='block' />,
<Account key={id} id={id} defaultAction='block' />,
)}
</ScrollableList>
</Column>

View file

@ -6,7 +6,7 @@ import { useSelector, useDispatch } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { cancelReplyCompose } from 'mastodon/actions/compose';
import Account from 'mastodon/components/account';
import { Account } from 'mastodon/components/account';
import { IconButton } from 'mastodon/components/icon_button';
import { me } from 'mastodon/initial_state';
@ -20,7 +20,6 @@ const messages = defineMessages({
export const NavigationBar = () => {
const dispatch = useDispatch();
const intl = useIntl();
const account = useSelector(state => state.getIn(['accounts', me]));
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
const handleCancelClick = useCallback(() => {
@ -29,7 +28,7 @@ export const NavigationBar = () => {
return (
<div className='navigation-bar'>
<Account account={account} minimal />
<Account id={me} minimal />
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
</div>
);

View file

@ -6,6 +6,7 @@ import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { expandSearch } from 'mastodon/actions/search';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@ -13,7 +14,6 @@ import { SearchSection } from 'mastodon/features/explore/components/search_secti
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
const INITIAL_PAGE_LIMIT = 10;
@ -49,7 +49,7 @@ export const SearchResults = () => {
if (results.get('accounts') && results.get('accounts').size > 0) {
accounts = (
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
{withoutLastResult(results.get('accounts')).map(accountId => <Account key={accountId} id={accountId} />)}
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreAccounts} />}
</SearchSection>
);

View file

@ -10,12 +10,13 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import { fetchEmojiReactions, expandEmojiReactions } from 'mastodon/actions/interactions';
import { Account } from 'mastodon/components/account';
import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column';
@ -101,11 +102,11 @@ class EmojiReactions extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{Object.keys(groups).map((key) =>(
<AccountContainer key={key} id={key} withNote={false} hideButtons>
<Account key={key} id={key} hideButtons>
<div style={{ 'maxWidth': '100px' }}>
{groups[key].map((value, index2) => <EmojiView key={index2} name={value.name} url={value.url} staticUrl={value.static_url} />)}
</div>
</AccountContainer>
</Account>
))}
</ScrollableList>

View file

@ -13,10 +13,10 @@ import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { submitSearch, expandSearch } from 'mastodon/actions/search';
import { Account } from 'mastodon/components/account';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import Account from 'mastodon/containers/account_container';
import Status from 'mastodon/containers/status_container';
import { SearchSection } from './components/search_section';

View file

@ -14,11 +14,11 @@ import { debounce } from 'lodash';
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
import { Account } from 'mastodon/components/account';
import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column';
const messages = defineMessages({
@ -89,7 +89,7 @@ class Favourites extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>

View file

@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { isHideItem, me } from 'mastodon/initial_state';
@ -24,7 +25,6 @@ import {
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
@ -179,7 +179,7 @@ class Followers extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{forceEmptyState ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>

View file

@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { isHideItem, me } from 'mastodon/initial_state';
@ -24,7 +25,6 @@ import {
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
@ -177,7 +177,7 @@ class Following extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{forceEmptyState ? [] : filteredAccountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>
</Column>

View file

@ -9,9 +9,13 @@ import { useDebouncedCallback } from 'use-debounce';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { fetchRelationships } from 'mastodon/actions/accounts';
import { showAlertForError } from 'mastodon/actions/alerts';
import { importFetchedAccounts } from 'mastodon/actions/importer';
import { fetchList } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
import { apiRequest } from 'mastodon/api';
import { apiFollowAccount } from 'mastodon/api/accounts';
import {
apiGetAccounts,
apiAddAccountToList,
@ -28,13 +32,14 @@ import { DisplayName } from 'mastodon/components/display_name';
import ScrollableList from 'mastodon/components/scrollable_list';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { me } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
placeholder: {
id: 'lists.search_placeholder',
defaultMessage: 'Search people you follow',
id: 'lists.search',
defaultMessage: 'Search',
},
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
add: { id: 'lists.add_member', defaultMessage: 'Add' },
@ -51,17 +56,51 @@ const AccountItem: React.FC<{
onToggle: (accountId: string) => void;
}> = ({ accountId, listId, partOfList, onToggle }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId));
const relationship = useAppSelector((state) =>
accountId ? state.relationships.get(accountId) : undefined,
);
const following =
accountId === me || relationship?.following || relationship?.requested;
useEffect(() => {
if (accountId) {
dispatch(fetchRelationships([accountId]));
}
}, [dispatch, accountId]);
const handleClick = useCallback(() => {
if (partOfList) {
void apiRemoveAccountFromList(listId, accountId);
onToggle(accountId);
} else {
void apiAddAccountToList(listId, accountId);
if (following) {
void apiAddAccountToList(listId, accountId);
onToggle(accountId);
} else {
dispatch(
openModal({
modalType: 'CONFIRM_FOLLOW_TO_LIST',
modalProps: {
accountId,
onConfirm: () => {
apiFollowAccount(accountId)
.then(() => apiAddAccountToList(listId, accountId))
.then(() => {
onToggle(accountId);
return '';
})
.catch((err: unknown) => {
dispatch(showAlertForError(err));
});
},
},
}),
);
}
}
onToggle(accountId);
}, [accountId, listId, partOfList, onToggle]);
}, [dispatch, accountId, following, listId, partOfList, onToggle]);
if (!account) {
return null;
@ -187,8 +226,7 @@ const ListMembers: React.FC<{
signal: searchRequestRef.current.signal,
params: {
q: value,
resolve: false,
following: true,
resolve: true,
},
})
.then((data) => {

View file

@ -11,10 +11,10 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { fetchMentionedUsers, expandMentionedUsers } from 'mastodon/actions/interactions';
import { Account } from 'mastodon/components/account';
import ColumnHeader from 'mastodon/components/column_header';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column';
const mapStateToProps = (state, props) => ({
@ -74,7 +74,7 @@ class MentionedUsers extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>

View file

@ -11,11 +11,11 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import { Account } from 'mastodon/components/account';
import { fetchMutes, expandMutes } from '../../actions/mutes';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
const messages = defineMessages({
@ -72,7 +72,7 @@ class Mutes extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} defaultAction='mute' />,
<Account key={id} id={id} defaultAction='mute' />,
)}
</ScrollableList>

View file

@ -20,9 +20,9 @@ import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { Account } from 'mastodon/components/account';
import EmojiView from 'mastodon/components/emoji_view';
import { Icon } from 'mastodon/components/icon';
import AccountContainer from 'mastodon/containers/account_container';
import StatusContainer from 'mastodon/containers/status_container';
import { me } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -153,7 +153,7 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);
@ -173,7 +173,7 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
<FollowRequestContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);
@ -533,7 +533,7 @@ class Notification extends ImmutablePureComponent {
</span>
</div>
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);

View file

@ -14,11 +14,11 @@ import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { markAsPartial } from 'mastodon/actions/timelines';
import { apiRequest } from 'mastodon/api';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { Account } from 'mastodon/components/account';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { ColumnSearchHeader } from 'mastodon/components/column_search_header';
import ScrollableList from 'mastodon/components/scrollable_list';
import Account from 'mastodon/containers/account_container';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
@ -170,12 +170,7 @@ export const Follows: React.FC<{
}
>
{displayedAccountIds.map((accountId) => (
<Account
/* @ts-expect-error inferred props are wrong */
id={accountId}
key={accountId}
withBio
/>
<Account id={accountId} key={accountId} withBio />
))}
</ScrollableList>

View file

@ -11,13 +11,13 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
import ColumnHeader from '../../components/column_header';
import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
const messages = defineMessages({
@ -88,7 +88,7 @@ class Reblogs extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
<Account key={id} id={id} />,
)}
</ScrollableList>

View file

@ -0,0 +1,43 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAppSelector } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
title: {
id: 'confirmations.follow_to_list.title',
defaultMessage: 'Follow user?',
},
confirm: {
id: 'confirmations.follow_to_list.confirm',
defaultMessage: 'Follow and add to list',
},
});
export const ConfirmFollowToListModal: React.FC<
{
accountId: string;
onConfirm: () => void;
} & BaseConfirmationModalProps
> = ({ accountId, onConfirm, onClose }) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(accountId));
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={
<FormattedMessage
id='confirmations.follow_to_list.message'
defaultMessage='You need to be following {name} to add them to a list.'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
}
confirm={intl.formatMessage(messages.confirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View file

@ -9,3 +9,4 @@ export { ConfirmEditStatusModal } from './edit_status';
export { ConfirmUnfollowModal } from './unfollow';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';

View file

@ -41,6 +41,7 @@ import {
ConfirmUnfollowModal,
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
ConfirmFollowToListModal,
} from './confirmation_modals';
import FocalPointModal from './focal_point_modal';
import ImageModal from './image_modal';
@ -65,6 +66,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,

View file

@ -101,6 +101,7 @@
"annual_report.summary.highlighted_post.possessive": "του χρήστη {name}",
"annual_report.summary.most_used_app.most_used_app": "πιο χρησιμοποιημένη εφαρμογή",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "πιο χρησιμοποιημένη ετικέτα",
"annual_report.summary.most_used_hashtag.none": "Κανένα",
"annual_report.summary.new_posts.new_posts": "νέες αναρτήσεις",
"annual_report.summary.percentile.text": "<topLabel>Αυτό σε βάζει στην κορυφή του </topLabel><percentage></percentage><bottomLabel>των χρηστών του Mastodon.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Δεν θα το πούμε στον Bernie.",
@ -128,6 +129,7 @@
"bundle_column_error.routing.body": "Η επιθυμητή σελίδα δεν βρέθηκε. Είναι σωστό το URL στο πεδίο διευθύνσεων;",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Κλείσιμο",
"bundle_modal_error.message": "Κάτι πήγε στραβά κατά τη φόρτωση αυτής της οθόνης.",
"bundle_modal_error.retry": "Δοκίμασε ξανά",
"closed_registrations.other_server_instructions": "Καθώς το Mastodon είναι αποκεντρωμένο, μπορείς να δημιουργήσεις λογαριασμό σε άλλον διακομιστή αλλά να συνεχίσεις να αλληλεπιδράς με αυτόν.",
"closed_registrations_modal.description": "Η δημιουργία λογαριασμού στον {domain} προς το παρόν δεν είναι δυνατή, αλλά λάβε υπόψη ότι δεν χρειάζεσαι λογαριασμό ειδικά στον {domain} για να χρησιμοποιήσεις το Mastodon.",
@ -138,13 +140,16 @@
"column.blocks": "Αποκλεισμένοι χρήστες",
"column.bookmarks": "Σελιδοδείκτες",
"column.community": "Τοπική ροή",
"column.create_list": "Δημιουργία λίστας",
"column.direct": "Ιδιωτικές αναφορές",
"column.directory": "Περιήγηση στα προφίλ",
"column.domain_blocks": "Αποκλεισμένοι τομείς",
"column.edit_list": "Επεξεργασία λίστας",
"column.favourites": "Αγαπημένα",
"column.firehose": "Ζωντανές ροές",
"column.follow_requests": "Αιτήματα ακολούθησης",
"column.home": "Αρχική",
"column.list_members": "Διαχείριση μελών λίστας",
"column.lists": "Λίστες",
"column.mutes": "Αποσιωπημένοι χρήστες",
"column.notifications": "Ειδοποιήσεις",
@ -157,6 +162,7 @@
"column_header.pin": "Καρφίτσωμα",
"column_header.show_settings": "Εμφάνιση ρυθμίσεων",
"column_header.unpin": "Ξεκαρφίτσωμα",
"column_search.cancel": "Ακύρωση",
"column_subheading.settings": "Ρυθμίσεις",
"community.column_settings.local_only": "Τοπικά μόνο",
"community.column_settings.media_only": "Μόνο πολυμέσα",
@ -230,6 +236,8 @@
"disabled_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προς το παρόν απενεργοποιημένος.",
"dismissable_banner.community_timeline": "Αυτές είναι οι πιο πρόσφατες δημόσιες αναρτήσεις ατόμων των οποίων οι λογαριασμοί φιλοξενούνται στο {domain}.",
"dismissable_banner.dismiss": "Παράβλεψη",
"dismissable_banner.explore_links": "Αυτές οι ιστορίες ειδήσεων μοιράζονται περισσότερο στο fediverse σήμερα. Νεότερες ιστορίες ειδήσεων που δημοσιεύτηκαν από πιο διαφορετικά άτομα κατατάσσονται υψηλότερα.",
"dismissable_banner.explore_statuses": "Αυτές οι αναρτήσεις από όλο το fediverse κερδίζουν την προσοχή σήμερα. Νεότερες αναρτήσεις με περισσότερες ενισχύσεις και αγαπημένα κατατάσσονται υψηλότερα.",
"domain_block_modal.block": "Αποκλεισμός διακομιστή",
"domain_block_modal.block_account_instead": "Αποκλεισμός @{name} αντ' αυτού",
"domain_block_modal.they_can_interact_with_old_posts": "Άτομα από αυτόν τον διακομιστή μπορούν να αλληλεπιδράσουν με τις παλιές αναρτήσεις σου.",

View file

@ -116,7 +116,7 @@
"annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag",
"annual_report.summary.most_used_hashtag.none": "None",
"annual_report.summary.new_posts.new_posts": "new posts",
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>",
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",
"annual_report.summary.thanks": "Thanks for being part of Mastodon!",
"antennas.accounts": "{count} accounts",
@ -342,6 +342,9 @@
"confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.edit.title": "Overwrite post?",
"confirmations.follow_to_list.confirm": "Follow and add to list",
"confirmations.follow_to_list.message": "You need to be following {name} to add them to a list.",
"confirmations.follow_to_list.title": "Follow user?",
"confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.logout.title": "Log out?",
@ -644,7 +647,7 @@
"lists.replies_policy.none": "No one",
"lists.save": "Save",
"lists.save_to_edit_member": "You can edit list members after saving.",
"lists.search_placeholder": "Search people you follow",
"lists.search": "Search",
"lists.show_replies_to": "Include replies from list members to",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading…",

View file

@ -0,0 +1 @@
{}

View file

@ -89,7 +89,7 @@
"announcement.announcement": "Объявление",
"annual_report.summary.archetype.booster": "Репостер",
"annual_report.summary.archetype.lurker": "Молчун",
"annual_report.summary.archetype.oracle": "Шаман",
"annual_report.summary.archetype.oracle": "Гуру",
"annual_report.summary.archetype.pollster": "Опросчик",
"annual_report.summary.archetype.replier": "Душа компании",
"annual_report.summary.followers.followers": "подписчиков",
@ -284,7 +284,7 @@
"empty_column.account_timeline": "Здесь нет постов!",
"empty_column.account_unavailable": "Профиль недоступен",
"empty_column.blocks": "Вы ещё никого не заблокировали.",
"empty_column.bookmarked_statuses": "У вас пока нет постов в закладках. Как добавите один, он отобразится здесь.",
"empty_column.bookmarked_statuses": "У вас пока нет закладок. Когда вы добавляете пост в закладки, он появляется здесь.",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
"empty_column.direct": "У вас пока нет личных сообщений. Как только вы отправите или получите сообщение, оно появится здесь.",
"empty_column.domain_blocks": "Скрытых доменов пока нет.",
@ -293,9 +293,9 @@
"empty_column.favourites": "Никто ещё не добавил этот пост в «Избранное». Как только кто-то это сделает, это отобразится здесь.",
"empty_column.follow_requests": "Вам ещё не приходили запросы на подписку. Все новые запросы будут показаны здесь.",
"empty_column.followed_tags": "Вы еще не подписались ни на один хэштег. Когда вы это сделаете, они появятся здесь.",
"empty_column.hashtag": "С этим хэштегом пока ещё ничего не постили.",
"empty_column.hashtag": "С этим хэштегом пока ещё ничего не публиковали.",
"empty_column.home": "Ваша лента совсем пуста! Подписывайтесь на других, чтобы заполнить её.",
"empty_column.list": "В этом списке пока ничего нет.",
"empty_column.list": "В этом списке пока ничего нет. Когда пользователи в списке публикуют новые посты, они появляются здесь.",
"empty_column.mutes": "Вы ещё никого не добавляли в список игнорируемых.",
"empty_column.notification_requests": "Здесь ничего нет! Когда вы получите новые уведомления, они здесь появятся согласно вашим настройкам.",
"empty_column.notifications": "У вас пока нет уведомлений. Взаимодействуйте с другими, чтобы завести разговор.",
@ -566,7 +566,7 @@
"notification.moderation_warning.action_sensitive": "С этого момента ваши сообщения будут помечены как деликатные.",
"notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
"notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.",
"notification.own_poll": "Ваш опрос закончился",
"notification.own_poll": "Ваш опрос завершился",
"notification.poll": "Голосование, в котором вы приняли участие, завершилось",
"notification.reblog": "{name} продвинул(а) ваш пост",
"notification.reblog.name_and_others_with_link": "{name} и ещё <a>{count, plural, one {# пользователь} few {# пользователя} other {# пользователей}}</a> продвинули ваш пост",
@ -575,7 +575,7 @@
"notification.relationships_severance_event.domain_block": "Администратор {from} заблокировал {target} включая {followersCount} ваших подписчиков и {followingCount, plural, one {# аккаунт} few {# аккаунта} other {# аккаунтов}}, на которые вы подписаны.",
"notification.relationships_severance_event.learn_more": "Узнать больше",
"notification.relationships_severance_event.user_domain_block": "Вы заблокировали {target} включая {followersCount} ваших подписчиков и {followingCount, plural, one {# аккаунт} few {# аккаунта} other {# аккаунтов}}, на которые вы подписаны.",
"notification.status": "{name} только что запостил",
"notification.status": "{name} опубликовал(а) новый пост",
"notification.update": "{name} изменил(а) пост",
"notification_requests.accept": "Принять",
"notification_requests.accept_multiple": "{count, plural, one {Принять # запрос…} few {Принять # запроса…} other {Принять # запросов…}}",
@ -722,7 +722,7 @@
"report.close": "Готово",
"report.comment.title": "Есть ли что-нибудь ещё, что нам стоит знать?",
"report.forward": "Переслать в {target}",
"report.forward_hint": "Эта учётная запись расположена на другом узле. Отправить туда анонимную копию вашей жалобы?",
"report.forward_hint": "Эта учётная запись расположена на другом сервере. Отправить туда анонимную копию вашей жалобы?",
"report.mute": "Игнорировать",
"report.mute_explanation": "Вы не будете видеть их посты. Они по-прежнему могут подписываться на вас и видеть ваши посты, но не будут знать, что они в списке игнорируемых.",
"report.next": "Далее",
@ -837,7 +837,7 @@
"status.replied_to": "Ответил(а) {name}",
"status.reply": "Ответить",
"status.replyAll": "Ответить всем",
"status.report": "Пожаловаться",
"status.report": "Пожаловаться на @{name}",
"status.sensitive_warning": "Содержимое «деликатного характера»",
"status.share": "Поделиться",
"status.show_less_all": "Свернуть все спойлеры в ветке",
@ -875,6 +875,7 @@
"upload_form.drag_and_drop.on_drag_cancel": "Перетаскивание было отменено. Вложение медиа {item} было удалено.",
"upload_form.drag_and_drop.on_drag_end": "Медиа вложение {item} было удалено.",
"upload_form.drag_and_drop.on_drag_over": "Медиа вложение {item} было перемещено.",
"upload_form.drag_and_drop.on_drag_start": "Загружается медиафайл {item}.",
"upload_form.edit": "Изменить",
"upload_form.thumbnail": "Изменить обложку",
"upload_form.video_description": "Опишите видео для людей с нарушением слуха или зрения",

View file

@ -93,7 +93,7 @@
"annual_report.summary.archetype.pollster": "投票狂魔",
"annual_report.summary.archetype.replier": "评论区原住民",
"annual_report.summary.followers.followers": "关注者",
"annual_report.summary.followers.total": "{count} 人",
"annual_report.summary.followers.total": "{count} 人",
"annual_report.summary.here_it_is": "你的 {year} 年度回顾在此:",
"annual_report.summary.highlighted_post.by_favourites": "最受欢迎嘟嘟",
"annual_report.summary.highlighted_post.by_reblogs": "传播最广嘟嘟",
@ -102,10 +102,10 @@
"annual_report.summary.most_used_app.most_used_app": "最常用的应用",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "最常用的话题",
"annual_report.summary.most_used_hashtag.none": "无",
"annual_report.summary.new_posts.new_posts": "嘟",
"annual_report.summary.new_posts.new_posts": "新嘟嘟",
"annual_report.summary.percentile.text": "<topLabel>这使你跻身 Mastodon 用户的前</topLabel><percentage></percentage><bottomLabel></bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "我们打死也不会告诉扣税国王的(他知道的话要来收你发嘟税了)。",
"annual_report.summary.thanks": "谢你这一年和 Mastodon 上的大家一起嘟嘟!",
"annual_report.summary.percentile.we_wont_tell_bernie": "我们打死也不会告诉扣税国王的。",
"annual_report.summary.thanks": "谢你这一年和 Mastodon 上的大家一起嘟嘟!",
"attachments_list.unprocessed": "(未处理)",
"audio.hide": "隐藏音频",
"block_modal.remote_users_caveat": "我们将要求服务器 {domain} 尊重你的决定。然而,我们无法保证对方一定遵从,因为某些服务器可能会以不同的方案处理屏蔽操作。公开嘟文仍然可能对未登录的用户可见。",
@ -392,7 +392,7 @@
"home.hide_announcements": "隐藏公告",
"home.pending_critical_update.body": "请尽快更新你的 Mastodon 服务器!",
"home.pending_critical_update.link": "查看更新",
"home.pending_critical_update.title": "紧急安全更新可用",
"home.pending_critical_update.title": "紧急安全更新!",
"home.show_announcements": "显示公告",
"ignore_notifications_modal.disclaimer": "Mastodon无法通知对方用户你忽略了他们的通知。忽略通知不会阻止消息本身的发送。",
"ignore_notifications_modal.filter_instead": "改为过滤",

View file

@ -523,6 +523,13 @@ a.sparkline {
}
}
.notification-group--annual-report {
.notification-group__icon,
.notification-group__main .link-button {
color: var(--indigo-3);
}
}
@supports not selector(::-webkit-scrollbar) {
html {
scrollbar-color: rgba($action-button-color, 0.25)
@ -530,13 +537,8 @@ a.sparkline {
}
}
::-webkit-scrollbar-thumb {
opacity: 0.25;
}
.notification-group--annual-report {
.notification-group__icon,
.notification-group__main .link-button {
color: var(--indigo-3);
.custom-scrollbars {
::-webkit-scrollbar-thumb {
opacity: 0.25;
}
}

View file

@ -59,24 +59,26 @@ table {
}
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbars {
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background-color: $action-button-color;
border: 2px var(--background-border-color);
border-radius: 12px;
width: 6px;
box-shadow: inset 0 0 0 2px var(--background-border-color);
}
::-webkit-scrollbar-thumb {
background-color: $action-button-color;
border: 2px var(--background-border-color);
border-radius: 12px;
width: 6px;
box-shadow: inset 0 0 0 2px var(--background-border-color);
}
::-webkit-scrollbar-track {
background-color: var(--background-border-color);
border-radius: 0px;
}
::-webkit-scrollbar-track {
background-color: var(--background-border-color);
border-radius: 0px;
}
::-webkit-scrollbar-corner {
background: transparent;
::-webkit-scrollbar-corner {
background: transparent;
}
}

View file

@ -131,6 +131,10 @@ module User::HasSettings
settings['reject_send_limited_to_suspects']
end
def setting_system_scrollbars_ui
settings['web.use_system_scrollbars']
end
def setting_noindex
settings['noindex']
end

View file

@ -59,6 +59,7 @@ class UserSettings
setting :use_custom_css, default: false
setting :content_font_size, default: 'medium', in: %w(medium large x_large xx_large)
setting :bookmark_category_needed, default: false
setting :use_system_scrollbars, default: false
setting :disable_swiping, default: false
setting :disable_hover_cards, default: false
setting :delete_modal, default: true

View file

@ -60,6 +60,7 @@
= ff.input :'web.disable_swiping', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_disable_swiping')
= ff.input :'web.disable_hover_cards', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_disable_hover_cards')
= ff.input :'web.use_system_font', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_system_font_ui')
= ff.input :'web.use_system_scrollbars', wrapper: :with_label, hint: I18n.t('simple_form.hints.defaults.setting_system_scrollbars_ui'), label: I18n.t('simple_form.labels.defaults.setting_system_scrollbars_ui')
.fields-group
= ff.input :'web.content_font_size',

View file

@ -0,0 +1 @@
nan:

View file

@ -61,6 +61,7 @@ bg:
demote: Понижаване
destroyed_msg: Данните на %{username} вече са на опашка за незабавно изтриване
disable: Замразяване
disable_sign_in_token_auth: Изключване на удостоверяването с маркер по е-поща
disable_two_factor_authentication: Изключване на 2факт. удостов.
disabled: Замразено
display_name: Име на показ
@ -69,6 +70,7 @@ bg:
email: Имейл
email_status: Състояние на имейл
enable: Размразяване
enable_sign_in_token_auth: Задействане на удостоверяването с маркер по е-поща
enabled: Включено
enabled_msg: Успешно размразяване на акаунта на %{username}
followers: Последователи
@ -133,6 +135,7 @@ bg:
resubscribe: Абониране пак
role: Роля
search: Търсене
search_same_email_domain: Други потребители със същия домейн за е-поща
search_same_ip: Други потребители със същия IP
security: Сигурност
security_measures:
@ -173,10 +176,12 @@ bg:
approve_appeal: Одобряване на обжалването
approve_user: Одобряване на потребител
assigned_to_self_report: Назначете доклад
change_email_user: Промяна на имейл за потребител
change_role_user: Промяна на роля за потребител
confirm_user: Потвърждаване на потребител
create_account_warning: Създаване на предупреждение
create_announcement: Създаване на оповестяване
create_canonical_email_block: Сътворяване на блоккиране за е-поща
create_custom_emoji: Създаване на персонализирано емоджи
create_domain_allow: Създаване на позволение за домейна
create_domain_block: Създаване на блокиране за домейна
@ -916,6 +921,8 @@ bg:
allow_provider: Позволяване на публикуващия
confirm_allow: Наистина ли искате да позволите избраните връзки?
confirm_allow_provider: Наистина ли искате да позволите избраните доставчици?
confirm_disallow: Наистина ли искате да забраните избраните връзки?
confirm_disallow_provider: Наистина ли искате да забраните избраните доставчици?
description_html: Това са връзки, които в момента са много пъти споделяни от акаунти, чиито публикации сървърът ви вижда. Може да помогне на потребителите ви да разберат какво се случва по света. Никоя връзка няма да се показва публично, докато не одобрите публикуващия. Може още и да одобрявате или отхвърляте отделни връзки.
disallow: Забранявам връзката
disallow_provider: Забраняване на публикуващия
@ -941,13 +948,15 @@ bg:
allow_account: Позволяване на автора
confirm_allow: Наистина ли искате да позволите избраните статуси?
confirm_allow_account: Наистина ли искате да позволите избраните акаунти?
description_html: Това са публикации, за които сървърът ви знае, че са често споделяни или харесвани в момента. Това може да помогне на вашите нови и завръщащи се потребители да открият повече хора за следване. Никоя от публикациите няма да бъде показана публично, докато не одобрите автора и докато авторът не позволи акаунтът му да бъде предлган на другите. Също така можете да позволявате или отхвърляте отделни публикации.
confirm_disallow: Наистина ли искате да забраните избраните статуси?
confirm_disallow_account: Наистина ли искате да забраните избраните акаунти?
description_html: Има публикации, за които сървърът ви знае, че в момента са често споделяни или любими. Биха помогнали на вашите нови и завръщащи се потребители да открият повече хора за последване. Никоя от публикациите няма да се показва публично, докато не одобрите автора и докато авторът не позволи акаунтът му да се предлага на другите. Може също така да позволявате или отхвърляте отделни публикации.
disallow: Забраняване на публикацията
disallow_account: Забрана на автора
no_status_selected: Няма промяна, тъй като няма избрана нашумяла публикация
not_discoverable: Авторът не е избрал да е откриваем
shared_by:
one: Споделено или харесано веднъж
one: Еднократно споделено или любимо
other: Споделено или харесано %{friendly_count} пъти
title: Налагащи се публикации
tags:
@ -973,6 +982,7 @@ bg:
used_by_over_week:
one: Употребено от един човек през последната седмица
other: Използвано от %{count} души през последната седмица
title: Препоръки и насоки на развитие
trending: Изгряващи
warning_presets:
add_new: Добавяне на ново
@ -1059,6 +1069,7 @@ bg:
application_mailer:
notification_preferences: Промяна на предпочитанията за е-поща
salutation: "%{name},"
settings: 'Промяна на предпочитанията за имейл: %{link}'
unsubscribe: Стоп на абонамента
view: 'Преглед:'
view_profile: Преглед на профила
@ -1078,6 +1089,7 @@ bg:
hint_html: Просто още едно нещо! Трябва да потвърдим, че сте човек (това е с цел предпазване на нежелани съобщения!). Разгадайте капчата долу и щракнете на "Продължаване".
title: Проверка за сигурност
confirmations:
awaiting_review: Вашият адрес на е-поща е потвърден! Служителите на %{domain} сега разглеждат регистрацията ви. Ще получите е-писмо, ако одобрят акаунта ви!
awaiting_review_title: Вашата регистрация се преглежда
clicking_this_link: щракване на тази връзка
login_link: влизане
@ -1426,8 +1438,12 @@ bg:
confirmation_html: Наистина ли искате да спрете абонамента от получаването на %{type} за Mastodon в %{domain} към имейла си при %{email}? Може винаги пак да се абонирате от своите <a href="%{settings_path}">настройки за известяване по е-поща</a>.
emails:
notification_emails:
favourite: е-писма за известия с любими
follow: е-писма с известия за последване
follow_request: е-писма със заявки за следване
mention: е-писма с известия за споменаване
reblog: е-писма с известия за подсилване
resubscribe_html: Ако погрешка сте спрели абонамента, то може пак да се абонирате от своите <a href="%{settings_path}">настройки за известия по е-поща</a>.
success_html: Повече няма да получавате %{type} за Mastodon на %{domain} към имейла си при %{email}.
title: Спиране на абонамента
media_attachments:
@ -1482,8 +1498,8 @@ bg:
sign_up:
subject: "%{name} се регистрира"
favourite:
body: 'Вашата публикация беше харесана от %{name}:'
subject: "%{name} хареса вашата публикация"
body: 'Ваша публикация е любима за %{name}:'
subject: "%{name} означи като любима ваша публикация"
title: Нова харесана публикация
follow:
body: "%{name} те последва!"
@ -1745,8 +1761,8 @@ bg:
keep_polls_hint: Не изтрива запитвания
keep_self_bookmark: Запазване на публикации, добавени в отметки
keep_self_bookmark_hint: Не се изтриват ваши публикации, ако сте ги добавили към отметки
keep_self_fav: Задържане на публикации, които сте харесали
keep_self_fav_hint: Не се изтриват публикации, които сте харесали
keep_self_fav: Задържане на любимите ви публикации
keep_self_fav_hint: Да не се изтриват ваши публикации, ако са ви любими
min_age:
'1209600': 2 седмици
'15778476': 6 месеца
@ -1757,7 +1773,7 @@ bg:
'63113904': 2 години
'7889238': 3 месеца
min_age_label: Възрастов праг
min_favs: Запазване на харесани публикации поне
min_favs: Задържане поне на любимите публикации
min_favs_hint: Не се изтрива никоя от публикациите, които сте харесали поне толкова пъти. Оставете празно, за да изтриете публикациите независимо от броя харесвания
min_reblogs: Запазване на публикации с поне толкова раздувания
min_reblogs_hint: Не се изтриват ваши публикации, споделени поне толкова пъти. Оставете празно, за да изтриете публикациите независимо от броя на техния раздувания

View file

@ -0,0 +1 @@
nan:

View file

@ -0,0 +1 @@
nan:

View file

@ -281,7 +281,7 @@ fi:
reject_appeal_html: "%{name} hylkäsi käyttäjän %{target} valituksen moderointipäätöksestä"
reject_user_html: "%{name} hylkäsi käyttäjän %{target} rekisteröitymisen"
remove_avatar_user_html: "%{name} poisti käyttäjän %{target} profiilikuvan"
reopen_report_html: "%{name} avasi uudelleen raportin %{target}"
reopen_report_html: "%{name} avasi raportin %{target} uudelleen"
resend_user_html: "%{name} lähetti vahvistussähköpostiviestin uudelleen käyttäjälle %{target}"
reset_password_user_html: "%{name} palautti käyttäjän %{target} salasanan"
resolve_report_html: "%{name} ratkaisi raportin %{target}"
@ -828,7 +828,7 @@ fi:
batch:
add_to_report: Lisää raporttiin nro %{id}
remove_from_report: Poista raportista
report: Raportti
report: Raportoi
contents: Sisältö
deleted: Poistettu
favourites: Suosikit

1
config/locales/nan.yml Normal file
View file

@ -0,0 +1 @@
nan:

View file

@ -854,6 +854,7 @@ ru:
back_to_account: Назад к учётной записи
back_to_report: Вернуться к жалобе
batch:
add_to_report: Добавить к жалобе №%{id}
remove_from_report: Убрать из жалобы
report: Пожаловаться
contents: Содержание

View file

@ -84,6 +84,7 @@ en:
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
setting_stay_privacy: If you choose to enable this setting, please consider manually setting the visibility of the boost
setting_stop_emoji_reaction_streaming: Helps to save communication capacity.
setting_system_scrollbars_ui: Applies only to desktop browsers based on Safari and Chrome
setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed
username: You can use letters, numbers, and underscores
@ -324,6 +325,7 @@ en:
setting_stay_privacy: Keep visibility after post
setting_stop_emoji_reaction_streaming: Disable emoji reaction streamings
setting_system_font_ui: Use system's default font
setting_system_scrollbars_ui: Use system's default scrollbar
setting_theme: Site theme
setting_translatable_private: Allow other users translation of your private posts
setting_trends: Show today's trends

View file

@ -0,0 +1 @@
nan:

View file

@ -60,6 +60,7 @@ tr:
setting_display_media_default: Hassas olarak işaretlenmiş medyayı gizle
setting_display_media_hide_all: Medyayı her zaman gizle
setting_display_media_show_all: Medyayı her zaman göster
setting_system_scrollbars_ui: Yalnızca Safari ve Chrome tabanlı masaüstü tarayıcılar için geçerlidir
setting_use_blurhash: Gradyenler gizli görsellerin renklerine dayanır, ancak detayları gizler
setting_use_pending_items: Akışı otomatik olarak kaydırmak yerine, zaman çizelgesi güncellemelerini tek bir tıklamayla gizleyin
username: Harfleri, sayıları veya alt çizgi kullanabilirsiniz
@ -223,6 +224,7 @@ tr:
setting_hide_network: Sosyal grafiğini gizle
setting_reduce_motion: Animasyonlarda hareketi azalt
setting_system_font_ui: Sistemin varsayılan yazı tipini kullan
setting_system_scrollbars_ui: Sistemin varsayılan kaydırma çubuğunu kullan
setting_theme: Site teması
setting_trends: Bugünün gündemini göster
setting_unfollow_modal: Birini takip etmeden önce onay iletişim kutusu göster

View file

@ -53,7 +53,7 @@
"@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^2.1.1",
"@hello-pangea/dnd": "^17.0.0",
"@rails/ujs": "7.1.402",
"@rails/ujs": "7.1.500",
"@reduxjs/toolkit": "^2.0.1",
"@svgr/webpack": "^5.5.0",
"arrow-key-navigation": "^1.2.0",

View file

@ -2849,7 +2849,7 @@ __metadata:
"@gamestdio/websocket": "npm:^0.3.2"
"@github/webauthn-json": "npm:^2.1.1"
"@hello-pangea/dnd": "npm:^17.0.0"
"@rails/ujs": "npm:7.1.402"
"@rails/ujs": "npm:7.1.500"
"@reduxjs/toolkit": "npm:^2.0.1"
"@svgr/webpack": "npm:^5.5.0"
"@testing-library/dom": "npm:^10.2.0"
@ -3148,10 +3148,10 @@ __metadata:
languageName: node
linkType: hard
"@rails/ujs@npm:7.1.402":
version: 7.1.402
resolution: "@rails/ujs@npm:7.1.402"
checksum: 10c0/ccab74b8013ed8a8ab8d7497d0fa510a6ec079725b5fcf679936d80c342940e462b60243ad2cb98128f29db5708a094e319767e8f33a18eb63ceb745de63d1e0
"@rails/ujs@npm:7.1.500":
version: 7.1.500
resolution: "@rails/ujs@npm:7.1.500"
checksum: 10c0/365f9a3944454d64c83463de017d9be7064494d6376c1f4d8cbff38c0f278bac7d9ab85f19b31abb70f0e775f30b64ad682fd4545bc27b5d91baef3618642b9f
languageName: node
linkType: hard