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

Merge upstream changes up to edeae945c0
shrike
Claire 2024-08-21 20:31:08 +02:00 committed by GitHub
commit 0cd60fdb82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 1078 additions and 606 deletions

View File

@ -281,8 +281,8 @@ GEM
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
formatador (1.1.0) formatador (1.1.0)
fugit (1.10.1) fugit (1.11.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4) raabro (~> 1.4)
fuubar (2.5.1) fuubar (2.5.1)
rspec-core (~> 3.0) rspec-core (~> 3.0)

View File

@ -437,12 +437,12 @@ export function unpinFail(status, error) {
}; };
} }
function toggleReblogWithoutConfirmation(status, privacy) { function toggleReblogWithoutConfirmation(status, visibility) {
return (dispatch) => { return (dispatch) => {
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') })); dispatch(unreblog({ statusId: status.get('id') }));
} else { } else {
dispatch(reblog({ statusId: status.get('id'), privacy })); dispatch(reblog({ statusId: status.get('id'), visibility }));
} }
}; };
} }

View File

@ -11,6 +11,7 @@ import type {
} from 'flavours/glitch/api_types/notifications'; } from 'flavours/glitch/api_types/notifications';
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications'; import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses'; import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
import { usePendingItems } from 'flavours/glitch/initial_state';
import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups'; import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
import { import {
selectSettingsNotificationsExcludedTypes, selectSettingsNotificationsExcludedTypes,
@ -103,6 +104,28 @@ export const fetchNotificationsGap = createDataLoadingThunk(
}, },
); );
export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotifications({
max_id: undefined,
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
since_id: usePendingItems
? getState().notificationGroups.groups.find(
(group) => group.type !== 'gap',
)?.page_max_id
: undefined,
});
},
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const processNewNotificationForGroups = createAppAsyncThunk( export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew', 'notificationGroups/processNew',
(notification: ApiNotificationJSON, { dispatch, getState }) => { (notification: ApiNotificationJSON, { dispatch, getState }) => {

View File

@ -10,7 +10,7 @@ import {
deleteAnnouncement, deleteAnnouncement,
} from './announcements'; } from './announcements';
import { updateConversations } from './conversations'; import { updateConversations } from './conversations';
import { processNewNotificationForGroups, refreshStaleNotificationGroups } from './notification_groups'; import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications'; import { updateNotifications, expandNotifications } from './notifications';
import { updateStatus } from './statuses'; import { updateStatus } from './statuses';
import { import {
@ -37,7 +37,7 @@ const randomUpTo = max =>
* @param {string} channelName * @param {string} channelName
* @param {Object.<string, string>} params * @param {Object.<string, string>} params
* @param {Object} options * @param {Object} options
* @param {function(Function): Promise<void>} [options.fallback] * @param {function(Function, Function): Promise<void>} [options.fallback]
* @param {function(): void} [options.fillGaps] * @param {function(): void} [options.fillGaps]
* @param {function(object): boolean} [options.accept] * @param {function(object): boolean} [options.accept]
* @returns {function(): void} * @returns {function(): void}
@ -52,11 +52,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
let pollingId; let pollingId;
/** /**
* @param {function(Function): Promise<void>} fallback * @param {function(Function, Function): Promise<void>} fallback
*/ */
const useFallback = async fallback => { const useFallback = async fallback => {
await fallback(dispatch); await fallback(dispatch, getState);
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
}; };
@ -139,10 +139,23 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/** /**
* @param {Function} dispatch * @param {Function} dispatch
* @param {Function} getState
*/ */
async function refreshHomeTimelineAndNotification(dispatch) { async function refreshHomeTimelineAndNotification(dispatch, getState) {
await dispatch(expandHomeTimeline({ maxId: undefined })); await dispatch(expandHomeTimeline({ maxId: undefined }));
await dispatch(expandNotifications({}));
// TODO: remove this once the groups feature replaces the previous one
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
// TODO: polling for merged notifications
try {
await dispatch(pollRecentGroupNotifications());
} catch (error) {
// TODO
}
} else {
await dispatch(expandNotifications({}));
}
await dispatch(fetchAnnouncements()); await dispatch(fetchAnnouncements());
} }

View File

@ -4,6 +4,7 @@ import type { ApiNotificationGroupsResultJSON } from 'flavours/glitch/api_types/
export const apiFetchNotifications = async (params?: { export const apiFetchNotifications = async (params?: {
exclude_types?: string[]; exclude_types?: string[];
max_id?: string; max_id?: string;
since_id?: string;
}) => { }) => {
const response = await api().request<ApiNotificationGroupsResultJSON>({ const response = await api().request<ApiNotificationGroupsResultJSON>({
method: 'GET', method: 'GET',

View File

@ -1,28 +1,23 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {
resource: JSX.Element; message: React.ReactNode;
label: React.ReactNode;
url: string; url: string;
className?: string; className?: string;
} }
export const TimelineHint: React.FC<Props> = ({ className, resource, url }) => ( export const TimelineHint: React.FC<Props> = ({
className,
message,
label,
url,
}) => (
<div className={classNames('timeline-hint', className)}> <div className={classNames('timeline-hint', className)}>
<strong> <p>{message}</p>
<FormattedMessage
id='timeline_hint.remote_resource_not_displayed'
defaultMessage='{resource} from other servers are not displayed.'
values={{ resource }}
/>
</strong>
<br />
<a href={url} target='_blank' rel='noopener noreferrer'> <a href={url} target='_blank' rel='noopener noreferrer'>
<FormattedMessage {label}
id='account.browse_more_on_origin_server'
defaultMessage='Browse more on the original profile'
/>
</a> </a>
</div> </div>
); );

View File

@ -12,6 +12,7 @@ import ProfileColumnHeader from 'flavours/glitch/features/account/components/pro
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountHidden } from 'flavours/glitch/selectors'; import { getAccountHidden } from 'flavours/glitch/selectors';
import { useAppSelector } from 'flavours/glitch/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags'; import { fetchFeaturedTags } from '../../actions/featured_tags';
@ -57,12 +58,22 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
}; };
}; };
const RemoteHint = ({ url }) => ( const RemoteHint = ({ accountId, url }) => {
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older posts' />} /> const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
); const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.posts_may_be_missing' defaultMessage='Some posts from this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_posts' defaultMessage='See more posts on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = { RemoteHint.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
}; };
class AccountTimeline extends ImmutablePureComponent { class AccountTimeline extends ImmutablePureComponent {
@ -176,12 +187,12 @@ class AccountTimeline extends ImmutablePureComponent {
} else if (hidden) { } else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />; emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (remote && statusIds.isEmpty()) { } else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else { } else {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />; emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
} }
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null; const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef}>

View File

@ -12,6 +12,7 @@ import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountHidden } from 'flavours/glitch/selectors'; import { getAccountHidden } from 'flavours/glitch/selectors';
import { useAppSelector } from 'flavours/glitch/store';
import { import {
lookupAccount, lookupAccount,
@ -50,12 +51,22 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
}; };
}; };
const RemoteHint = ({ url }) => ( const RemoteHint = ({ accountId, url }) => {
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} /> const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
); const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.followers_may_be_missing' defaultMessage='Followers for this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_followers' defaultMessage='See more followers on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = { RemoteHint.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
}; };
class Followers extends ImmutablePureComponent { class Followers extends ImmutablePureComponent {
@ -145,12 +156,12 @@ class Followers extends ImmutablePureComponent {
} else if (hideCollections && accountIds.isEmpty()) { } else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />; emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else { } else {
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />; emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
} }
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null; const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef}>

View File

@ -12,6 +12,7 @@ import { TimelineHint } from 'flavours/glitch/components/timeline_hint';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map';
import { getAccountHidden } from 'flavours/glitch/selectors'; import { getAccountHidden } from 'flavours/glitch/selectors';
import { useAppSelector } from 'flavours/glitch/store';
import { import {
lookupAccount, lookupAccount,
@ -50,12 +51,22 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
}; };
}; };
const RemoteHint = ({ url }) => ( const RemoteHint = ({ accountId, url }) => {
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} /> const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
); const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.follows_may_be_missing' defaultMessage='Follows for this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_follows' defaultMessage='See more follows on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = { RemoteHint.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
}; };
class Following extends ImmutablePureComponent { class Following extends ImmutablePureComponent {
@ -145,12 +156,12 @@ class Following extends ImmutablePureComponent {
} else if (hideCollections && accountIds.isEmpty()) { } else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />; emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else { } else {
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />; emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
} }
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null; const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef}>

View File

@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import { useAppSelector } from 'flavours/glitch/store';
export const DisplayedName: React.FC<{
accountIds: string[];
}> = ({ accountIds }) => {
const lastAccountId = accountIds[0] ?? '0';
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
if (!account) return null;
return (
<Link
to={`/@${account.acct}`}
title={`@${account.acct}`}
data-hover-card-account={account.id}
>
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
</Link>
);
};

View File

@ -1,51 +0,0 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { useAppSelector } from 'flavours/glitch/store';
export const NamesList: React.FC<{
accountIds: string[];
total: number;
seeMoreHref?: string;
}> = ({ accountIds, total, seeMoreHref }) => {
const lastAccountId = accountIds[0] ?? '0';
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
if (!account) return null;
const displayedName = (
<Link
to={`/@${account.acct}`}
title={`@${account.acct}`}
data-hover-card-account={account.id}
>
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
</Link>
);
if (total === 1) {
return displayedName;
}
if (seeMoreHref)
return (
<FormattedMessage
id='name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a>'
values={{
name: displayedName,
count: total - 1,
a: (chunks) => <Link to={seeMoreHref}>{chunks}</Link>,
}}
/>
);
return (
<FormattedMessage
id='name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}}'
values={{ name: displayedName, count: total - 1 }}
/>
);
};

View File

@ -6,13 +6,27 @@ import type { NotificationGroupAdminSignUp } from 'flavours/glitch/models/notifi
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total) => {
<FormattedMessage if (total === 1)
id='notification.admin.sign_up' return (
defaultMessage='{name} signed up' <FormattedMessage
values={values} id='notification.admin.sign_up'
/> defaultMessage='{name} signed up'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.admin.sign_up.name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}} signed up'
values={{
name: displayedName,
count: total - 1,
}}
/>
);
};
export const NotificationAdminSignUp: React.FC<{ export const NotificationAdminSignUp: React.FC<{
notification: NotificationGroupAdminSignUp; notification: NotificationGroupAdminSignUp;

View File

@ -1,5 +1,7 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import type { NotificationGroupFavourite } from 'flavours/glitch/models/notification_group'; import type { NotificationGroupFavourite } from 'flavours/glitch/models/notification_group';
import { useAppSelector } from 'flavours/glitch/store'; import { useAppSelector } from 'flavours/glitch/store';
@ -7,13 +9,29 @@ import { useAppSelector } from 'flavours/glitch/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
<FormattedMessage if (total === 1)
id='notification.favourite' return (
defaultMessage='{name} favorited your status' <FormattedMessage
values={values} id='notification.favourite'
/> defaultMessage='{name} favorited your status'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.favourite.name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post'
values={{
name: displayedName,
count: total - 1,
a: (chunks) =>
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
}}
/>
);
};
export const NotificationFavourite: React.FC<{ export const NotificationFavourite: React.FC<{
notification: NotificationGroupFavourite; notification: NotificationGroupFavourite;

View File

@ -10,13 +10,27 @@ import { useAppSelector } from 'flavours/glitch/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total) => {
<FormattedMessage if (total === 1)
id='notification.follow' return (
defaultMessage='{name} followed you' <FormattedMessage
values={values} id='notification.follow'
/> defaultMessage='{name} followed you'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.follow.name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}} followed you'
values={{
name: displayedName,
count: total - 1,
}}
/>
);
};
const FollowerCount: React.FC<{ accountId: string }> = ({ accountId }) => { const FollowerCount: React.FC<{ accountId: string }> = ({ accountId }) => {
const account = useAppSelector((s) => s.accounts.get(accountId)); const account = useAppSelector((s) => s.accounts.get(accountId));

View File

@ -21,13 +21,27 @@ const messages = defineMessages({
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
}); });
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total) => {
<FormattedMessage if (total === 1)
id='notification.follow_request' return (
defaultMessage='{name} has requested to follow you' <FormattedMessage
values={values} id='notification.follow_request'
/> defaultMessage='{name} has requested to follow you'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.follow_request.name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}} has requested to follow you'
values={{
name: displayedName,
count: total - 1,
}}
/>
);
};
export const NotificationFollowRequest: React.FC<{ export const NotificationFollowRequest: React.FC<{
notification: NotificationGroupFollowRequest; notification: NotificationGroupFollowRequest;

View File

@ -12,11 +12,13 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp
import { useAppDispatch } from 'flavours/glitch/store'; import { useAppDispatch } from 'flavours/glitch/store';
import { AvatarGroup } from './avatar_group'; import { AvatarGroup } from './avatar_group';
import { DisplayedName } from './displayed_name';
import { EmbeddedStatus } from './embedded_status'; import { EmbeddedStatus } from './embedded_status';
import { NamesList } from './names_list';
export type LabelRenderer = ( export type LabelRenderer = (
values: Record<string, React.ReactNode>, displayedName: JSX.Element,
total: number,
seeMoreHref?: string,
) => JSX.Element; ) => JSX.Element;
export const NotificationGroupWithStatus: React.FC<{ export const NotificationGroupWithStatus: React.FC<{
@ -50,15 +52,11 @@ export const NotificationGroupWithStatus: React.FC<{
const label = useMemo( const label = useMemo(
() => () =>
labelRenderer({ labelRenderer(
name: ( <DisplayedName accountIds={accountIds} />,
<NamesList count,
accountIds={accountIds} labelSeeMoreHref,
total={count} ),
seeMoreHref={labelSeeMoreHref}
/>
),
}),
[labelRenderer, accountIds, count, labelSeeMoreHref], [labelRenderer, accountIds, count, labelSeeMoreHref],
); );

View File

@ -1,5 +1,7 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import type { NotificationGroupReblog } from 'flavours/glitch/models/notification_group'; import type { NotificationGroupReblog } from 'flavours/glitch/models/notification_group';
import { useAppSelector } from 'flavours/glitch/store'; import { useAppSelector } from 'flavours/glitch/store';
@ -7,13 +9,29 @@ import { useAppSelector } from 'flavours/glitch/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
<FormattedMessage if (total === 1)
id='notification.reblog' return (
defaultMessage='{name} boosted your status' <FormattedMessage
values={values} id='notification.reblog'
/> defaultMessage='{name} boosted your status'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.reblog.name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post'
values={{
name: displayedName,
count: total - 1,
a: (chunks) =>
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
}}
/>
);
};
export const NotificationReblog: React.FC<{ export const NotificationReblog: React.FC<{
notification: NotificationGroupReblog; notification: NotificationGroupReblog;

View File

@ -6,11 +6,11 @@ import type { NotificationGroupStatus } from 'flavours/glitch/models/notificatio
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status'; import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName) => (
<FormattedMessage <FormattedMessage
id='notification.status' id='notification.status'
defaultMessage='{name} just posted' defaultMessage='{name} just posted'
values={values} values={{ name: displayedName }}
/> />
); );

View File

@ -6,11 +6,11 @@ import type { NotificationGroupUpdate } from 'flavours/glitch/models/notificatio
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status'; import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName) => (
<FormattedMessage <FormattedMessage
id='notification.update' id='notification.update'
defaultMessage='{name} edited a post' defaultMessage='{name} edited a post'
values={values} values={{ name: displayedName }}
/> />
); );

View File

@ -18,7 +18,7 @@ import { Icon } from 'flavours/glitch/components/icon';
import Status from 'flavours/glitch/containers/status_container'; import Status from 'flavours/glitch/containers/status_container';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { NamesList } from './names_list'; import { DisplayedName } from './displayed_name';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
export const NotificationWithStatus: React.FC<{ export const NotificationWithStatus: React.FC<{
@ -43,10 +43,7 @@ export const NotificationWithStatus: React.FC<{
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const label = useMemo( const label = useMemo(
() => () => labelRenderer(<DisplayedName accountIds={accountIds} />, count),
labelRenderer({
name: <NamesList accountIds={accountIds} total={count} />,
}),
[labelRenderer, accountIds, count], [labelRenderer, accountIds, count],
); );

View File

@ -655,7 +655,14 @@ class Status extends ImmutablePureComponent {
const isIndexable = !status.getIn(['account', 'noindex']); const isIndexable = !status.getIn(['account', 'noindex']);
if (!isLocal) { if (!isLocal) {
remoteHint = <TimelineHint className={classNames(!!descendants && 'timeline-hint--with-descendants')} url={status.get('url')} resource={<FormattedMessage id='timeline_hint.resources.replies' defaultMessage='Some replies' />} />; remoteHint = (
<TimelineHint
className={classNames(!!descendants && 'timeline-hint--with-descendants')}
url={status.get('url')}
message={<FormattedMessage id='hints.threads.replies_may_be_missing' defaultMessage='Replies from other servers may be missing.' />}
label={<FormattedMessage id='hints.threads.see_more' defaultMessage='See more replies on {domain}' values={{ domain: <strong>{status.getIn(['account', 'acct']).split('@')[1]}</strong> }} />}
/>
);
} }
const handlers = { const handlers = {

View File

@ -20,12 +20,16 @@ import {
mountNotifications, mountNotifications,
unmountNotifications, unmountNotifications,
refreshStaleNotificationGroups, refreshStaleNotificationGroups,
pollRecentNotifications,
} from 'flavours/glitch/actions/notification_groups'; } from 'flavours/glitch/actions/notification_groups';
import { import {
disconnectTimeline, disconnectTimeline,
timelineDelete, timelineDelete,
} from 'flavours/glitch/actions/timelines_typed'; } from 'flavours/glitch/actions/timelines_typed';
import type { ApiNotificationJSON } from 'flavours/glitch/api_types/notifications'; import type {
ApiNotificationJSON,
ApiNotificationGroupJSON,
} from 'flavours/glitch/api_types/notifications';
import { compareId } from 'flavours/glitch/compare_id'; import { compareId } from 'flavours/glitch/compare_id';
import { usePendingItems } from 'flavours/glitch/initial_state'; import { usePendingItems } from 'flavours/glitch/initial_state';
import { import {
@ -296,6 +300,106 @@ function commitLastReadId(state: NotificationGroupsState) {
} }
} }
function fillNotificationsGap(
groups: NotificationGroupsState['groups'],
gap: NotificationGap,
notifications: ApiNotificationGroupJSON[],
): NotificationGroupsState['groups'] {
// find the gap in the existing notifications
const gapIndex = groups.findIndex(
(groupOrGap) =>
groupOrGap.type === 'gap' &&
groupOrGap.sinceId === gap.sinceId &&
groupOrGap.maxId === gap.maxId,
);
if (gapIndex < 0)
// We do not know where to insert, let's return
return groups;
// Filling a disconnection gap means we're getting historical data
// about groups we may know or may not know about.
// The notifications timeline is split in two by the gap, with
// group information newer than the gap, and group information older
// than the gap.
// Filling a gap should not touch anything before the gap, so any
// information on groups already appearing before the gap should be
// discarded, while any information on groups appearing after the gap
// can be updated and re-ordered.
const oldestPageNotification = notifications.at(-1)?.page_min_id;
// replace the gap with the notifications + a new gap
const newerGroupKeys = groups
.slice(0, gapIndex)
.filter(isNotificationGroup)
.map((group) => group.group_key);
const toInsert: NotificationGroupsState['groups'] = notifications
.map((json) => createNotificationGroupFromJSON(json))
.filter((notification) => !newerGroupKeys.includes(notification.group_key));
const apiGroupKeys = (toInsert as NotificationGroup[]).map(
(group) => group.group_key,
);
const sinceId = gap.sinceId;
if (
notifications.length > 0 &&
!(
oldestPageNotification &&
sinceId &&
compareId(oldestPageNotification, sinceId) <= 0
)
) {
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
// Similarly, if we've fetched more than the gap's, this means we have completely filled it
toInsert.push({
type: 'gap',
maxId: notifications.at(-1)?.page_max_id,
sinceId,
} as NotificationGap);
}
// Remove older groups covered by the API
groups = groups.filter(
(groupOrGap) =>
groupOrGap.type !== 'gap' && !apiGroupKeys.includes(groupOrGap.group_key),
);
// Replace the gap with API results (+ the new gap if needed)
groups.splice(gapIndex, 1, ...toInsert);
// Finally, merge any adjacent gaps that could have been created by filtering
// groups earlier
mergeGaps(groups);
return groups;
}
// Ensure the groups list starts with a gap, mutating it to prepend one if needed
function ensureLeadingGap(
groups: NotificationGroupsState['groups'],
): NotificationGap {
if (groups[0]?.type === 'gap') {
// We're expecting new notifications, so discard the maxId if there is one
groups[0].maxId = undefined;
return groups[0];
} else {
const gap: NotificationGap = {
type: 'gap',
sinceId: groups[0]?.page_min_id,
};
groups.unshift(gap);
return gap;
}
}
export const notificationGroupsReducer = createReducer<NotificationGroupsState>( export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
initialState, initialState,
(builder) => { (builder) => {
@ -309,86 +413,36 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
updateLastReadId(state); updateLastReadId(state);
}) })
.addCase(fetchNotificationsGap.fulfilled, (state, action) => { .addCase(fetchNotificationsGap.fulfilled, (state, action) => {
const { notifications } = action.payload; state.groups = fillNotificationsGap(
state.groups,
// find the gap in the existing notifications action.meta.arg.gap,
const gapIndex = state.groups.findIndex( action.payload.notifications,
(groupOrGap) =>
groupOrGap.type === 'gap' &&
groupOrGap.sinceId === action.meta.arg.gap.sinceId &&
groupOrGap.maxId === action.meta.arg.gap.maxId,
); );
state.isLoading = false;
if (gapIndex < 0) updateLastReadId(state);
// We do not know where to insert, let's return })
return; .addCase(pollRecentNotifications.fulfilled, (state, action) => {
if (usePendingItems) {
// Filling a disconnection gap means we're getting historical data const gap = ensureLeadingGap(state.pendingGroups);
// about groups we may know or may not know about. state.pendingGroups = fillNotificationsGap(
state.pendingGroups,
// The notifications timeline is split in two by the gap, with gap,
// group information newer than the gap, and group information older action.payload.notifications,
// than the gap. );
} else {
// Filling a gap should not touch anything before the gap, so any const gap = ensureLeadingGap(state.groups);
// information on groups already appearing before the gap should be state.groups = fillNotificationsGap(
// discarded, while any information on groups appearing after the gap state.groups,
// can be updated and re-ordered. gap,
action.payload.notifications,
const oldestPageNotification = notifications.at(-1)?.page_min_id;
// replace the gap with the notifications + a new gap
const newerGroupKeys = state.groups
.slice(0, gapIndex)
.filter(isNotificationGroup)
.map((group) => group.group_key);
const toInsert: NotificationGroupsState['groups'] = notifications
.map((json) => createNotificationGroupFromJSON(json))
.filter(
(notification) => !newerGroupKeys.includes(notification.group_key),
); );
const apiGroupKeys = (toInsert as NotificationGroup[]).map(
(group) => group.group_key,
);
const sinceId = action.meta.arg.gap.sinceId;
if (
notifications.length > 0 &&
!(
oldestPageNotification &&
sinceId &&
compareId(oldestPageNotification, sinceId) <= 0
)
) {
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
// Similarly, if we've fetched more than the gap's, this means we have completely filled it
toInsert.push({
type: 'gap',
maxId: notifications.at(-1)?.page_max_id,
sinceId,
} as NotificationGap);
} }
// Remove older groups covered by the API
state.groups = state.groups.filter(
(groupOrGap) =>
groupOrGap.type !== 'gap' &&
!apiGroupKeys.includes(groupOrGap.group_key),
);
// Replace the gap with API results (+ the new gap if needed)
state.groups.splice(gapIndex, 1, ...toInsert);
// Finally, merge any adjacent gaps that could have been created by filtering
// groups earlier
mergeGaps(state.groups);
state.isLoading = false; state.isLoading = false;
updateLastReadId(state); updateLastReadId(state);
trimNotifications(state);
}) })
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => { .addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
const notification = action.payload; const notification = action.payload;
@ -403,10 +457,11 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
}) })
.addCase(disconnectTimeline, (state, action) => { .addCase(disconnectTimeline, (state, action) => {
if (action.payload.timeline === 'home') { if (action.payload.timeline === 'home') {
if (state.groups.length > 0 && state.groups[0]?.type !== 'gap') { const groups = usePendingItems ? state.pendingGroups : state.groups;
state.groups.unshift({ if (groups.length > 0 && groups[0]?.type !== 'gap') {
groups.unshift({
type: 'gap', type: 'gap',
sinceId: state.groups[0]?.page_min_id, sinceId: groups[0]?.page_min_id,
}); });
} }
} }
@ -453,12 +508,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
} }
} }
} }
trimNotifications(state);
}); });
// Then build the consolidated list and clear pending groups // Then build the consolidated list and clear pending groups
state.groups = state.pendingGroups.concat(state.groups); state.groups = state.pendingGroups.concat(state.groups);
state.pendingGroups = []; state.pendingGroups = [];
mergeGaps(state.groups);
trimNotifications(state);
}) })
.addCase(updateScrollPosition.fulfilled, (state, action) => { .addCase(updateScrollPosition.fulfilled, (state, action) => {
state.scrolledToTop = action.payload.top; state.scrolledToTop = action.payload.top;
@ -518,13 +574,21 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
}, },
) )
.addMatcher( .addMatcher(
isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending), isAnyOf(
fetchNotifications.pending,
fetchNotificationsGap.pending,
pollRecentNotifications.pending,
),
(state) => { (state) => {
state.isLoading = true; state.isLoading = true;
}, },
) )
.addMatcher( .addMatcher(
isAnyOf(fetchNotifications.rejected, fetchNotificationsGap.rejected), isAnyOf(
fetchNotifications.rejected,
fetchNotificationsGap.rejected,
pollRecentNotifications.rejected,
),
(state) => { (state) => {
state.isLoading = false; state.isLoading = false;
}, },

View File

@ -130,21 +130,11 @@
.older { .older {
float: left; float: left;
padding-inline-start: 0; padding-inline-start: 0;
.fa {
display: inline-block;
margin-inline-end: 5px;
}
} }
.newer { .newer {
float: right; float: right;
padding-inline-start: 0; padding-inline-start: 0;
.fa {
display: inline-block;
margin-inline-start: 5px;
}
} }
.disabled { .disabled {

View File

@ -122,10 +122,6 @@ $content-width: 840px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
i.fa {
margin-inline-end: 5px;
}
&:hover { &:hover {
color: $primary-text-color; color: $primary-text-color;
transition: all 100ms linear; transition: all 100ms linear;
@ -306,10 +302,6 @@ $content-width: 840px;
box-shadow: none; box-shadow: none;
} }
.directory__tag .table-action-link .fa {
color: inherit;
}
.directory__tag h4 { .directory__tag h4 {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;

View File

@ -3073,10 +3073,6 @@ $ui-header-logo-wordmark-width: 99px;
padding-inline-end: 30px; padding-inline-end: 30px;
} }
.search__icon .fa {
top: 15px;
}
.scrollable { .scrollable {
overflow: visible; overflow: visible;
@ -4422,11 +4418,12 @@ a.status-card {
.timeline-hint { .timeline-hint {
text-align: center; text-align: center;
color: $darker-text-color; color: $dark-text-color;
padding: 15px; padding: 16px;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
cursor: default; font-size: 14px;
line-height: 21px;
strong { strong {
font-weight: 500; font-weight: 500;
@ -4443,10 +4440,10 @@ a.status-card {
color: lighten($highlight-text-color, 4%); color: lighten($highlight-text-color, 4%);
} }
} }
}
.timeline-hint--with-descendants { &--with-descendants {
border-top: 1px solid var(--background-border-color); border-top: 1px solid var(--background-border-color);
}
} }
.regeneration-indicator { .regeneration-indicator {
@ -6725,7 +6722,7 @@ a.status-card {
} }
.boost-modal__container { .boost-modal__container {
overflow-x: scroll; overflow-y: auto;
padding: 10px; padding: 10px;
.status { .status {

View File

@ -113,10 +113,6 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.fa {
flex: 0 0 auto;
}
strong { strong {
font-weight: 700; font-weight: 700;
} }

View File

@ -931,10 +931,6 @@ code {
font-weight: 700; font-weight: 700;
} }
} }
.fa {
font-weight: 400;
}
} }
} }
} }

View File

@ -96,14 +96,6 @@ body.rtl {
no-repeat left 8px center / auto 16px; no-repeat left 8px center / auto 16px;
} }
.fa-chevron-left::before {
content: '\F054';
}
.fa-chevron-right::before {
content: '\F053';
}
.dismissable-banner, .dismissable-banner,
.warning-banner { .warning-banner {
&__action { &__action {

View File

@ -142,11 +142,6 @@ a.table-action-link {
color: $highlight-text-color; color: $highlight-text-color;
} }
i.fa {
font-weight: 400;
margin-inline-end: 5px;
}
&:first-child { &:first-child {
padding-inline-start: 0; padding-inline-start: 0;
} }

View File

@ -163,11 +163,6 @@
&__message { &__message {
margin-bottom: 15px; margin-bottom: 15px;
.fa {
margin-inline-end: 5px;
color: $darker-text-color;
}
} }
&__card { &__card {
@ -354,9 +349,7 @@
width: 21px; width: 21px;
} }
.fa { .icon {
font-size: 16px;
&.active { &.active {
color: $highlight-text-color; color: $highlight-text-color;
} }

View File

@ -437,12 +437,12 @@ export function unpinFail(status, error) {
}; };
} }
function toggleReblogWithoutConfirmation(status, privacy) { function toggleReblogWithoutConfirmation(status, visibility) {
return (dispatch) => { return (dispatch) => {
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') })); dispatch(unreblog({ statusId: status.get('id') }));
} else { } else {
dispatch(reblog({ statusId: status.get('id'), privacy })); dispatch(reblog({ statusId: status.get('id'), visibility }));
} }
}; };
} }

View File

@ -11,6 +11,7 @@ import type {
} from 'mastodon/api_types/notifications'; } from 'mastodon/api_types/notifications';
import { allNotificationTypes } from 'mastodon/api_types/notifications'; import { allNotificationTypes } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
import { usePendingItems } from 'mastodon/initial_state';
import type { NotificationGap } from 'mastodon/reducers/notification_groups'; import type { NotificationGap } from 'mastodon/reducers/notification_groups';
import { import {
selectSettingsNotificationsExcludedTypes, selectSettingsNotificationsExcludedTypes,
@ -103,6 +104,28 @@ export const fetchNotificationsGap = createDataLoadingThunk(
}, },
); );
export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotifications({
max_id: undefined,
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
since_id: usePendingItems
? getState().notificationGroups.groups.find(
(group) => group.type !== 'gap',
)?.page_max_id
: undefined,
});
},
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const processNewNotificationForGroups = createAppAsyncThunk( export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew', 'notificationGroups/processNew',
(notification: ApiNotificationJSON, { dispatch, getState }) => { (notification: ApiNotificationJSON, { dispatch, getState }) => {

View File

@ -10,7 +10,7 @@ import {
deleteAnnouncement, deleteAnnouncement,
} from './announcements'; } from './announcements';
import { updateConversations } from './conversations'; import { updateConversations } from './conversations';
import { processNewNotificationForGroups, refreshStaleNotificationGroups } from './notification_groups'; import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications'; import { updateNotifications, expandNotifications } from './notifications';
import { updateStatus } from './statuses'; import { updateStatus } from './statuses';
import { import {
@ -37,7 +37,7 @@ const randomUpTo = max =>
* @param {string} channelName * @param {string} channelName
* @param {Object.<string, string>} params * @param {Object.<string, string>} params
* @param {Object} options * @param {Object} options
* @param {function(Function): Promise<void>} [options.fallback] * @param {function(Function, Function): Promise<void>} [options.fallback]
* @param {function(): void} [options.fillGaps] * @param {function(): void} [options.fillGaps]
* @param {function(object): boolean} [options.accept] * @param {function(object): boolean} [options.accept]
* @returns {function(): void} * @returns {function(): void}
@ -52,11 +52,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
let pollingId; let pollingId;
/** /**
* @param {function(Function): Promise<void>} fallback * @param {function(Function, Function): Promise<void>} fallback
*/ */
const useFallback = async fallback => { const useFallback = async fallback => {
await fallback(dispatch); await fallback(dispatch, getState);
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
}; };
@ -139,10 +139,23 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/** /**
* @param {Function} dispatch * @param {Function} dispatch
* @param {Function} getState
*/ */
async function refreshHomeTimelineAndNotification(dispatch) { async function refreshHomeTimelineAndNotification(dispatch, getState) {
await dispatch(expandHomeTimeline({ maxId: undefined })); await dispatch(expandHomeTimeline({ maxId: undefined }));
await dispatch(expandNotifications({}));
// TODO: remove this once the groups feature replaces the previous one
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
// TODO: polling for merged notifications
try {
await dispatch(pollRecentGroupNotifications());
} catch (error) {
// TODO
}
} else {
await dispatch(expandNotifications({}));
}
await dispatch(fetchAnnouncements()); await dispatch(fetchAnnouncements());
} }

View File

@ -4,6 +4,7 @@ import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notific
export const apiFetchNotifications = async (params?: { export const apiFetchNotifications = async (params?: {
exclude_types?: string[]; exclude_types?: string[];
max_id?: string; max_id?: string;
since_id?: string;
}) => { }) => {
const response = await api().request<ApiNotificationGroupsResultJSON>({ const response = await api().request<ApiNotificationGroupsResultJSON>({
method: 'GET', method: 'GET',

View File

@ -1,28 +1,23 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {
resource: JSX.Element; message: React.ReactNode;
label: React.ReactNode;
url: string; url: string;
className?: string; className?: string;
} }
export const TimelineHint: React.FC<Props> = ({ className, resource, url }) => ( export const TimelineHint: React.FC<Props> = ({
className,
message,
label,
url,
}) => (
<div className={classNames('timeline-hint', className)}> <div className={classNames('timeline-hint', className)}>
<strong> <p>{message}</p>
<FormattedMessage
id='timeline_hint.remote_resource_not_displayed'
defaultMessage='{resource} from other servers are not displayed.'
values={{ resource }}
/>
</strong>
<br />
<a href={url} target='_blank' rel='noopener noreferrer'> <a href={url} target='_blank' rel='noopener noreferrer'>
<FormattedMessage {label}
id='account.browse_more_on_origin_server'
defaultMessage='Browse more on the original profile'
/>
</a> </a>
</div> </div>
); );

View File

@ -12,6 +12,7 @@ import BundleColumnError from 'mastodon/features/ui/components/bundle_column_err
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors'; import { getAccountHidden } from 'mastodon/selectors';
import { useAppSelector } from 'mastodon/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags'; import { fetchFeaturedTags } from '../../actions/featured_tags';
@ -59,12 +60,22 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
}; };
}; };
const RemoteHint = ({ url }) => ( const RemoteHint = ({ accountId, url }) => {
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older posts' />} /> const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
); const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.posts_may_be_missing' defaultMessage='Some posts from this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_posts' defaultMessage='See more posts on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = { RemoteHint.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
}; };
class AccountTimeline extends ImmutablePureComponent { class AccountTimeline extends ImmutablePureComponent {
@ -175,12 +186,12 @@ class AccountTimeline extends ImmutablePureComponent {
} else if (blockedBy) { } else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />; emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && statusIds.isEmpty()) { } else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else { } else {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />; emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
} }
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null; const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return ( return (
<Column> <Column>

View File

@ -12,6 +12,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors'; import { getAccountHidden } from 'mastodon/selectors';
import { useAppSelector } from 'mastodon/store';
import { import {
lookupAccount, lookupAccount,
@ -51,12 +52,22 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
}; };
}; };
const RemoteHint = ({ url }) => ( const RemoteHint = ({ accountId, url }) => {
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} /> const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
); const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.followers_may_be_missing' defaultMessage='Followers for this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_followers' defaultMessage='See more followers on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = { RemoteHint.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
}; };
class Followers extends ImmutablePureComponent { class Followers extends ImmutablePureComponent {
@ -141,12 +152,12 @@ class Followers extends ImmutablePureComponent {
} else if (hideCollections && accountIds.isEmpty()) { } else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />; emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else { } else {
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />; emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
} }
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null; const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return ( return (
<Column> <Column>

View File

@ -12,6 +12,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors'; import { getAccountHidden } from 'mastodon/selectors';
import { useAppSelector } from 'mastodon/store';
import { import {
lookupAccount, lookupAccount,
@ -51,12 +52,22 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
}; };
}; };
const RemoteHint = ({ url }) => ( const RemoteHint = ({ accountId, url }) => {
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} /> const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
); const domain = acct ? acct.split('@')[1] : undefined;
return (
<TimelineHint
url={url}
message={<FormattedMessage id='hints.profiles.follows_may_be_missing' defaultMessage='Follows for this profile may be missing.' />}
label={<FormattedMessage id='hints.profiles.see_more_follows' defaultMessage='See more follows on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
/>
);
};
RemoteHint.propTypes = { RemoteHint.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
}; };
class Following extends ImmutablePureComponent { class Following extends ImmutablePureComponent {
@ -141,12 +152,12 @@ class Following extends ImmutablePureComponent {
} else if (hideCollections && accountIds.isEmpty()) { } else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />; emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else { } else {
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />; emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
} }
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null; const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
return ( return (
<Column> <Column>

View File

@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import { useAppSelector } from 'mastodon/store';
export const DisplayedName: React.FC<{
accountIds: string[];
}> = ({ accountIds }) => {
const lastAccountId = accountIds[0] ?? '0';
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
if (!account) return null;
return (
<Link
to={`/@${account.acct}`}
title={`@${account.acct}`}
data-hover-card-account={account.id}
>
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
</Link>
);
};

View File

@ -1,51 +0,0 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { useAppSelector } from 'mastodon/store';
export const NamesList: React.FC<{
accountIds: string[];
total: number;
seeMoreHref?: string;
}> = ({ accountIds, total, seeMoreHref }) => {
const lastAccountId = accountIds[0] ?? '0';
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
if (!account) return null;
const displayedName = (
<Link
to={`/@${account.acct}`}
title={`@${account.acct}`}
data-hover-card-account={account.id}
>
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
</Link>
);
if (total === 1) {
return displayedName;
}
if (seeMoreHref)
return (
<FormattedMessage
id='name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a>'
values={{
name: displayedName,
count: total - 1,
a: (chunks) => <Link to={seeMoreHref}>{chunks}</Link>,
}}
/>
);
return (
<FormattedMessage
id='name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}}'
values={{ name: displayedName, count: total - 1 }}
/>
);
};

View File

@ -6,13 +6,27 @@ import type { NotificationGroupAdminSignUp } from 'mastodon/models/notification_
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total) => {
<FormattedMessage if (total === 1)
id='notification.admin.sign_up' return (
defaultMessage='{name} signed up' <FormattedMessage
values={values} id='notification.admin.sign_up'
/> defaultMessage='{name} signed up'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.admin.sign_up.name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}} signed up'
values={{
name: displayedName,
count: total - 1,
}}
/>
);
};
export const NotificationAdminSignUp: React.FC<{ export const NotificationAdminSignUp: React.FC<{
notification: NotificationGroupAdminSignUp; notification: NotificationGroupAdminSignUp;

View File

@ -1,5 +1,7 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import type { NotificationGroupFavourite } from 'mastodon/models/notification_group'; import type { NotificationGroupFavourite } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
@ -7,13 +9,29 @@ import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
<FormattedMessage if (total === 1)
id='notification.favourite' return (
defaultMessage='{name} favorited your status' <FormattedMessage
values={values} id='notification.favourite'
/> defaultMessage='{name} favorited your status'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.favourite.name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post'
values={{
name: displayedName,
count: total - 1,
a: (chunks) =>
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
}}
/>
);
};
export const NotificationFavourite: React.FC<{ export const NotificationFavourite: React.FC<{
notification: NotificationGroupFavourite; notification: NotificationGroupFavourite;

View File

@ -10,13 +10,27 @@ import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total) => {
<FormattedMessage if (total === 1)
id='notification.follow' return (
defaultMessage='{name} followed you' <FormattedMessage
values={values} id='notification.follow'
/> defaultMessage='{name} followed you'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.follow.name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}} followed you'
values={{
name: displayedName,
count: total - 1,
}}
/>
);
};
const FollowerCount: React.FC<{ accountId: string }> = ({ accountId }) => { const FollowerCount: React.FC<{ accountId: string }> = ({ accountId }) => {
const account = useAppSelector((s) => s.accounts.get(accountId)); const account = useAppSelector((s) => s.accounts.get(accountId));

View File

@ -21,13 +21,27 @@ const messages = defineMessages({
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
}); });
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total) => {
<FormattedMessage if (total === 1)
id='notification.follow_request' return (
defaultMessage='{name} has requested to follow you' <FormattedMessage
values={values} id='notification.follow_request'
/> defaultMessage='{name} has requested to follow you'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.follow_request.name_and_others'
defaultMessage='{name} and {count, plural, one {# other} other {# others}} has requested to follow you'
values={{
name: displayedName,
count: total - 1,
}}
/>
);
};
export const NotificationFollowRequest: React.FC<{ export const NotificationFollowRequest: React.FC<{
notification: NotificationGroupFollowRequest; notification: NotificationGroupFollowRequest;

View File

@ -12,11 +12,13 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useAppDispatch } from 'mastodon/store'; import { useAppDispatch } from 'mastodon/store';
import { AvatarGroup } from './avatar_group'; import { AvatarGroup } from './avatar_group';
import { DisplayedName } from './displayed_name';
import { EmbeddedStatus } from './embedded_status'; import { EmbeddedStatus } from './embedded_status';
import { NamesList } from './names_list';
export type LabelRenderer = ( export type LabelRenderer = (
values: Record<string, React.ReactNode>, displayedName: JSX.Element,
total: number,
seeMoreHref?: string,
) => JSX.Element; ) => JSX.Element;
export const NotificationGroupWithStatus: React.FC<{ export const NotificationGroupWithStatus: React.FC<{
@ -50,15 +52,11 @@ export const NotificationGroupWithStatus: React.FC<{
const label = useMemo( const label = useMemo(
() => () =>
labelRenderer({ labelRenderer(
name: ( <DisplayedName accountIds={accountIds} />,
<NamesList count,
accountIds={accountIds} labelSeeMoreHref,
total={count} ),
seeMoreHref={labelSeeMoreHref}
/>
),
}),
[labelRenderer, accountIds, count, labelSeeMoreHref], [labelRenderer, accountIds, count, labelSeeMoreHref],
); );

View File

@ -1,5 +1,7 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import type { NotificationGroupReblog } from 'mastodon/models/notification_group'; import type { NotificationGroupReblog } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
@ -7,13 +9,29 @@ import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
<FormattedMessage if (total === 1)
id='notification.reblog' return (
defaultMessage='{name} boosted your status' <FormattedMessage
values={values} id='notification.reblog'
/> defaultMessage='{name} boosted your status'
); values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.reblog.name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post'
values={{
name: displayedName,
count: total - 1,
a: (chunks) =>
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
}}
/>
);
};
export const NotificationReblog: React.FC<{ export const NotificationReblog: React.FC<{
notification: NotificationGroupReblog; notification: NotificationGroupReblog;

View File

@ -6,11 +6,11 @@ import type { NotificationGroupStatus } from 'mastodon/models/notification_group
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status'; import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName) => (
<FormattedMessage <FormattedMessage
id='notification.status' id='notification.status'
defaultMessage='{name} just posted' defaultMessage='{name} just posted'
values={values} values={{ name: displayedName }}
/> />
); );

View File

@ -6,11 +6,11 @@ import type { NotificationGroupUpdate } from 'mastodon/models/notification_group
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status'; import { NotificationWithStatus } from './notification_with_status';
const labelRenderer: LabelRenderer = (values) => ( const labelRenderer: LabelRenderer = (displayedName) => (
<FormattedMessage <FormattedMessage
id='notification.update' id='notification.update'
defaultMessage='{name} edited a post' defaultMessage='{name} edited a post'
values={values} values={{ name: displayedName }}
/> />
); );

View File

@ -15,7 +15,7 @@ import { Icon } from 'mastodon/components/icon';
import Status from 'mastodon/containers/status_container'; import Status from 'mastodon/containers/status_container';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { NamesList } from './names_list'; import { DisplayedName } from './displayed_name';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
export const NotificationWithStatus: React.FC<{ export const NotificationWithStatus: React.FC<{
@ -40,10 +40,7 @@ export const NotificationWithStatus: React.FC<{
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const label = useMemo( const label = useMemo(
() => () => labelRenderer(<DisplayedName accountIds={accountIds} />, count),
labelRenderer({
name: <NamesList accountIds={accountIds} total={count} />,
}),
[labelRenderer, accountIds, count], [labelRenderer, accountIds, count],
); );

View File

@ -629,7 +629,14 @@ class Status extends ImmutablePureComponent {
const isIndexable = !status.getIn(['account', 'noindex']); const isIndexable = !status.getIn(['account', 'noindex']);
if (!isLocal) { if (!isLocal) {
remoteHint = <TimelineHint className={classNames(!!descendants && 'timeline-hint--with-descendants')} url={status.get('url')} resource={<FormattedMessage id='timeline_hint.resources.replies' defaultMessage='Some replies' />} />; remoteHint = (
<TimelineHint
className={classNames(!!descendants && 'timeline-hint--with-descendants')}
url={status.get('url')}
message={<FormattedMessage id='hints.threads.replies_may_be_missing' defaultMessage='Replies from other servers may be missing.' />}
label={<FormattedMessage id='hints.threads.see_more' defaultMessage='See more replies on {domain}' values={{ domain: <strong>{status.getIn(['account', 'acct']).split('@')[1]}</strong> }} />}
/>
);
} }
const handlers = { const handlers = {

View File

@ -518,7 +518,17 @@
"notification.status": "{name} hat gerade etwas gepostet", "notification.status": "{name} hat gerade etwas gepostet",
"notification.update": "{name} bearbeitete einen Beitrag", "notification.update": "{name} bearbeitete einen Beitrag",
"notification_requests.accept": "Genehmigen", "notification_requests.accept": "Genehmigen",
"notification_requests.accept_multiple": "{count, plural, one {# Anfrage genehmigen …} other {# Anfragen genehmigen …}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Anfrage genehmigen} other {Anfragen genehmigen}}",
"notification_requests.confirm_accept_multiple.message": "Du bist dabei, {{count, plural, one {eine Benachrichtigungsanfrage} other {# Benachrichtigungsanfragen}} zu genehmigen. Möchtest du wirklich fortfahren?",
"notification_requests.confirm_accept_multiple.title": "Benachrichtigungsanfragen genehmigen?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Anfrage ablehnen} other {Anfragen ablehnen}}",
"notification_requests.confirm_dismiss_multiple.message": "Du bist dabei, {count, plural, one {eine Benachrichtigungsanfrage} other {# Benachrichtigungsanfragen}} abzulehnen. Du wirst nicht mehr ohne Weiteres auf {count, plural, one {sie} other {sie}} zugreifen können. Möchtest du wirklich fortfahren?",
"notification_requests.confirm_dismiss_multiple.title": "Benachrichtigungsanfragen ablehnen?",
"notification_requests.dismiss": "Ablehnen", "notification_requests.dismiss": "Ablehnen",
"notification_requests.dismiss_multiple": "{count, plural, one {# Anfrage ablehnen …} other {# Anfragen ablehnen …}}",
"notification_requests.edit_selection": "Bearbeiten",
"notification_requests.exit_selection": "Fertig",
"notification_requests.explainer_for_limited_account": "Benachrichtigungen von diesem Konto wurden gefiltert, weil es durch Moderator*innen eingeschränkt wurde.", "notification_requests.explainer_for_limited_account": "Benachrichtigungen von diesem Konto wurden gefiltert, weil es durch Moderator*innen eingeschränkt wurde.",
"notification_requests.explainer_for_limited_remote_account": "Benachrichtigungen von diesem Konto wurden gefiltert, weil deren Konto oder Server durch Moderator*innen eingeschränkt wurde.", "notification_requests.explainer_for_limited_remote_account": "Benachrichtigungen von diesem Konto wurden gefiltert, weil deren Konto oder Server durch Moderator*innen eingeschränkt wurde.",
"notification_requests.maximize": "Maximieren", "notification_requests.maximize": "Maximieren",

View File

@ -19,7 +19,6 @@
"account.block_domain": "Block domain {domain}", "account.block_domain": "Block domain {domain}",
"account.block_short": "Block", "account.block_short": "Block",
"account.blocked": "Blocked", "account.blocked": "Blocked",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Cancel follow", "account.cancel_follow_request": "Cancel follow",
"account.copy": "Copy link to profile", "account.copy": "Copy link to profile",
"account.direct": "Privately mention @{name}", "account.direct": "Privately mention @{name}",
@ -349,6 +348,14 @@
"hashtag.follow": "Follow hashtag", "hashtag.follow": "Follow hashtag",
"hashtag.unfollow": "Unfollow hashtag", "hashtag.unfollow": "Unfollow hashtag",
"hashtags.and_other": "…and {count, plural, other {# more}}", "hashtags.and_other": "…and {count, plural, other {# more}}",
"hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.",
"hints.profiles.follows_may_be_missing": "Follows for this profile may be missing.",
"hints.profiles.posts_may_be_missing": "Some posts from this profile may be missing.",
"hints.profiles.see_more_followers": "See more followers on {domain}",
"hints.profiles.see_more_follows": "See more follows on {domain}",
"hints.profiles.see_more_posts": "See more posts on {domain}",
"hints.threads.replies_may_be_missing": "Replies from other servers may be missing.",
"hints.threads.see_more": "See more replies on {domain}",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.hide_announcements": "Hide announcements", "home.hide_announcements": "Hide announcements",
@ -456,8 +463,6 @@
"mute_modal.title": "Mute user?", "mute_modal.title": "Mute user?",
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.", "mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.", "mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
"name_and_others": "{name} and {count, plural, one {# other} other {# others}}",
"name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a>",
"navigation_bar.about": "About", "navigation_bar.about": "About",
"navigation_bar.advanced_interface": "Open in advanced web interface", "navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.blocks": "Blocked users", "navigation_bar.blocks": "Blocked users",
@ -490,9 +495,13 @@
"notification.admin.report_statuses": "{name} reported {target} for {category}", "notification.admin.report_statuses": "{name} reported {target} for {category}",
"notification.admin.report_statuses_other": "{name} reported {target}", "notification.admin.report_statuses_other": "{name} reported {target}",
"notification.admin.sign_up": "{name} signed up", "notification.admin.sign_up": "{name} signed up",
"notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up",
"notification.favourite": "{name} favorited your post", "notification.favourite": "{name} favorited your post",
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.follow.name_and_others": "{name} and {count, plural, one {# other} other {# others}} followed you",
"notification.follow_request": "{name} has requested to follow you", "notification.follow_request": "{name} has requested to follow you",
"notification.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you",
"notification.label.mention": "Mention", "notification.label.mention": "Mention",
"notification.label.private_mention": "Private mention", "notification.label.private_mention": "Private mention",
"notification.label.private_reply": "Private reply", "notification.label.private_reply": "Private reply",
@ -510,6 +519,7 @@
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you voted in has ended", "notification.poll": "A poll you voted in has ended",
"notification.reblog": "{name} boosted your post", "notification.reblog": "{name} boosted your post",
"notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post",
"notification.relationships_severance_event": "Lost connections with {name}", "notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.", "notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.", "notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
@ -826,11 +836,6 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.replies": "Some replies",
"timeline_hint.resources.statuses": "Older posts",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}", "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} acaba de enviar un mensaje", "notification.status": "{name} acaba de enviar un mensaje",
"notification.update": "{name} editó un mensaje", "notification.update": "{name} editó un mensaje",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
"notification_requests.accept_multiple": "{count, plural, one {Aceptar # solicitud…} other {Aceptar # solicitudes…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar solicitud} other {Aceptar solicitudes}}",
"notification_requests.confirm_accept_multiple.message": "Estás a punto de aceptar {count, plural, one {una solicitud} other {# solicitudes}}. ¿Estás seguro de que querés continuar?",
"notification_requests.confirm_accept_multiple.title": "¿Aceptar solicitudes de notificación?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Descartar solicitud} other {Descartar solicitudes}}",
"notification_requests.confirm_dismiss_multiple.message": "Estás a punto de descartar {count, plural, one {una solicitud} other {# solicitudes}}. No vas a poder acceder fácilmente a {count, plural, one {ella} other {ellas}} de nuevo. ¿Estás seguro de que querés continuar?",
"notification_requests.confirm_dismiss_multiple.title": "¿Descartar solicitudes de notificación?",
"notification_requests.dismiss": "Descartar", "notification_requests.dismiss": "Descartar",
"notification_requests.dismiss_multiple": "{count, plural, one {Descartar # solicitud…} other {Descartar # solicitudes…}}",
"notification_requests.edit_selection": "Editar",
"notification_requests.exit_selection": "Listo",
"notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta fueron filtradas porque la misma fue limitada por un moderador.", "notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta fueron filtradas porque la misma fue limitada por un moderador.",
"notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta fueron filtradas porque la cuenta o su servidor fueron limitados por un moderador.", "notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta fueron filtradas porque la cuenta o su servidor fueron limitados por un moderador.",
"notification_requests.maximize": "Maximizar", "notification_requests.maximize": "Maximizar",

View File

@ -518,6 +518,8 @@
"notification.status": "{name} acaba de publicar", "notification.status": "{name} acaba de publicar",
"notification.update": "{name} editó una publicación", "notification.update": "{name} editó una publicación",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
"notification_requests.accept_multiple": "{count, plural, one {Aceptar # solicitud…} other {Aceptar # solicitudes…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar solicitud} other {Aceptar solicitudes}}",
"notification_requests.dismiss": "Descartar", "notification_requests.dismiss": "Descartar",
"notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta ha sido limitada por un moderador.", "notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta ha sido limitada por un moderador.",
"notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta o su servidor ha sido limitada por un moderador.", "notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta o su servidor ha sido limitada por un moderador.",

View File

@ -518,6 +518,8 @@
"notification.status": "{name} acaba de publicar", "notification.status": "{name} acaba de publicar",
"notification.update": "{name} editó una publicación", "notification.update": "{name} editó una publicación",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
"notification_requests.accept_multiple": "{count, plural, one {Aceptar # solicitud…} other {Aceptar # solicitudes…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar solicitud} other {Aceptar solicitudes}}",
"notification_requests.dismiss": "Descartar", "notification_requests.dismiss": "Descartar",
"notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta ha sido limitada por un moderador.", "notification_requests.explainer_for_limited_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta ha sido limitada por un moderador.",
"notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta o su servidor ha sido limitada por un moderador.", "notification_requests.explainer_for_limited_remote_account": "Las notificaciones de esta cuenta han sido filtradas porque la cuenta o su servidor ha sido limitada por un moderador.",

View File

@ -477,7 +477,7 @@
"navigation_bar.logout": "Kirjaudu ulos", "navigation_bar.logout": "Kirjaudu ulos",
"navigation_bar.mutes": "Mykistetyt käyttäjät", "navigation_bar.mutes": "Mykistetyt käyttäjät",
"navigation_bar.opened_in_classic_interface": "Julkaisut, profiilit ja tietyt muut sivut avautuvat oletuksena perinteiseen selainkäyttöliittymään.", "navigation_bar.opened_in_classic_interface": "Julkaisut, profiilit ja tietyt muut sivut avautuvat oletuksena perinteiseen selainkäyttöliittymään.",
"navigation_bar.personal": "Henkilökohtainen", "navigation_bar.personal": "Henkilökohtaiset",
"navigation_bar.pins": "Kiinnitetyt julkaisut", "navigation_bar.pins": "Kiinnitetyt julkaisut",
"navigation_bar.preferences": "Asetukset", "navigation_bar.preferences": "Asetukset",
"navigation_bar.public_timeline": "Yleinen aikajana", "navigation_bar.public_timeline": "Yleinen aikajana",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} hevur júst postað", "notification.status": "{name} hevur júst postað",
"notification.update": "{name} rættaði ein post", "notification.update": "{name} rættaði ein post",
"notification_requests.accept": "Góðtak", "notification_requests.accept": "Góðtak",
"notification_requests.accept_multiple": "{count, plural, one {Góðtak # umbøn…} other {Góðtak # umbønir…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Góðtak umbøn} other {Góðtak umbønir}}",
"notification_requests.confirm_accept_multiple.message": "Tú er í ferð við at góðtaka {count, plural, one {eina fráboðanarumbøn} other {# fráboðanarumbønir}}. Er tú vís/ur í, at tú vil halda fram?",
"notification_requests.confirm_accept_multiple.title": "Góðtak fráboðanarumbønir?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Avvís umbøn} other {Avvís umbønir}}",
"notification_requests.confirm_dismiss_multiple.message": "Tú er í ferð við at avvísa {count, plural, one {eina fráboðanarumbøn} other {# fráboðanarumbønir}}. Tað verður ikki lætt hjá tær at fáa fatur á {count, plural, one {henni} other {teimum}} aftur. Er tú vís/ur í at tú vil halda fram?",
"notification_requests.confirm_dismiss_multiple.title": "Avvís fráboðanarumbønir?",
"notification_requests.dismiss": "Avvís", "notification_requests.dismiss": "Avvís",
"notification_requests.dismiss_multiple": "{count, plural, one {Avvís # umbøn…} other {Avvís # umbønir…}}",
"notification_requests.edit_selection": "Rætta",
"notification_requests.exit_selection": "Liðugt",
"notification_requests.explainer_for_limited_account": "Fráboðanir frá hesi kontuni eru filtreraðar burtur, tí kontan er avmarkað av einum umsjónarfólki.", "notification_requests.explainer_for_limited_account": "Fráboðanir frá hesi kontuni eru filtreraðar burtur, tí kontan er avmarkað av einum umsjónarfólki.",
"notification_requests.explainer_for_limited_remote_account": "Fráboðanir frá hesi kontuni eru filtreraðar burtur, tí kontan ella ambætarin hjá kontuni eru avmarkaði av einum umsjónarfólki.", "notification_requests.explainer_for_limited_remote_account": "Fráboðanir frá hesi kontuni eru filtreraðar burtur, tí kontan ella ambætarin hjá kontuni eru avmarkaði av einum umsjónarfólki.",
"notification_requests.maximize": "Mesta", "notification_requests.maximize": "Mesta",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} publicou", "notification.status": "{name} publicou",
"notification.update": "{name} editou unha publicación", "notification.update": "{name} editou unha publicación",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
"notification_requests.accept_multiple": "{count, plural, one {Aceptar # solicitude…} other {Aceptar # solicitudes…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar solicitude} other {Aceptar solicitudes}}",
"notification_requests.confirm_accept_multiple.message": "Vas aceptar {count, plural, one {unha solicitude de notificación} other {# solicitudes de notificación}}. Tes certeza de querer aceptar?",
"notification_requests.confirm_accept_multiple.title": "Aceptar solicitudes de notificación?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Rexeitar solicitude} other {Rexeitar solicitudes}}",
"notification_requests.confirm_dismiss_multiple.message": "Vas rexeitar {count, plural, one {unha solicitude de notificación} other {# solicitudes de notificación}}. Non poderás volver acceder fácilmente a {count, plural, one {ela} other {elas}}. Tes certeza de querer rexeitar?",
"notification_requests.confirm_dismiss_multiple.title": "Rexeitar solicitudes de notificación?",
"notification_requests.dismiss": "Desbotar", "notification_requests.dismiss": "Desbotar",
"notification_requests.dismiss_multiple": "{count, plural, one {Rexeitar # solicitude…} other {Rexeitar # solicitudes…}}",
"notification_requests.edit_selection": "Editar",
"notification_requests.exit_selection": "Feito",
"notification_requests.explainer_for_limited_account": "Filtráronse as notificacións desta conta porque a conta ten limitacións impostas pola moderación.", "notification_requests.explainer_for_limited_account": "Filtráronse as notificacións desta conta porque a conta ten limitacións impostas pola moderación.",
"notification_requests.explainer_for_limited_remote_account": "Filtráronse as notificacións desta conta porque a conta ou o seu servidor teñen limitacións impostas pola moderación.", "notification_requests.explainer_for_limited_remote_account": "Filtráronse as notificacións desta conta porque a conta ou o seu servidor teñen limitacións impostas pola moderación.",
"notification_requests.maximize": "Maximizar", "notification_requests.maximize": "Maximizar",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} bejegyzést tett közzé", "notification.status": "{name} bejegyzést tett közzé",
"notification.update": "{name} szerkesztett egy bejegyzést", "notification.update": "{name} szerkesztett egy bejegyzést",
"notification_requests.accept": "Elfogadás", "notification_requests.accept": "Elfogadás",
"notification_requests.accept_multiple": "{count, plural, one {# kérés elfogadása…} other {# kérés elfogadása…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Kérés elfogadása} other {Kérések elfogadása}}",
"notification_requests.confirm_accept_multiple.message": "Elfogadni készülsz {count, plural, one {egy értesítési kérést} other {# értesítési kérést}}. Biztosan folytatod?",
"notification_requests.confirm_accept_multiple.title": "Értesítési kérések elfogadása?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Kérés elvetése} other {Kérések elvetése}}",
"notification_requests.confirm_dismiss_multiple.message": "{count, plural, one {Egy értesítési kérés} other {# értesítési kérés}} elvetésére készülsz. Többé nem fogsz {count, plural, one {hozzáférni} other {hozzájuk férni}}. Biztosan folytatod?",
"notification_requests.confirm_dismiss_multiple.title": "Értesítési kérések elvetése?",
"notification_requests.dismiss": "Elvetés", "notification_requests.dismiss": "Elvetés",
"notification_requests.dismiss_multiple": "{count, plural, one {# kérés elvetése…} other {# kérés elvetése…}}",
"notification_requests.edit_selection": "Szerkesztés",
"notification_requests.exit_selection": "Kész",
"notification_requests.explainer_for_limited_account": "Az ettől a fióktól származó értesítéseket kiszűrték, mert a fiókot egy moderátor korlátozta.", "notification_requests.explainer_for_limited_account": "Az ettől a fióktól származó értesítéseket kiszűrték, mert a fiókot egy moderátor korlátozta.",
"notification_requests.explainer_for_limited_remote_account": "Az ettől a fióktól származó értesítéseket kiszűrték, mert a fiókot vagy annak kiszolgálóját egy moderátor korlátozta.", "notification_requests.explainer_for_limited_remote_account": "Az ettől a fióktól származó értesítéseket kiszűrték, mert a fiókot vagy annak kiszolgálóját egy moderátor korlátozta.",
"notification_requests.maximize": "Maximalizálás", "notification_requests.maximize": "Maximalizálás",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} sendi inn rétt í þessu", "notification.status": "{name} sendi inn rétt í þessu",
"notification.update": "{name} breytti færslu", "notification.update": "{name} breytti færslu",
"notification_requests.accept": "Samþykkja", "notification_requests.accept": "Samþykkja",
"notification_requests.accept_multiple": "{count, plural, one {Samþykkja # beiðni…} other {Samþykkja # beiðnir…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Samþykkja beiðni} other {Samþykkja beiðnir}}",
"notification_requests.confirm_accept_multiple.message": "Þú ert að fara að samþykkja {count, plural, one {eina beiðni um tilkynningar} other {# beiðnir um tilkynningar}}. Ertu viss um að þú viljir halda áfram?",
"notification_requests.confirm_accept_multiple.title": "Samþykkja beiðnir um tilkynningar?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Afgreiða beiðni} other {Afgreiða beiðnir}}",
"notification_requests.confirm_dismiss_multiple.message": "Þú ert að fara að hunsa {count, plural, one {eina beiðni um tilkynningar} other {# beiðnir um tilkynningar}}. Þú munt ekki eiga auðvelt með að skoða {count, plural, one {hana} other {þær}} aftur síðar. Ertu viss um að þú viljir halda áfram?",
"notification_requests.confirm_dismiss_multiple.title": "Hunsa beiðnir um tilkynningar?",
"notification_requests.dismiss": "Afgreiða", "notification_requests.dismiss": "Afgreiða",
"notification_requests.dismiss_multiple": "{count, plural, one {Afgreiða # beiðni…} other {Afgreiða # beiðnir…}}",
"notification_requests.edit_selection": "Breyta",
"notification_requests.exit_selection": "Lokið",
"notification_requests.explainer_for_limited_account": "Tilkynningar frá þessum notanda hafa verið síaðar þar sem aðgangur hans hefur verið takmarkaður af umsjónarmanni.", "notification_requests.explainer_for_limited_account": "Tilkynningar frá þessum notanda hafa verið síaðar þar sem aðgangur hans hefur verið takmarkaður af umsjónarmanni.",
"notification_requests.explainer_for_limited_remote_account": "Tilkynningar frá þessum notanda hafa verið síaðar þar sem aðgangurinn eða netþjónn hans hefur verið takmarkaður af umsjónarmanni.", "notification_requests.explainer_for_limited_remote_account": "Tilkynningar frá þessum notanda hafa verið síaðar þar sem aðgangurinn eða netþjónn hans hefur verið takmarkaður af umsjónarmanni.",
"notification_requests.maximize": "Hámarka", "notification_requests.maximize": "Hámarka",

View File

@ -11,6 +11,7 @@
"about.not_available": "この情報はこのサーバーでは利用できません。", "about.not_available": "この情報はこのサーバーでは利用できません。",
"about.powered_by": "{mastodon}による分散型ソーシャルメディア", "about.powered_by": "{mastodon}による分散型ソーシャルメディア",
"about.rules": "サーバーのルール", "about.rules": "サーバーのルール",
"account.account_note_header": "自分用メモ",
"account.add_or_remove_from_list": "リストから追加または外す", "account.add_or_remove_from_list": "リストから追加または外す",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Group", "account.badges.group": "Group",
@ -533,14 +534,23 @@
"notifications.permission_denied": "ブラウザの通知が拒否されているためデスクトップ通知は利用できません", "notifications.permission_denied": "ブラウザの通知が拒否されているためデスクトップ通知は利用できません",
"notifications.permission_denied_alert": "ブラウザの通知が拒否されているためデスクトップ通知を有効にできません", "notifications.permission_denied_alert": "ブラウザの通知が拒否されているためデスクトップ通知を有効にできません",
"notifications.permission_required": "必要な権限が付与されていないため、デスクトップ通知は利用できません。", "notifications.permission_required": "必要な権限が付与されていないため、デスクトップ通知は利用できません。",
"notifications.policy.filter_new_accounts.hint": "作成から{days, plural, other {#日}}以内のアカウントからの通知がブロックされます", "notifications.policy.accept": "受入れ",
"notifications.policy.filter_new_accounts_title": "新しいアカウントからの通知をブロックする", "notifications.policy.accept_hint": "通知を表示します",
"notifications.policy.filter_not_followers_hint": "フォローされていても、フォローから{days, plural, other {#日}}経っていない場合はブロックされます", "notifications.policy.drop": "無視",
"notifications.policy.filter_not_followers_title": "フォローされていないアカウントからの通知をブロックする", "notifications.policy.drop_hint": "通知を破棄します。再表示はできません。",
"notifications.policy.filter_not_following_hint": "手動で通知を受け入れたアカウントはブロックされません", "notifications.policy.filter": "保留",
"notifications.policy.filter_not_following_title": "フォローしていないアカウントからの通知をブロックする", "notifications.policy.filter_hint": "「保留中の通知」に止め置きます",
"notifications.policy.filter_private_mentions_hint": "あなたがメンションした相手からの返信、およびフォローしているアカウントからの返信以外がブロックされます", "notifications.policy.filter_limited_accounts_hint": "モデレーターにより制限されたアカウントから送られる通知が対象です",
"notifications.policy.filter_private_mentions_title": "外部からの非公開の返信をブロックする", "notifications.policy.filter_limited_accounts_title": "モデレーションされたアカウントからの通知",
"notifications.policy.filter_new_accounts.hint": "作成から{days, plural, other {#日}}以内のアカウントが対象です",
"notifications.policy.filter_new_accounts_title": "新しいアカウントからの通知",
"notifications.policy.filter_not_followers_hint": "フォローされていても、フォローから{days, plural, other {#日}}経っていない場合は対象になります",
"notifications.policy.filter_not_followers_title": "フォローされていないアカウントからの通知",
"notifications.policy.filter_not_following_hint": "手動で通知を受け入れたアカウントは対象外です",
"notifications.policy.filter_not_following_title": "フォローしていないアカウントからの通知",
"notifications.policy.filter_private_mentions_hint": "あなたがメンションした相手からの返信、およびフォローしているアカウントからの返信は対象外です",
"notifications.policy.filter_private_mentions_title": "外部からの非公開の返信",
"notifications.policy.title": "通知のフィルタリング",
"notifications_permission_banner.enable": "デスクトップ通知を有効にする", "notifications_permission_banner.enable": "デスクトップ通知を有効にする",
"notifications_permission_banner.how_to_control": "Mastodonを閉じている間でも通知を受信するにはデスクトップ通知を有効にしてください。有効にすると上の {icon} ボタンから通知の内容を細かくカスタマイズできます。", "notifications_permission_banner.how_to_control": "Mastodonを閉じている間でも通知を受信するにはデスクトップ通知を有効にしてください。有効にすると上の {icon} ボタンから通知の内容を細かくカスタマイズできます。",
"notifications_permission_banner.title": "お見逃しなく", "notifications_permission_banner.title": "お見逃しなく",
@ -781,6 +791,7 @@
"timeline_hint.remote_resource_not_displayed": "他のサーバーの{resource}は表示されません。", "timeline_hint.remote_resource_not_displayed": "他のサーバーの{resource}は表示されません。",
"timeline_hint.resources.followers": "フォロワー", "timeline_hint.resources.followers": "フォロワー",
"timeline_hint.resources.follows": "フォロー", "timeline_hint.resources.follows": "フォロー",
"timeline_hint.resources.replies": "返信の一部",
"timeline_hint.resources.statuses": "以前の投稿", "timeline_hint.resources.statuses": "以前の投稿",
"trends.counter_by_accounts": "過去{days, plural, one {{days}日} other {{days}日}}に{count, plural, one {{counter}人} other {{counter} 人}}", "trends.counter_by_accounts": "過去{days, plural, one {{days}日} other {{days}日}}に{count, plural, one {{counter}人} other {{counter} 人}}",
"trends.trending_now": "トレンドタグ", "trends.trending_now": "トレンドタグ",

View File

@ -348,7 +348,7 @@
"hashtag.counter_by_uses_today": "오늘 {count, plural, other {{counter} 개의 게시물}}", "hashtag.counter_by_uses_today": "오늘 {count, plural, other {{counter} 개의 게시물}}",
"hashtag.follow": "팔로우", "hashtag.follow": "팔로우",
"hashtag.unfollow": "팔로우 해제", "hashtag.unfollow": "팔로우 해제",
"hashtags.and_other": "…그리고 {count, plural,other {#개 더}}", "hashtags.and_other": "…그리고 {count, plural,other {# 개 더}}",
"home.column_settings.show_reblogs": "부스트 표시", "home.column_settings.show_reblogs": "부스트 표시",
"home.column_settings.show_replies": "답글 표시", "home.column_settings.show_replies": "답글 표시",
"home.hide_announcements": "공지사항 숨기기", "home.hide_announcements": "공지사항 숨기기",
@ -359,6 +359,8 @@
"ignore_notifications_modal.disclaimer": "마스토돈은 당신이 그들의 알림을 무시했다는 걸 알려줄 수 없습니다. 알림을 무시한다고 해서 메시지가 오지 않는 것은 아닙니다.", "ignore_notifications_modal.disclaimer": "마스토돈은 당신이 그들의 알림을 무시했다는 걸 알려줄 수 없습니다. 알림을 무시한다고 해서 메시지가 오지 않는 것은 아닙니다.",
"ignore_notifications_modal.filter_instead": "대신 필터로 거르기", "ignore_notifications_modal.filter_instead": "대신 필터로 거르기",
"ignore_notifications_modal.filter_to_act_users": "여전히 사용자를 수락, 거절, 신고할 수 있습니다", "ignore_notifications_modal.filter_to_act_users": "여전히 사용자를 수락, 거절, 신고할 수 있습니다",
"ignore_notifications_modal.filter_to_avoid_confusion": "필터링은 혼란을 예방하는데 도움이 될 수 있습니다",
"ignore_notifications_modal.filter_to_review_separately": "걸러진 알림들을 개별적으로 검토할 수 있습니다",
"ignore_notifications_modal.ignore": "알림 무시", "ignore_notifications_modal.ignore": "알림 무시",
"ignore_notifications_modal.limited_accounts_title": "중재된 계정의 알림을 무시할까요?", "ignore_notifications_modal.limited_accounts_title": "중재된 계정의 알림을 무시할까요?",
"ignore_notifications_modal.new_accounts_title": "새 계정의 알림을 무시할까요?", "ignore_notifications_modal.new_accounts_title": "새 계정의 알림을 무시할까요?",
@ -441,7 +443,7 @@
"lists.replies_policy.title": "답글 표시:", "lists.replies_policy.title": "답글 표시:",
"lists.search": "팔로우 중인 사람들 중에서 찾기", "lists.search": "팔로우 중인 사람들 중에서 찾기",
"lists.subheading": "리스트", "lists.subheading": "리스트",
"load_pending": "{count}개의 새 항목", "load_pending": "{count, plural, other {#}} 개의 새 항목",
"loading_indicator.label": "불러오는 중...", "loading_indicator.label": "불러오는 중...",
"media_gallery.toggle_visible": "이미지 숨기기", "media_gallery.toggle_visible": "이미지 숨기기",
"moved_to_account_banner.text": "당신의 계정 {disabledAccount}는 {movedToAccount}로 이동하였기 때문에 현재 비활성화 상태입니다.", "moved_to_account_banner.text": "당신의 계정 {disabledAccount}는 {movedToAccount}로 이동하였기 때문에 현재 비활성화 상태입니다.",
@ -516,7 +518,17 @@
"notification.status": "{name} 님이 방금 게시물을 올렸습니다", "notification.status": "{name} 님이 방금 게시물을 올렸습니다",
"notification.update": "{name} 님이 게시물을 수정했습니다", "notification.update": "{name} 님이 게시물을 수정했습니다",
"notification_requests.accept": "수락", "notification_requests.accept": "수락",
"notification_requests.accept_multiple": "{count, plural, other {#}} 개의 요청 수락하기",
"notification_requests.confirm_accept_multiple.button": "{count, plural, other {}}요청 수락하기",
"notification_requests.confirm_accept_multiple.message": "{count, plural, other {#}} 개의 요청을 수락하려고 합니다. 계속 진행할까요?",
"notification_requests.confirm_accept_multiple.title": "알림 요청을 수락할까요?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, other {요청 지우기}}",
"notification_requests.confirm_dismiss_multiple.message": "{count, plural, other {# 개의 요청}}을 지우려고 합니다. {count, plural, other {}}다시 접근하기 어렵습니다. 계속할까요?",
"notification_requests.confirm_dismiss_multiple.title": "알림 요청을 지울까요?",
"notification_requests.dismiss": "지우기", "notification_requests.dismiss": "지우기",
"notification_requests.dismiss_multiple": "{count, plural, other {# 개의 요청 지우기}}",
"notification_requests.edit_selection": "편집",
"notification_requests.exit_selection": "완료",
"notification_requests.explainer_for_limited_account": "이 계정은 중재자에 의해 제한되었기 때문에 이 계정의 알림은 걸러졌습니다.", "notification_requests.explainer_for_limited_account": "이 계정은 중재자에 의해 제한되었기 때문에 이 계정의 알림은 걸러졌습니다.",
"notification_requests.explainer_for_limited_remote_account": "이 계정 혹은 그가 속한 서버는 중재자에 의해 제한되었기 때문에 이 계정의 알림은 걸러졌습니다.", "notification_requests.explainer_for_limited_remote_account": "이 계정 혹은 그가 속한 서버는 중재자에 의해 제한되었기 때문에 이 계정의 알림은 걸러졌습니다.",
"notification_requests.maximize": "최대화", "notification_requests.maximize": "최대화",
@ -624,8 +636,8 @@
"poll.closed": "마감", "poll.closed": "마감",
"poll.refresh": "새로고침", "poll.refresh": "새로고침",
"poll.reveal": "결과 보기", "poll.reveal": "결과 보기",
"poll.total_people": "{count}명", "poll.total_people": "{count, plural, other {#}} 명",
"poll.total_votes": "{count} 표", "poll.total_votes": "{count, plural, other {#}} 표",
"poll.vote": "투표", "poll.vote": "투표",
"poll.voted": "이 답변에 투표함", "poll.voted": "이 답변에 투표함",
"poll.votes": "{votes} 표", "poll.votes": "{votes} 표",
@ -658,7 +670,7 @@
"relative_time.minutes": "{number}분 전", "relative_time.minutes": "{number}분 전",
"relative_time.seconds": "{number}초 전", "relative_time.seconds": "{number}초 전",
"relative_time.today": "오늘", "relative_time.today": "오늘",
"reply_indicator.attachments": "{count, plural, one {#} other {#}}개의 첨부파일", "reply_indicator.attachments": "{count, plural, other {#}} 개의 첨부파일",
"reply_indicator.cancel": "취소", "reply_indicator.cancel": "취소",
"reply_indicator.poll": "투표", "reply_indicator.poll": "투표",
"report.block": "차단", "report.block": "차단",
@ -758,7 +770,7 @@
"status.direct_indicator": "개인적인 멘션", "status.direct_indicator": "개인적인 멘션",
"status.edit": "수정", "status.edit": "수정",
"status.edited": "{date}에 마지막으로 편집됨", "status.edited": "{date}에 마지막으로 편집됨",
"status.edited_x_times": "{count}번 수정됨", "status.edited_x_times": "{count, plural, other {{count}}} 번 수정됨",
"status.embed": "임베드", "status.embed": "임베드",
"status.favourite": "좋아요", "status.favourite": "좋아요",
"status.favourites": "{count, plural, other {좋아요}}", "status.favourites": "{count, plural, other {좋아요}}",

View File

@ -516,7 +516,17 @@
"notification.status": "{name} ką tik paskelbė", "notification.status": "{name} ką tik paskelbė",
"notification.update": "{name} redagavo įrašą", "notification.update": "{name} redagavo įrašą",
"notification_requests.accept": "Priimti", "notification_requests.accept": "Priimti",
"notification_requests.accept_multiple": "{count, plural, one {Priimti # prašymą…} few {Priimti # prašymus…} many {Priimti # prašymo…} other {Priimti # prašymų…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Priimti prašymą} few {Priimti prašymus} many {Priimti prašymo} other {Priimti prašymų}}",
"notification_requests.confirm_accept_multiple.message": "Ketini priimti {count, plural, one {# pranešimo prašymą} few {# pranešimų prašymus} many {# pranešimo prašymo} other {# pranešimų prašymų}}. Ar tikrai nori tęsti?",
"notification_requests.confirm_accept_multiple.title": "Priimti pranešimų prašymus?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Atmesti prašymą} few {Atmesti prašymus} many {Atmesti prašymo} other {Atmesti prašymų}}",
"notification_requests.confirm_dismiss_multiple.message": "Ketini atmesti {count, plural, one {# pranešimo prašymą} few {# pranešimų prašymus} many {# pranešimo prašymo} other {# pranešimų prašymų}}. Daugiau negalėsi lengvai pasiekti {count, plural, one {jo} few {jų} many {juos} other {jų}}. Ar tikrai nori tęsti?",
"notification_requests.confirm_dismiss_multiple.title": "Atmesti pranešimų prašymus?",
"notification_requests.dismiss": "Atmesti", "notification_requests.dismiss": "Atmesti",
"notification_requests.dismiss_multiple": "{count, plural, one {Atmesti prašymą…} few {Atmesti prašymus…} many {Atmesti prašymo…} other {Atmesti prašymų…}}",
"notification_requests.edit_selection": "Redaguoti",
"notification_requests.exit_selection": "Atlikta",
"notification_requests.explainer_for_limited_account": "Pranešimai iš šios paskyros buvo filtruojami, nes prižiūrėtojas (-a) apribojo paskyrą.", "notification_requests.explainer_for_limited_account": "Pranešimai iš šios paskyros buvo filtruojami, nes prižiūrėtojas (-a) apribojo paskyrą.",
"notification_requests.explainer_for_limited_remote_account": "Pranešimai iš šios paskyros buvo filtruojami, nes prižiūrėtojas (-a) apribojo paskyrą arba serverį.", "notification_requests.explainer_for_limited_remote_account": "Pranešimai iš šios paskyros buvo filtruojami, nes prižiūrėtojas (-a) apribojo paskyrą arba serverį.",
"notification_requests.maximize": "Padidinti", "notification_requests.maximize": "Padidinti",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} heeft zojuist een bericht geplaatst", "notification.status": "{name} heeft zojuist een bericht geplaatst",
"notification.update": "{name} heeft een bericht bewerkt", "notification.update": "{name} heeft een bericht bewerkt",
"notification_requests.accept": "Accepteren", "notification_requests.accept": "Accepteren",
"notification_requests.accept_multiple": "{count, plural, one {# verzoek accepteren…} other {# verzoeken accepteren…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Verzoek accepteren} other {Verzoeken accepteren}}",
"notification_requests.confirm_accept_multiple.message": "Je staat op het punt om {count, plural, one {een meldingsverzoek} other {# meldingsverzoeken}} te accepteren. Weet je zeker dat je door wilt gaan?",
"notification_requests.confirm_accept_multiple.title": "Meldingsverzoeken accepteren?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Verzoek afwijzen} other {Verzoeken afwijzen}}",
"notification_requests.confirm_dismiss_multiple.message": "Je staat op het punt om {count, plural, one {een meldingsverzoek} other {# meldingsverzoeken}} af te wijzen. Je zult niet in staat zijn om {count, plural, one {hier} other {hier}} weer gemakkelijk toegang toe te krijgen. Wil je doorgaan?",
"notification_requests.confirm_dismiss_multiple.title": "Meldingsverzoeken afwijzen?",
"notification_requests.dismiss": "Afwijzen", "notification_requests.dismiss": "Afwijzen",
"notification_requests.dismiss_multiple": "{count, plural, one {# verzoek afwijzen…} other {# verzoeken afwijzen…}}",
"notification_requests.edit_selection": "Bewerken",
"notification_requests.exit_selection": "Klaar",
"notification_requests.explainer_for_limited_account": "Meldingen van dit account zijn gefilterd omdat dit account door een moderator is beperkt.", "notification_requests.explainer_for_limited_account": "Meldingen van dit account zijn gefilterd omdat dit account door een moderator is beperkt.",
"notification_requests.explainer_for_limited_remote_account": "Meldingen van dit account zijn gefilterd omdat dit account of diens server door een moderator is beperkt.", "notification_requests.explainer_for_limited_remote_account": "Meldingen van dit account zijn gefilterd omdat dit account of diens server door een moderator is beperkt.",
"notification_requests.maximize": "Maximaliseren", "notification_requests.maximize": "Maximaliseren",

View File

@ -189,6 +189,7 @@
"confirmations.reply.title": "Перепишем пост?", "confirmations.reply.title": "Перепишем пост?",
"confirmations.unfollow.confirm": "Отписаться", "confirmations.unfollow.confirm": "Отписаться",
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?", "confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
"confirmations.unfollow.title": "Отписаться?",
"conversation.delete": "Удалить беседу", "conversation.delete": "Удалить беседу",
"conversation.mark_as_read": "Отметить как прочитанное", "conversation.mark_as_read": "Отметить как прочитанное",
"conversation.open": "Просмотр беседы", "conversation.open": "Просмотр беседы",
@ -351,6 +352,7 @@
"home.pending_critical_update.link": "Посмотреть обновления", "home.pending_critical_update.link": "Посмотреть обновления",
"home.pending_critical_update.title": "Доступно критическое обновление безопасности!", "home.pending_critical_update.title": "Доступно критическое обновление безопасности!",
"home.show_announcements": "Показать объявления", "home.show_announcements": "Показать объявления",
"ignore_notifications_modal.filter_to_act_users": "Вы и далее сможете принять, отвергнуть и жаловаться на пользователей",
"interaction_modal.description.favourite": "С учётной записью Mastodon, вы можете добавить этот пост в избранное, чтобы сохранить его на будущее и дать автору знать, что пост вам понравился.", "interaction_modal.description.favourite": "С учётной записью Mastodon, вы можете добавить этот пост в избранное, чтобы сохранить его на будущее и дать автору знать, что пост вам понравился.",
"interaction_modal.description.follow": "С учётной записью Mastodon вы можете подписаться на {name}, чтобы получать их посты в своей домашней ленте.", "interaction_modal.description.follow": "С учётной записью Mastodon вы можете подписаться на {name}, чтобы получать их посты в своей домашней ленте.",
"interaction_modal.description.reblog": "С учётной записью Mastodon, вы можете продвинуть этот пост, чтобы поделиться им со своими подписчиками.", "interaction_modal.description.reblog": "С учётной записью Mastodon, вы можете продвинуть этот пост, чтобы поделиться им со своими подписчиками.",

View File

@ -487,6 +487,7 @@
"notification.label.private_mention": "Zasebna omemba", "notification.label.private_mention": "Zasebna omemba",
"notification.label.private_reply": "Zasebni odgovor", "notification.label.private_reply": "Zasebni odgovor",
"notification.label.reply": "Odgovori", "notification.label.reply": "Odgovori",
"notification.mention": "Omemba",
"notification.moderation-warning.learn_more": "Več o tem", "notification.moderation-warning.learn_more": "Več o tem",
"notification.moderation_warning": "Prejeli ste opozorilo moderatorjev", "notification.moderation_warning": "Prejeli ste opozorilo moderatorjev",
"notification.moderation_warning.action_delete_statuses": "Nekatere vaše objave so odstranjene.", "notification.moderation_warning.action_delete_statuses": "Nekatere vaše objave so odstranjene.",
@ -508,6 +509,8 @@
"notification.update": "{name} je uredil(a) objavo", "notification.update": "{name} je uredil(a) objavo",
"notification_requests.accept": "Sprejmi", "notification_requests.accept": "Sprejmi",
"notification_requests.dismiss": "Zavrni", "notification_requests.dismiss": "Zavrni",
"notification_requests.edit_selection": "Uredi",
"notification_requests.exit_selection": "Opravljeno",
"notification_requests.maximize": "Maksimiraj", "notification_requests.maximize": "Maksimiraj",
"notification_requests.notifications_from": "Obvestila od {name}", "notification_requests.notifications_from": "Obvestila od {name}",
"notification_requests.title": "Filtrirana obvestila", "notification_requests.title": "Filtrirana obvestila",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} sapo postoi", "notification.status": "{name} sapo postoi",
"notification.update": "{name} përpunoi një postim", "notification.update": "{name} përpunoi një postim",
"notification_requests.accept": "Pranoje", "notification_requests.accept": "Pranoje",
"notification_requests.accept_multiple": "{count, plural, one {Pranoni # kërkesë…} other {Pranoni # kërkesa…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Pranojeni kërkesën} other {Pranoje kërkesën}}",
"notification_requests.confirm_accept_multiple.message": "Ju ndan një hap nga pranimi i {count, plural, one {një kërkese njoftimi} other {# kërkesash njoftimi}}. Jeni i sigurt se doni të vazhdohet?",
"notification_requests.confirm_accept_multiple.title": "Të pranohen kërkesa njoftimesh?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Hidheni tej kërkesën} other {Hidhini tej kërkesat}}",
"notification_requests.confirm_dismiss_multiple.message": "Ju ndan një hap nga hedhja tej e {count, plural, one {një kërkese njoftimesh} other {# kërkesash njoftimesh}}. Sdo të jeni në gjendje të shihni sërish {count, plural, one {atë} other {ato}}. Jeni i sigurt se doni të bëhet kjo?",
"notification_requests.confirm_dismiss_multiple.title": "Të hidhen tej kërkesa njoftimesh?",
"notification_requests.dismiss": "Hidhe tej", "notification_requests.dismiss": "Hidhe tej",
"notification_requests.dismiss_multiple": "{count, plural, one {Hidhni tej # kërkesë…} other {Hidhni tej # kërkesa…}}",
"notification_requests.edit_selection": "Përpunoni",
"notification_requests.exit_selection": "U bë",
"notification_requests.explainer_for_limited_account": "Njoftimet prej kësaj llogarie janë filtruar, ngaqë llogaria është kufizuar nga një moderator.", "notification_requests.explainer_for_limited_account": "Njoftimet prej kësaj llogarie janë filtruar, ngaqë llogaria është kufizuar nga një moderator.",
"notification_requests.explainer_for_limited_remote_account": "Njoftimet prej kësaj llogarie janë filtruar, ngaqë llogaria, ose shërbyesi është kufizuar nga një moderator.", "notification_requests.explainer_for_limited_remote_account": "Njoftimet prej kësaj llogarie janë filtruar, ngaqë llogaria, ose shërbyesi është kufizuar nga një moderator.",
"notification_requests.maximize": "Maksimizoje", "notification_requests.maximize": "Maksimizoje",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} az önce gönderdi", "notification.status": "{name} az önce gönderdi",
"notification.update": "{name} bir gönderiyi düzenledi", "notification.update": "{name} bir gönderiyi düzenledi",
"notification_requests.accept": "Onayla", "notification_requests.accept": "Onayla",
"notification_requests.accept_multiple": "{count, plural, one {# isteği kabul et…} other {# isteği kabul et…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {İsteği kabul et} other {İstekleri kabul et}}",
"notification_requests.confirm_accept_multiple.message": "{count, plural, one {Bir bildirim isteğini} other {# bildirim isteğini}} kabul etmek üzeresiniz. Devam etmek istediğinizden emin misiniz?",
"notification_requests.confirm_accept_multiple.title": "Bildirim isteklerini kabul et?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {İsteği reddet} other {İstekleri reddet}}",
"notification_requests.confirm_dismiss_multiple.message": "{count, plural, one {Bir bildirim isteğini} other {# bildirim isteğini}} reddetmek üzeresiniz. {count, plural, one {Ona} other {Onlara}} tekrar kolayca ulaşamayacaksınz. Devam etmek istediğinizden emin misiniz?",
"notification_requests.confirm_dismiss_multiple.title": "Bildirim isteklerini reddet?",
"notification_requests.dismiss": "Yoksay", "notification_requests.dismiss": "Yoksay",
"notification_requests.dismiss_multiple": "{count, plural, one {# isteği reddet…} other {# isteği reddet…}}",
"notification_requests.edit_selection": "Düzenle",
"notification_requests.exit_selection": "Tamamlandı",
"notification_requests.explainer_for_limited_account": "Hesap bir moderatör tarafından sınırlandığı için, bu hesaptan gönderilen bildirimler filtrelendi.", "notification_requests.explainer_for_limited_account": "Hesap bir moderatör tarafından sınırlandığı için, bu hesaptan gönderilen bildirimler filtrelendi.",
"notification_requests.explainer_for_limited_remote_account": "Hesap veya sunucusu bir moderatör tarafından sınırlandığı için, bu hesaptan gönderilen bildirimler filtrelendi.", "notification_requests.explainer_for_limited_remote_account": "Hesap veya sunucusu bir moderatör tarafından sınırlandığı için, bu hesaptan gönderilen bildirimler filtrelendi.",
"notification_requests.maximize": "Büyüt", "notification_requests.maximize": "Büyüt",

View File

@ -518,7 +518,17 @@
"notification.status": "{name} щойно дописує", "notification.status": "{name} щойно дописує",
"notification.update": "{name} змінює допис", "notification.update": "{name} змінює допис",
"notification_requests.accept": "Прийняти", "notification_requests.accept": "Прийняти",
"notification_requests.accept_multiple": "{count, plural, one {Прийняти # запит…} few {Прийняти # запити…} many {Прийняти # запитів…} other {Прийняти # запит…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Прийняти запит} other {Прийняти запити}}",
"notification_requests.confirm_accept_multiple.message": "Ви збираєтеся прийняти {count, plural, one {запит на сповіщення} few {# запити на сповіщення} many {# запитів на сповіщення} other {# запит на сповіщення}}. Ви впевнені, що хочете продовжити?",
"notification_requests.confirm_accept_multiple.title": "Прийняти запит на сповіщення?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Відхилити запит} other {Відхилити запити}}",
"notification_requests.confirm_dismiss_multiple.message": "Ви збираєтеся відхилити {count, plural, one {один запит на сповіщення} few {# запити на сповіщення} many {# запитів на сповіщення} other {# запит на сповіщення}}. Ви не зможете легко отримати доступ до {count, plural, one {нього} other {них}} пізніше. Ви впевнені, що хочете продовжити?",
"notification_requests.confirm_dismiss_multiple.title": "Відхилити запити на сповіщення?",
"notification_requests.dismiss": "Відхилити", "notification_requests.dismiss": "Відхилити",
"notification_requests.dismiss_multiple": "{count, plural, one {Відхилити # запит…} few {Відхилити # запити…} many {Відхилити # запитів…} other {Відхилити # запит…}}",
"notification_requests.edit_selection": "Змінити",
"notification_requests.exit_selection": "Готово",
"notification_requests.explainer_for_limited_account": "Сповіщення від цього облікового запису фільтровані, оскільки обліковий запис обмежений модератором.", "notification_requests.explainer_for_limited_account": "Сповіщення від цього облікового запису фільтровані, оскільки обліковий запис обмежений модератором.",
"notification_requests.explainer_for_limited_remote_account": "Сповіщення від цього облікового запису фільтровані, оскільки обліковий запис або його сервер обмежений модератором.", "notification_requests.explainer_for_limited_remote_account": "Сповіщення від цього облікового запису фільтровані, оскільки обліковий запис або його сервер обмежений модератором.",
"notification_requests.maximize": "Розгорнути", "notification_requests.maximize": "Розгорнути",

View File

@ -20,12 +20,16 @@ import {
mountNotifications, mountNotifications,
unmountNotifications, unmountNotifications,
refreshStaleNotificationGroups, refreshStaleNotificationGroups,
pollRecentNotifications,
} from 'mastodon/actions/notification_groups'; } from 'mastodon/actions/notification_groups';
import { import {
disconnectTimeline, disconnectTimeline,
timelineDelete, timelineDelete,
} from 'mastodon/actions/timelines_typed'; } from 'mastodon/actions/timelines_typed';
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications'; import type {
ApiNotificationJSON,
ApiNotificationGroupJSON,
} from 'mastodon/api_types/notifications';
import { compareId } from 'mastodon/compare_id'; import { compareId } from 'mastodon/compare_id';
import { usePendingItems } from 'mastodon/initial_state'; import { usePendingItems } from 'mastodon/initial_state';
import { import {
@ -296,6 +300,106 @@ function commitLastReadId(state: NotificationGroupsState) {
} }
} }
function fillNotificationsGap(
groups: NotificationGroupsState['groups'],
gap: NotificationGap,
notifications: ApiNotificationGroupJSON[],
): NotificationGroupsState['groups'] {
// find the gap in the existing notifications
const gapIndex = groups.findIndex(
(groupOrGap) =>
groupOrGap.type === 'gap' &&
groupOrGap.sinceId === gap.sinceId &&
groupOrGap.maxId === gap.maxId,
);
if (gapIndex < 0)
// We do not know where to insert, let's return
return groups;
// Filling a disconnection gap means we're getting historical data
// about groups we may know or may not know about.
// The notifications timeline is split in two by the gap, with
// group information newer than the gap, and group information older
// than the gap.
// Filling a gap should not touch anything before the gap, so any
// information on groups already appearing before the gap should be
// discarded, while any information on groups appearing after the gap
// can be updated and re-ordered.
const oldestPageNotification = notifications.at(-1)?.page_min_id;
// replace the gap with the notifications + a new gap
const newerGroupKeys = groups
.slice(0, gapIndex)
.filter(isNotificationGroup)
.map((group) => group.group_key);
const toInsert: NotificationGroupsState['groups'] = notifications
.map((json) => createNotificationGroupFromJSON(json))
.filter((notification) => !newerGroupKeys.includes(notification.group_key));
const apiGroupKeys = (toInsert as NotificationGroup[]).map(
(group) => group.group_key,
);
const sinceId = gap.sinceId;
if (
notifications.length > 0 &&
!(
oldestPageNotification &&
sinceId &&
compareId(oldestPageNotification, sinceId) <= 0
)
) {
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
// Similarly, if we've fetched more than the gap's, this means we have completely filled it
toInsert.push({
type: 'gap',
maxId: notifications.at(-1)?.page_max_id,
sinceId,
} as NotificationGap);
}
// Remove older groups covered by the API
groups = groups.filter(
(groupOrGap) =>
groupOrGap.type !== 'gap' && !apiGroupKeys.includes(groupOrGap.group_key),
);
// Replace the gap with API results (+ the new gap if needed)
groups.splice(gapIndex, 1, ...toInsert);
// Finally, merge any adjacent gaps that could have been created by filtering
// groups earlier
mergeGaps(groups);
return groups;
}
// Ensure the groups list starts with a gap, mutating it to prepend one if needed
function ensureLeadingGap(
groups: NotificationGroupsState['groups'],
): NotificationGap {
if (groups[0]?.type === 'gap') {
// We're expecting new notifications, so discard the maxId if there is one
groups[0].maxId = undefined;
return groups[0];
} else {
const gap: NotificationGap = {
type: 'gap',
sinceId: groups[0]?.page_min_id,
};
groups.unshift(gap);
return gap;
}
}
export const notificationGroupsReducer = createReducer<NotificationGroupsState>( export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
initialState, initialState,
(builder) => { (builder) => {
@ -309,86 +413,36 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
updateLastReadId(state); updateLastReadId(state);
}) })
.addCase(fetchNotificationsGap.fulfilled, (state, action) => { .addCase(fetchNotificationsGap.fulfilled, (state, action) => {
const { notifications } = action.payload; state.groups = fillNotificationsGap(
state.groups,
// find the gap in the existing notifications action.meta.arg.gap,
const gapIndex = state.groups.findIndex( action.payload.notifications,
(groupOrGap) =>
groupOrGap.type === 'gap' &&
groupOrGap.sinceId === action.meta.arg.gap.sinceId &&
groupOrGap.maxId === action.meta.arg.gap.maxId,
); );
state.isLoading = false;
if (gapIndex < 0) updateLastReadId(state);
// We do not know where to insert, let's return })
return; .addCase(pollRecentNotifications.fulfilled, (state, action) => {
if (usePendingItems) {
// Filling a disconnection gap means we're getting historical data const gap = ensureLeadingGap(state.pendingGroups);
// about groups we may know or may not know about. state.pendingGroups = fillNotificationsGap(
state.pendingGroups,
// The notifications timeline is split in two by the gap, with gap,
// group information newer than the gap, and group information older action.payload.notifications,
// than the gap. );
} else {
// Filling a gap should not touch anything before the gap, so any const gap = ensureLeadingGap(state.groups);
// information on groups already appearing before the gap should be state.groups = fillNotificationsGap(
// discarded, while any information on groups appearing after the gap state.groups,
// can be updated and re-ordered. gap,
action.payload.notifications,
const oldestPageNotification = notifications.at(-1)?.page_min_id;
// replace the gap with the notifications + a new gap
const newerGroupKeys = state.groups
.slice(0, gapIndex)
.filter(isNotificationGroup)
.map((group) => group.group_key);
const toInsert: NotificationGroupsState['groups'] = notifications
.map((json) => createNotificationGroupFromJSON(json))
.filter(
(notification) => !newerGroupKeys.includes(notification.group_key),
); );
const apiGroupKeys = (toInsert as NotificationGroup[]).map(
(group) => group.group_key,
);
const sinceId = action.meta.arg.gap.sinceId;
if (
notifications.length > 0 &&
!(
oldestPageNotification &&
sinceId &&
compareId(oldestPageNotification, sinceId) <= 0
)
) {
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
// Similarly, if we've fetched more than the gap's, this means we have completely filled it
toInsert.push({
type: 'gap',
maxId: notifications.at(-1)?.page_max_id,
sinceId,
} as NotificationGap);
} }
// Remove older groups covered by the API
state.groups = state.groups.filter(
(groupOrGap) =>
groupOrGap.type !== 'gap' &&
!apiGroupKeys.includes(groupOrGap.group_key),
);
// Replace the gap with API results (+ the new gap if needed)
state.groups.splice(gapIndex, 1, ...toInsert);
// Finally, merge any adjacent gaps that could have been created by filtering
// groups earlier
mergeGaps(state.groups);
state.isLoading = false; state.isLoading = false;
updateLastReadId(state); updateLastReadId(state);
trimNotifications(state);
}) })
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => { .addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
const notification = action.payload; const notification = action.payload;
@ -403,10 +457,11 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
}) })
.addCase(disconnectTimeline, (state, action) => { .addCase(disconnectTimeline, (state, action) => {
if (action.payload.timeline === 'home') { if (action.payload.timeline === 'home') {
if (state.groups.length > 0 && state.groups[0]?.type !== 'gap') { const groups = usePendingItems ? state.pendingGroups : state.groups;
state.groups.unshift({ if (groups.length > 0 && groups[0]?.type !== 'gap') {
groups.unshift({
type: 'gap', type: 'gap',
sinceId: state.groups[0]?.page_min_id, sinceId: groups[0]?.page_min_id,
}); });
} }
} }
@ -453,12 +508,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
} }
} }
} }
trimNotifications(state);
}); });
// Then build the consolidated list and clear pending groups // Then build the consolidated list and clear pending groups
state.groups = state.pendingGroups.concat(state.groups); state.groups = state.pendingGroups.concat(state.groups);
state.pendingGroups = []; state.pendingGroups = [];
mergeGaps(state.groups);
trimNotifications(state);
}) })
.addCase(updateScrollPosition.fulfilled, (state, action) => { .addCase(updateScrollPosition.fulfilled, (state, action) => {
state.scrolledToTop = action.payload.top; state.scrolledToTop = action.payload.top;
@ -518,13 +574,21 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
}, },
) )
.addMatcher( .addMatcher(
isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending), isAnyOf(
fetchNotifications.pending,
fetchNotificationsGap.pending,
pollRecentNotifications.pending,
),
(state) => { (state) => {
state.isLoading = true; state.isLoading = true;
}, },
) )
.addMatcher( .addMatcher(
isAnyOf(fetchNotifications.rejected, fetchNotificationsGap.rejected), isAnyOf(
fetchNotifications.rejected,
fetchNotificationsGap.rejected,
pollRecentNotifications.rejected,
),
(state) => { (state) => {
state.isLoading = false; state.isLoading = false;
}, },

View File

@ -130,21 +130,11 @@
.older { .older {
float: left; float: left;
padding-inline-start: 0; padding-inline-start: 0;
.fa {
display: inline-block;
margin-inline-end: 5px;
}
} }
.newer { .newer {
float: right; float: right;
padding-inline-end: 0; padding-inline-end: 0;
.fa {
display: inline-block;
margin-inline-start: 5px;
}
} }
.disabled { .disabled {

View File

@ -122,10 +122,6 @@ $content-width: 840px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
i.fa {
margin-inline-end: 5px;
}
&:hover { &:hover {
color: $primary-text-color; color: $primary-text-color;
transition: all 100ms linear; transition: all 100ms linear;
@ -306,10 +302,6 @@ $content-width: 840px;
box-shadow: none; box-shadow: none;
} }
.directory__tag .table-action-link .fa {
color: inherit;
}
.directory__tag h4 { .directory__tag h4 {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;

View File

@ -2891,10 +2891,6 @@ $ui-header-logo-wordmark-width: 99px;
padding-inline-end: 30px; padding-inline-end: 30px;
} }
.search__icon .fa {
top: 15px;
}
.scrollable { .scrollable {
overflow: visible; overflow: visible;
@ -4217,11 +4213,12 @@ a.status-card {
.timeline-hint { .timeline-hint {
text-align: center; text-align: center;
color: $darker-text-color; color: $dark-text-color;
padding: 15px; padding: 16px;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
cursor: default; font-size: 14px;
line-height: 21px;
strong { strong {
font-weight: 500; font-weight: 500;
@ -4238,10 +4235,10 @@ a.status-card {
color: lighten($highlight-text-color, 4%); color: lighten($highlight-text-color, 4%);
} }
} }
}
.timeline-hint--with-descendants { &--with-descendants {
border-top: 1px solid var(--background-border-color); border-top: 1px solid var(--background-border-color);
}
} }
.regeneration-indicator { .regeneration-indicator {
@ -6246,7 +6243,7 @@ a.status-card {
} }
.boost-modal__container { .boost-modal__container {
overflow-x: scroll; overflow-y: auto;
padding: 10px; padding: 10px;
.status { .status {

View File

@ -113,10 +113,6 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.fa {
flex: 0 0 auto;
}
strong { strong {
font-weight: 700; font-weight: 700;
} }

View File

@ -930,10 +930,6 @@ code {
font-weight: 700; font-weight: 700;
} }
} }
.fa {
font-weight: 400;
}
} }
} }
} }

View File

@ -41,14 +41,6 @@ body.rtl {
no-repeat left 8px center / auto 16px; no-repeat left 8px center / auto 16px;
} }
.fa-chevron-left::before {
content: '\F054';
}
.fa-chevron-right::before {
content: '\F053';
}
.dismissable-banner, .dismissable-banner,
.warning-banner { .warning-banner {
&__action { &__action {

View File

@ -142,11 +142,6 @@ a.table-action-link {
color: $highlight-text-color; color: $highlight-text-color;
} }
i.fa {
font-weight: 400;
margin-inline-end: 5px;
}
&:first-child { &:first-child {
padding-inline-start: 0; padding-inline-start: 0;
} }

View File

@ -169,11 +169,6 @@
&__message { &__message {
margin-bottom: 15px; margin-bottom: 15px;
.fa {
margin-inline-end: 5px;
color: $darker-text-color;
}
} }
&__card { &__card {
@ -366,9 +361,7 @@
padding-inline-end: 16px; padding-inline-end: 16px;
} }
.fa { .icon {
font-size: 16px;
&.active { &.active {
color: $highlight-text-color; color: $highlight-text-color;
} }

View File

@ -11,7 +11,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :domain, :title, :version, :source_url, :description, attributes :domain, :title, :version, :source_url, :description,
:usage, :thumbnail, :languages, :configuration, :usage, :thumbnail, :languages, :configuration,
:registrations :registrations, :api_versions
has_one :contact, serializer: ContactSerializer has_one :contact, serializer: ContactSerializer
has_many :rules, serializer: REST::RuleSerializer has_many :rules, serializer: REST::RuleSerializer
@ -95,6 +95,12 @@ class REST::InstanceSerializer < ActiveModel::Serializer
} }
end end
def api_versions
{
mastodon: 1,
}
end
private private
def registrations_enabled? def registrations_enabled?

View File

@ -1,11 +1,10 @@
:ruby :ruby
show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired?
own_votes = user_signed_in? ? poll.own_votes(current_account) : []
total_votes_count = poll.voters_count || poll.votes_count total_votes_count = poll.voters_count || poll.votes_count
.poll .poll
%ul %ul
- poll.loaded_options.each_with_index do |option, index| - poll.loaded_options.each do |option|
%li %li
- if show_results - if show_results
- percent = total_votes_count.positive? ? 100 * option.votes_count / total_votes_count : 0 - percent = total_votes_count.positive? ? 100 * option.votes_count / total_votes_count : 0
@ -14,9 +13,6 @@
#{percent.round}% #{percent.round}%
%span.poll__option__text %span.poll__option__text
= prerender_custom_emojis(h(option.title), status.emojis) = prerender_custom_emojis(h(option.title), status.emojis)
- if own_votes.include?(index)
%span.poll__voted
%i.poll__voted__mark.fa.fa-check
%progress{ max: 100, value: [percent, 1].max, 'aria-hidden': 'true' } %progress{ max: 100, value: [percent, 1].max, 'aria-hidden': 'true' }
%span.poll__chart %span.poll__chart

View File

@ -117,4 +117,4 @@ ko:
not_found: 찾을 수 없습니다 not_found: 찾을 수 없습니다
not_locked: 잠기지 않았습니다 not_locked: 잠기지 않았습니다
not_saved: not_saved:
other: "%{count}개의 에러로 인해 %{resource}가 저장 될 수 없습니다:" other: "%{count} 개의 에러로 인해 %{resource}가 저장 될 수 없습니다:"

View File

@ -493,7 +493,7 @@ gl:
content_policies: content_policies:
comment: Nota interna comment: Nota interna
description_html: Podes definir políticas acerca do contido que serán aplicadas a tódalas contas deste dominio e tódolos seus subdominios. description_html: Podes definir políticas acerca do contido que serán aplicadas a tódalas contas deste dominio e tódolos seus subdominios.
limited_federation_mode_description_html: Podes elexir se permites ou non a federación con este dominio. limited_federation_mode_description_html: Podes escoller se permites ou non a federación con este dominio.
policies: policies:
reject_media: Rexeitar multimedia reject_media: Rexeitar multimedia
reject_reports: Rexeitar denuncias reject_reports: Rexeitar denuncias

View File

@ -257,8 +257,10 @@ ko:
destroy_user_role_html: "%{name} 님이 %{target} 역할을 삭제했습니다" destroy_user_role_html: "%{name} 님이 %{target} 역할을 삭제했습니다"
disable_2fa_user_html: "%{name} 님이 사용자 %{target} 님의 2단계 인증을 비활성화 했습니다" disable_2fa_user_html: "%{name} 님이 사용자 %{target} 님의 2단계 인증을 비활성화 했습니다"
disable_custom_emoji_html: "%{name} 님이 에모지 %{target}를 비활성화했습니다" disable_custom_emoji_html: "%{name} 님이 에모지 %{target}를 비활성화했습니다"
disable_sign_in_token_auth_user_html: "%{name} 님이 %{target} 님의 이메일 토큰 인증을 비활성화했습니다"
disable_user_html: "%{name} 님이 사용자 %{target}의 로그인을 비활성화했습니다" disable_user_html: "%{name} 님이 사용자 %{target}의 로그인을 비활성화했습니다"
enable_custom_emoji_html: "%{name} 님이 에모지 %{target}를 활성화했습니다" enable_custom_emoji_html: "%{name} 님이 에모지 %{target}를 활성화했습니다"
enable_sign_in_token_auth_user_html: "%{name} 님이 %{target} 님의 이메일 토큰 인증을 활성화했습니다"
enable_user_html: "%{name} 님이 사용자 %{target}의 로그인을 활성화했습니다" enable_user_html: "%{name} 님이 사용자 %{target}의 로그인을 활성화했습니다"
memorialize_account_html: "%{name} 님이 %{target}의 계정을 고인의 계정 페이지로 전환했습니다" memorialize_account_html: "%{name} 님이 %{target}의 계정을 고인의 계정 페이지로 전환했습니다"
promote_user_html: "%{name} 님이 사용자 %{target}를 승급시켰습니다" promote_user_html: "%{name} 님이 사용자 %{target}를 승급시켰습니다"
@ -266,6 +268,7 @@ ko:
reject_user_html: "%{name} 님이 %{target} 님의 가입을 거부했습니다" reject_user_html: "%{name} 님이 %{target} 님의 가입을 거부했습니다"
remove_avatar_user_html: "%{name} 님이 %{target} 님의 아바타를 지웠습니다" remove_avatar_user_html: "%{name} 님이 %{target} 님의 아바타를 지웠습니다"
reopen_report_html: "%{name} 님이 신고 %{target}을 다시 열었습니다" reopen_report_html: "%{name} 님이 신고 %{target}을 다시 열었습니다"
resend_user_html: "%{name} 님이 %{target} 님에 대한 확인 메일을 다시 보냈습니다"
reset_password_user_html: "%{name} 님이 사용자 %{target}의 암호를 초기화했습니다" reset_password_user_html: "%{name} 님이 사용자 %{target}의 암호를 초기화했습니다"
resolve_report_html: "%{name} 님이 %{target}번 신고를 해결로 변경하였습니다" resolve_report_html: "%{name} 님이 %{target}번 신고를 해결로 변경하였습니다"
sensitive_account_html: "%{name} 님이 %{target}의 미디어를 민감함으로 표시했습니다" sensitive_account_html: "%{name} 님이 %{target}의 미디어를 민감함으로 표시했습니다"
@ -423,6 +426,7 @@ ko:
allow_registrations_with_approval: 승인을 통한 가입 허용 allow_registrations_with_approval: 승인을 통한 가입 허용
attempts_over_week: attempts_over_week:
other: 지난 주 동안 %{count}건의 가입 시도가 있었습니다 other: 지난 주 동안 %{count}건의 가입 시도가 있었습니다
created_msg: 이메일 도메인 차단 규칙을 생성했습니다
delete: 삭제하기 delete: 삭제하기
dns: dns:
types: types:
@ -432,7 +436,9 @@ ko:
create: 도메인 추가하기 create: 도메인 추가하기
resolve: 도메인 검사 resolve: 도메인 검사
title: 새 이메일 도메인 차단 title: 새 이메일 도메인 차단
no_email_domain_block_selected: 아무 것도 선택 되지 않아 어떤 이메일 도메인 차단도 변경되지 않았습니다
not_permitted: 허용하지 않음 not_permitted: 허용하지 않음
resolved_dns_records_hint_html: 도메인 네임은 다음의 MX 도메인으로 연결되어 있으며, 이메일을 받는데 필수적입니다. MX 도메인을 차단하면 같은 MX 도메인을 사용하는 어떤 이메일이라도 가입할 수 없게 되며, 보여지는 도메인이 다르더라도 적용됩니다. <strong>주요 이메일 제공자를 차단하지 않도록 조심하세요.</strong>
resolved_through_html: "%{domain}을 통해 리졸빙됨" resolved_through_html: "%{domain}을 통해 리졸빙됨"
title: 차단된 이메일 도메인 title: 차단된 이메일 도메인
export_domain_allows: export_domain_allows:
@ -584,6 +590,7 @@ ko:
resolve_description_html: 신고된 계정에 대해 아무런 동작도 취하지 않으며, 처벌기록이 남지 않으며, 신고는 처리됨으로 변경됩니다. resolve_description_html: 신고된 계정에 대해 아무런 동작도 취하지 않으며, 처벌기록이 남지 않으며, 신고는 처리됨으로 변경됩니다.
silence_description_html: 이 계정을 이미 팔로우 하고 있는 사람이나 수동으로 찾아보는 사람에게만 프로필이 보여지고, 도달 범위를 엄격하게 제한합니다. 언제든지 되돌릴 수 있습니다. 이 계정에 대한 모든 신고를 닫습니다. silence_description_html: 이 계정을 이미 팔로우 하고 있는 사람이나 수동으로 찾아보는 사람에게만 프로필이 보여지고, 도달 범위를 엄격하게 제한합니다. 언제든지 되돌릴 수 있습니다. 이 계정에 대한 모든 신고를 닫습니다.
suspend_description_html: 이 계정과 이 계정의 콘텐츠들은 접근 불가능해지고 삭제될 것이며, 상호작용은 불가능해집니다. 30일 이내에 되돌릴 수 있습니다. 이 계정에 대한 모든 신고를 닫습니다. suspend_description_html: 이 계정과 이 계정의 콘텐츠들은 접근 불가능해지고 삭제될 것이며, 상호작용은 불가능해집니다. 30일 이내에 되돌릴 수 있습니다. 이 계정에 대한 모든 신고를 닫습니다.
actions_description_html: 이 신고를 해결하기 위해 취해야 할 조치를 지정해주세요. 신고된 계정에 대해 처벌 조치를 취하면, <strong>스팸</strong> 카테고리가 선택된 경우를 제외하고 해당 계정으로 이메일 알림이 전송됩니다.
actions_description_remote_html: 이 신고를 해결하기 위해 실행할 행동을 결정하세요. 이 결정은 이 원격 계정과 그 콘텐츠를 다루는 방식에 대해 <strong>이 서버</strong>에서만 영향을 끼칩니다 actions_description_remote_html: 이 신고를 해결하기 위해 실행할 행동을 결정하세요. 이 결정은 이 원격 계정과 그 콘텐츠를 다루는 방식에 대해 <strong>이 서버</strong>에서만 영향을 끼칩니다
add_to_report: 신고에 더 추가하기 add_to_report: 신고에 더 추가하기
already_suspended_badges: already_suspended_badges:
@ -648,6 +655,7 @@ ko:
delete_data_html: "<strong>@%{acct}</strong>의 프로필과 콘텐츠를 30일의 유예기간 이후에 삭제합니다" delete_data_html: "<strong>@%{acct}</strong>의 프로필과 콘텐츠를 30일의 유예기간 이후에 삭제합니다"
preview_preamble_html: "<strong>@%{acct}</strong>는 다음 내용의 경고를 받게 됩니다:" preview_preamble_html: "<strong>@%{acct}</strong>는 다음 내용의 경고를 받게 됩니다:"
record_strike_html: 향후 규칙위반에 대한 참고사항이 될 수 있도록 <strong>@%{acct}</strong>에 대한 처벌기록을 남깁니다 record_strike_html: 향후 규칙위반에 대한 참고사항이 될 수 있도록 <strong>@%{acct}</strong>에 대한 처벌기록을 남깁니다
send_email_html: "<strong>@%{acct}</strong>에게 경고 메일을 보냅니다"
warning_placeholder: 중재 결정에 대한 추가적인 이유. warning_placeholder: 중재 결정에 대한 추가적인 이유.
target_origin: 신고된 계정의 소속 target_origin: 신고된 계정의 소속
title: 신고 title: 신고
@ -685,6 +693,7 @@ ko:
manage_appeals: 이의제기 관리 manage_appeals: 이의제기 관리
manage_appeals_description: 사용자가 중재에 대한 이의제기를 리뷰할 수 있도록 허용 manage_appeals_description: 사용자가 중재에 대한 이의제기를 리뷰할 수 있도록 허용
manage_blocks: 차단 관리 manage_blocks: 차단 관리
manage_blocks_description: 사용자가 이메일 제공자와 IP 주소를 차단할 수 있도록 허용
manage_custom_emojis: 커스텀 에모지 관리 manage_custom_emojis: 커스텀 에모지 관리
manage_custom_emojis_description: 사용자가 서버의 커스텀 에모지를 관리할 수 있도록 허용 manage_custom_emojis_description: 사용자가 서버의 커스텀 에모지를 관리할 수 있도록 허용
manage_federation: 연합 관리 manage_federation: 연합 관리
@ -702,6 +711,7 @@ ko:
manage_taxonomies: 분류 관리 manage_taxonomies: 분류 관리
manage_taxonomies_description: 사용자가 트렌드를 리뷰하고 해시태그 설정을 수정할 수 있도록 허용합니다 manage_taxonomies_description: 사용자가 트렌드를 리뷰하고 해시태그 설정을 수정할 수 있도록 허용합니다
manage_user_access: 사용자 접근 관리 manage_user_access: 사용자 접근 관리
manage_user_access_description: 사용자가 다른 사용자의 2차 인증을 비활성화 하거나, 이메일 주소를 바꾸거나, 암호를 초기화 할 수 있도록 허용
manage_users: 사용자 관리 manage_users: 사용자 관리
manage_users_description: 사용자가 다른 사용자의 상세정보를 보고 해당 사용자에 대한 중재활동을 할 수 있도록 허용 manage_users_description: 사용자가 다른 사용자의 상세정보를 보고 해당 사용자에 대한 중재활동을 할 수 있도록 허용
manage_webhooks: 웹훅 관리 manage_webhooks: 웹훅 관리
@ -776,6 +786,7 @@ ko:
destroyed_msg: 사이트 업로드를 성공적으로 삭제했습니다! destroyed_msg: 사이트 업로드를 성공적으로 삭제했습니다!
software_updates: software_updates:
critical_update: 긴급 — 빠른 업데이트 요망 critical_update: 긴급 — 빠른 업데이트 요망
description: 최신 수정 사항과 기능을 활용하기 위해 Mastodon 설치를 최신 상태로 유지하기를 권장합니다. 더욱이, 때로는 보안 문제를 피하기 위해 Mastodon을 적절한 시점에 긴급 업데이트해야 하는 경우도 있습니다. 따라서 Mastodon은 30분마다 업데이트를 확인하며, 이메일 알림 환경설정에 따라 사용자에게 알려드립니다.
documentation_link: 더 알아보기 documentation_link: 더 알아보기
release_notes: 릴리스 노트 release_notes: 릴리스 노트
title: 사용 가능한 업데이트 title: 사용 가능한 업데이트
@ -884,12 +895,18 @@ ko:
trends: trends:
allow: 허용 allow: 허용
approved: 승인됨 approved: 승인됨
disallow: 거부 confirm_allow: 정말로 선택된 태그들을 허용하시겠습니까?
confirm_disallow: 정말로 선택된 태그들을 금지하시겠습니까?
disallow: 금지
links: links:
allow: 링크 허용 allow: 링크 허용
allow_provider: 발행처 허용 allow_provider: 발행처 허용
confirm_allow: 정말로 선택된 링크들을 허용하시겠습니까?
confirm_allow_provider: 정말로 선택된 제공자들을 허용하시겠습니까?
confirm_disallow: 정말로 선택된 링크들을 금지하시겠습니까?
confirm_disallow_provider: 정말로 선택된 제공자들을 금지하시겠습니까?
description_html: 현재 서버에서 게시물을 볼 수 있는 계정에서 많이 공유되고 있는 링크들입니다. 사용자가 세상 돌아가는 상황을 파악하는 데 도움이 됩니다. 출처를 승인할 때까지 링크는 공개적으로 게시되지 않습니다. 각각의 링크를 개별적으로 허용하거나 거부할 수도 있습니다. description_html: 현재 서버에서 게시물을 볼 수 있는 계정에서 많이 공유되고 있는 링크들입니다. 사용자가 세상 돌아가는 상황을 파악하는 데 도움이 됩니다. 출처를 승인할 때까지 링크는 공개적으로 게시되지 않습니다. 각각의 링크를 개별적으로 허용하거나 거부할 수도 있습니다.
disallow: 링크 거부 disallow: 링크 금지
disallow_provider: 발행처 비허용 disallow_provider: 발행처 비허용
no_link_selected: 아무 것도 선택 되지 않아 어떤 링크도 바뀌지 않았습니다 no_link_selected: 아무 것도 선택 되지 않아 어떤 링크도 바뀌지 않았습니다
publishers: publishers:
@ -910,9 +927,13 @@ ko:
statuses: statuses:
allow: 게시물 허용 allow: 게시물 허용
allow_account: 작성자 허용 allow_account: 작성자 허용
confirm_allow: 정말로 선택된 게시물들을 허용하시겠습니까?
confirm_allow_account: 정말로 선택된 계정들을 허용하시겠습니까?
confirm_disallow: 정말로 선택된 게시물들을 금지하시겠습니까?
confirm_disallow_account: 정말로 선택된 계정들을 금지하시겠습니까?
description_html: 여러분의 서버가 알기로 현재 많은 수의 공유와 좋아요가 되고 있는 게시물들입니다. 새로운 사용자나 돌아오는 사용자들이 팔로우 할 사람들을 찾는 데 도움이 될 수 있습니다. 작성자를 승인하고, 작성자가 그들의 계정이 다른 계정에게 탐색되도록 설정하지 않는 한 게시물들은 공개적으로 표시되지 않습니다. 또한 각각의 게시물을 별개로 거절할 수도 있습니다. description_html: 여러분의 서버가 알기로 현재 많은 수의 공유와 좋아요가 되고 있는 게시물들입니다. 새로운 사용자나 돌아오는 사용자들이 팔로우 할 사람들을 찾는 데 도움이 될 수 있습니다. 작성자를 승인하고, 작성자가 그들의 계정이 다른 계정에게 탐색되도록 설정하지 않는 한 게시물들은 공개적으로 표시되지 않습니다. 또한 각각의 게시물을 별개로 거절할 수도 있습니다.
disallow: 게시물 비허용 disallow: 게시물 비허용
disallow_account: 작성자 비허용 disallow_account: 작성자 금지
no_status_selected: 아무 것도 선택 되지 않아 어떤 유행중인 게시물도 바뀌지 않았습니다 no_status_selected: 아무 것도 선택 되지 않아 어떤 유행중인 게시물도 바뀌지 않았습니다
not_discoverable: 작성자가 발견되기를 원치 않습니다 not_discoverable: 작성자가 발견되기를 원치 않습니다
shared_by: shared_by:
@ -940,6 +961,7 @@ ko:
usage_comparison: 오늘은 %{today}회 쓰였고, 어제는 %{yesterday}회 쓰임 usage_comparison: 오늘은 %{today}회 쓰였고, 어제는 %{yesterday}회 쓰임
used_by_over_week: used_by_over_week:
other: 지난 주 동안 %{count} 명의 사람들이 사용했습니다 other: 지난 주 동안 %{count} 명의 사람들이 사용했습니다
title: 추천과 유행
trending: 유행 중 trending: 유행 중
warning_presets: warning_presets:
add_new: 새로 추가 add_new: 새로 추가
@ -1053,6 +1075,7 @@ ko:
redirect_to_app_html: 곧 <strong>%{app_name}</strong>으로 리디렉션 됩니다. 안 된다면, %{clicking_this_link}하거나 직접 앱으로 돌아가세요. redirect_to_app_html: 곧 <strong>%{app_name}</strong>으로 리디렉션 됩니다. 안 된다면, %{clicking_this_link}하거나 직접 앱으로 돌아가세요.
registration_complete: 지금 막 %{domain} 가입이 완료되었습니다! registration_complete: 지금 막 %{domain} 가입이 완료되었습니다!
welcome_title: "%{name} 님 반갑습니다!" welcome_title: "%{name} 님 반갑습니다!"
wrong_email_hint: 만약 이메일 주소가 올바르지 않다면, 계정 설정에서 변경할 수 있습니다.
delete_account: 계정 삭제 delete_account: 계정 삭제
delete_account_html: 계정을 삭제하고 싶은 경우, <a href="%{path}">여기서</a> 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다. delete_account_html: 계정을 삭제하고 싶은 경우, <a href="%{path}">여기서</a> 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다.
description: description:
@ -1098,6 +1121,7 @@ ko:
email_below_hint_html: 스팸 폴더를 체크해보거나, 새로 요청할 수 있습니다. 이메일을 잘못 입력한 경우 수정할 수 있습니다. email_below_hint_html: 스팸 폴더를 체크해보거나, 새로 요청할 수 있습니다. 이메일을 잘못 입력한 경우 수정할 수 있습니다.
email_settings_hint_html: "%{email}을 인증하기 위해 우리가 보낸 링크를 누르세요. 여기서 기다리겠습니다." email_settings_hint_html: "%{email}을 인증하기 위해 우리가 보낸 링크를 누르세요. 여기서 기다리겠습니다."
link_not_received: 링크를 못 받으셨나요? link_not_received: 링크를 못 받으셨나요?
new_confirmation_instructions_sent: 확인 링크가 담긴 이메일이 몇 분 안에 도착할것입니다!
title: 수신함 확인하기 title: 수신함 확인하기
sign_in: sign_in:
preamble_html: "<strong>%{domain}</strong>의 계정 정보를 이용해 로그인 하세요. 만약 내 계정이 다른 서버에 존재한다면, 여기서는 로그인 할 수 없습니다." preamble_html: "<strong>%{domain}</strong>의 계정 정보를 이용해 로그인 하세요. 만약 내 계정이 다른 서버에 존재한다면, 여기서는 로그인 할 수 없습니다."
@ -1108,7 +1132,9 @@ ko:
title: "%{domain}에 가입하기 위한 정보들을 입력하세요." title: "%{domain}에 가입하기 위한 정보들을 입력하세요."
status: status:
account_status: 계정 상태 account_status: 계정 상태
confirming: 이메일 확인 과정이 완료되기를 기다리는 중.
functional: 계정이 완벽히 작동합니다. functional: 계정이 완벽히 작동합니다.
pending: 당신의 가입 신청은 스태프의 검사를 위해 대기 중입니다. 시간이 조금 걸릴 수 있습니다. 가입 신청이 승인되면 이메일을 받게 됩니다.
redirecting_to: 계정이 %{acct}로 리다이렉트 중이기 때문에 비활성 상태입니다. redirecting_to: 계정이 %{acct}로 리다이렉트 중이기 때문에 비활성 상태입니다.
self_destruct: "%{domain} 도메인 폐쇄가 진행중이기 때문에, 계정에는 제한된 접근만 할 수 있습니다." self_destruct: "%{domain} 도메인 폐쇄가 진행중이기 때문에, 계정에는 제한된 접근만 할 수 있습니다."
view_strikes: 내 계정에 대한 과거 중재 기록 보기 view_strikes: 내 계정에 대한 과거 중재 기록 보기
@ -1151,6 +1177,9 @@ ko:
before: '진행하기 전, 주의사항을 꼼꼼히 읽어보세요:' before: '진행하기 전, 주의사항을 꼼꼼히 읽어보세요:'
caches: 다른 서버에 캐싱된 정보들은 남아있을 수 있습니다 caches: 다른 서버에 캐싱된 정보들은 남아있을 수 있습니다
data_removal: 당신의 게시물과 다른 정보들은 영구적으로 삭제 됩니다 data_removal: 당신의 게시물과 다른 정보들은 영구적으로 삭제 됩니다
email_change_html: 계정을 지우지 않고도 <a href="%{path}">이메일 주소를 변경할 수 있습니다</a>
email_contact_html: 아직 도착하지 않았다면, <a href="mailto:%{email}">%{email}</a>에 메일을 보내 도움을 요청할 수 있습니다
email_reconfirmation_html: 아직 확인 메일이 도착하지 않은 경우, <a href="%{path}">다시 요청할 수 있습니다</a>
irreversible: 계정을 복구하거나 다시 사용할 수 없게 됩니다 irreversible: 계정을 복구하거나 다시 사용할 수 없게 됩니다
more_details_html: 더 자세한 정보는, <a href="%{terms_path}">개인정보처리방침</a>을 참고하세요. more_details_html: 더 자세한 정보는, <a href="%{terms_path}">개인정보처리방침</a>을 참고하세요.
username_available: 당신의 계정명은 다시 사용할 수 있게 됩니다 username_available: 당신의 계정명은 다시 사용할 수 있게 됩니다
@ -1386,6 +1415,16 @@ ko:
unsubscribe: unsubscribe:
action: 네, 구독 취소합니다 action: 네, 구독 취소합니다
complete: 구독 취소됨 complete: 구독 취소됨
confirmation_html: 정말로 %{domain}에서 %{email}로 보내는 마스토돈의 %{type}에 대한 구독을 취소하시겠습니까? 언제든지 <a href="%{settings_path}">이메일 알림 설정</a>에서 다시 구독할 수 있습니다.
emails:
notification_emails:
favourite: 좋아요 알림 이메일
follow: 팔로우 알림 이메일
follow_request: 팔로우 요청 이메일
mention: 멘션 알림 이메일
reblog: 부스트 알림 이메일
resubscribe_html: 만약 실수로 구독 취소를 했다면 <a href="%{settings_path}">이메일 알림 설정</a>에서 다시 구독할 수 있습니다.
success_html: 이제 더이상 %{domain}의 마스토돈에서 %{email}로 %{type} 알림을 보내지 않습니다.
title: 구독 취소 title: 구독 취소
media_attachments: media_attachments:
validations: validations:
@ -1466,6 +1505,8 @@ ko:
update: update:
subject: "%{name} 님이 게시물을 수정했습니다" subject: "%{name} 님이 게시물을 수정했습니다"
notifications: notifications:
administration_emails: 관리자 이메일 알림
email_events: 이메일 알림에 대한 이벤트
email_events_hint: '알림 받을 이벤트를 선택해주세요:' email_events_hint: '알림 받을 이벤트를 선택해주세요:'
number: number:
human: human:
@ -1624,6 +1665,7 @@ ko:
import: 데이터 가져오기 import: 데이터 가져오기
import_and_export: 가져오기 & 내보내기 import_and_export: 가져오기 & 내보내기
migrate: 계정 이동 migrate: 계정 이동
notifications: 이메일 알림
preferences: 환경설정 preferences: 환경설정
profile: 공개 프로필 profile: 공개 프로필
relationships: 팔로잉과 팔로워 relationships: 팔로잉과 팔로워
@ -1646,12 +1688,12 @@ ko:
statuses: statuses:
attached: attached:
audio: audio:
other: "%{count}개의 오디오" other: "%{count} 개의 오디오"
description: '첨부: %{attached}' description: '첨부: %{attached}'
image: image:
other: "%{count}장의 이미지" other: "%{count}장의 이미지"
video: video:
other: "%{count}개의 영상" other: "%{count} 개의 영상"
boosted_from_html: "%{acct_link}의 글을 부스트" boosted_from_html: "%{acct_link}의 글을 부스트"
content_warning: '열람 주의: %{warning}' content_warning: '열람 주의: %{warning}'
default_language: 화면 표시 언어와 동일하게 default_language: 화면 표시 언어와 동일하게
@ -1671,7 +1713,7 @@ ko:
total_people: total_people:
other: "%{count}명" other: "%{count}명"
total_votes: total_votes:
other: "%{count}명 투표함" other: "%{count} 명 투표함"
vote: 투표 vote: 투표
show_more: 더 보기 show_more: 더 보기
show_thread: 글타래 보기 show_thread: 글타래 보기
@ -1863,6 +1905,7 @@ ko:
invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다
otp_lost_help_html: 만약 양쪽 모두를 잃어버렸다면 %{email}을 통해 복구할 수 있습니다 otp_lost_help_html: 만약 양쪽 모두를 잃어버렸다면 %{email}을 통해 복구할 수 있습니다
rate_limited: 너무 많은 인증 시도가 있었습니다, 잠시 후에 시도하세요. rate_limited: 너무 많은 인증 시도가 있었습니다, 잠시 후에 시도하세요.
seamless_external_login: 외부 서비스를 이용해 로그인했으므로 이메일과 암호는 설정할 수 없습니다.
signed_in_as: '다음과 같이 로그인 중:' signed_in_as: '다음과 같이 로그인 중:'
verification: verification:
extra_instructions_html: <strong>팁:</strong> 웹사이트에 안 보이는 링크로 삽입할 수 있습니다. 중요한 것은 나를 도용하는 것을 방지하는 <code>rel="me"</code> 부분입니다. 심지어 <code>a</code> 대신 <code>link</code>태그를 페이지 헤더에 넣는 것으로 대체할 수도 있습니다. 하지만 HTML 코드는 자바스크립트 실행 없이 접근이 가능해야 합니다. extra_instructions_html: <strong>팁:</strong> 웹사이트에 안 보이는 링크로 삽입할 수 있습니다. 중요한 것은 나를 도용하는 것을 방지하는 <code>rel="me"</code> 부분입니다. 심지어 <code>a</code> 대신 <code>link</code>태그를 페이지 헤더에 넣는 것으로 대체할 수도 있습니다. 하지만 HTML 코드는 자바스크립트 실행 없이 접근이 가능해야 합니다.

View File

@ -35,6 +35,7 @@ ru:
created_msg: Заметка модератора успешно создана! created_msg: Заметка модератора успешно создана!
destroyed_msg: Заметка модератора успешно удалена! destroyed_msg: Заметка модератора успешно удалена!
accounts: accounts:
add_email_domain_block: Забанить email домен
approve: Подтвердить approve: Подтвердить
approved_msg: Успешно одобрена заявка на регистрацию %{username} approved_msg: Успешно одобрена заявка на регистрацию %{username}
are_you_sure: Вы уверены? are_you_sure: Вы уверены?
@ -428,6 +429,7 @@ ru:
many: "%{count} попыток за последнюю неделю" many: "%{count} попыток за последнюю неделю"
one: "%{count} попытка за последнюю неделю" one: "%{count} попытка за последнюю неделю"
other: "%{count} попыток регистрации за последнюю неделю" other: "%{count} попыток регистрации за последнюю неделю"
created_msg: Домен email забанен, ура
delete: Удалить delete: Удалить
dns: dns:
types: types:
@ -599,6 +601,9 @@ ru:
suspend_description_html: Аккаунт и все его содержимое будут недоступны и в конечном итоге удалены, и взаимодействие с ним будет невозможно. Это действие можно отменить в течение 30 дней. Отменяет все жалобы против этого аккаунта. suspend_description_html: Аккаунт и все его содержимое будут недоступны и в конечном итоге удалены, и взаимодействие с ним будет невозможно. Это действие можно отменить в течение 30 дней. Отменяет все жалобы против этого аккаунта.
actions_description_remote_html: Решите вопрос о том, какие меры необходимо принять для урегулирования этой жалобы. Это повлияет только на то, как <strong>ваш</strong> сервер взаимодействует с этим удаленным аккаунтом и обрабатывает его содержимое. actions_description_remote_html: Решите вопрос о том, какие меры необходимо принять для урегулирования этой жалобы. Это повлияет только на то, как <strong>ваш</strong> сервер взаимодействует с этим удаленным аккаунтом и обрабатывает его содержимое.
add_to_report: Прикрепить ещё add_to_report: Прикрепить ещё
already_suspended_badges:
local: На этом сервере уже забанен
remote: На этом сервере уже забанен
are_you_sure: Вы уверены? are_you_sure: Вы уверены?
assign_to_self: Назначить себе assign_to_self: Назначить себе
assigned: Назначенный модератор assigned: Назначенный модератор
@ -752,6 +757,7 @@ ru:
desc_html: Это зависит от внешних скриптов из hCaptcha, которые могут представлять интерес для безопасности и конфиденциальности. Кроме того, <strong>это может сделать процесс регистрации значительно менее доступным для некоторых (особенно отключенных) людей</strong>. По этим причинам просьба рассмотреть альтернативные меры, такие, как регистрация, основанная на официальном утверждении или на приглашении. desc_html: Это зависит от внешних скриптов из hCaptcha, которые могут представлять интерес для безопасности и конфиденциальности. Кроме того, <strong>это может сделать процесс регистрации значительно менее доступным для некоторых (особенно отключенных) людей</strong>. По этим причинам просьба рассмотреть альтернативные меры, такие, как регистрация, основанная на официальном утверждении или на приглашении.
title: Запрашивать новых пользователей для решения CAPTCHA для подтверждения учетной записи title: Запрашивать новых пользователей для решения CAPTCHA для подтверждения учетной записи
content_retention: content_retention:
danger_zone: Осторожно!
preamble: Управление сохранением пользовательского контента в Mastodon. preamble: Управление сохранением пользовательского контента в Mastodon.
title: Хранение контента title: Хранение контента
default_noindex: default_noindex:

View File

@ -138,7 +138,7 @@ gl:
permissions_as_keys: As usuarias con este rol terán acceso a... permissions_as_keys: As usuarias con este rol terán acceso a...
position: O rol superior decide nos conflitos en certas situacións. Algunhas accións só poden aplicarse sobre roles cunha prioridade menor position: O rol superior decide nos conflitos en certas situacións. Algunhas accións só poden aplicarse sobre roles cunha prioridade menor
webhook: webhook:
events: Elixir eventos a enviar events: Escoller eventos a enviar
template: Crea o teu propio JSON interpolando variables. Déixao en branco para usar o JSON predeterminado. template: Crea o teu propio JSON interpolando variables. Déixao en branco para usar o JSON predeterminado.
url: A onde se enviarán os eventos url: A onde se enviarán os eventos
labels: labels:

View File

@ -238,6 +238,7 @@ ru:
warn: Скрыть с предупреждением warn: Скрыть с предупреждением
form_admin_settings: form_admin_settings:
activity_api_enabled: Публикация агрегированной статистики активности пользователей в API activity_api_enabled: Публикация агрегированной статистики активности пользователей в API
app_icon: Иконка приложения
backups_retention_period: Период хранения архива пользователя backups_retention_period: Период хранения архива пользователя
bootstrap_timeline_accounts: Всегда рекомендовать эти учетные записи новым пользователям bootstrap_timeline_accounts: Всегда рекомендовать эти учетные записи новым пользователям
closed_registrations_message: Сообщение, когда регистрация недоступна closed_registrations_message: Сообщение, когда регистрация недоступна
@ -307,6 +308,7 @@ ru:
listable: Разрешить показ хэштега в поиске или в каталоге профилей listable: Разрешить показ хэштега в поиске или в каталоге профилей
name: Хэштег name: Хэштег
trendable: Разрешить показ хэштега в трендах trendable: Разрешить показ хэштега в трендах
usable: Позволить этот хэштег в локальных сообщениях
user: user:
role: Роль role: Роль
time_zone: Часовой пояс time_zone: Часовой пояс

View File

@ -18,6 +18,7 @@ describe 'Instances' do
expect(body_as_json) expect(body_as_json)
.to be_present .to be_present
.and include(title: 'Mastodon Glitch Edition') .and include(title: 'Mastodon Glitch Edition')
.and include_api_versions
.and include_configuration_limits .and include_configuration_limits
end end
end end
@ -32,6 +33,7 @@ describe 'Instances' do
expect(body_as_json) expect(body_as_json)
.to be_present .to be_present
.and include(title: 'Mastodon Glitch Edition') .and include(title: 'Mastodon Glitch Edition')
.and include_api_versions
.and include_configuration_limits .and include_configuration_limits
end end
end end
@ -53,5 +55,13 @@ describe 'Instances' do
) )
) )
end end
def include_api_versions
include(
api_versions: include(
mastodon: anything
)
)
end
end end
end end