Merge commit 'ca9966ce2ff79dcac90b2feced65fa991534d53e' into glitch-soc/merge-upstream
This commit is contained in:
@@ -62,7 +62,7 @@ module Admin
|
||||
|
||||
def resource_params
|
||||
params
|
||||
.expect(user_role: [:name, :color, :highlighted, :position, permissions_as_keys: []])
|
||||
.expect(user_role: [:name, :color, :highlighted, :position, :require_2fa, permissions_as_keys: []])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,19 +61,25 @@ class ApplicationController < ActionController::Base
|
||||
return if request.referer.blank?
|
||||
|
||||
redirect_uri = URI(request.referer)
|
||||
return if redirect_uri.path.start_with?('/auth')
|
||||
return if redirect_uri.path.start_with?('/auth', '/settings/two_factor_authentication', '/settings/otp_authentication')
|
||||
|
||||
stored_url = redirect_uri.to_s if redirect_uri.host == request.host && redirect_uri.port == request.port
|
||||
|
||||
store_location_for(:user, stored_url)
|
||||
end
|
||||
|
||||
def mfa_setup_path(path_params = {})
|
||||
settings_two_factor_authentication_methods_path(path_params)
|
||||
end
|
||||
|
||||
def require_functional!
|
||||
return if current_user.functional?
|
||||
|
||||
respond_to do |format|
|
||||
format.any do
|
||||
if current_user.confirmed?
|
||||
if current_user.missing_2fa?
|
||||
redirect_to mfa_setup_path
|
||||
elsif current_user.confirmed?
|
||||
redirect_to edit_user_registration_path
|
||||
else
|
||||
redirect_to auth_setup_path
|
||||
@@ -85,6 +91,8 @@ class ApplicationController < ActionController::Base
|
||||
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
|
||||
elsif !current_user.approved?
|
||||
render json: { error: 'Your login is currently pending approval' }, status: 403
|
||||
elsif current_user.missing_2fa?
|
||||
render json: { error: 'Your account requires two-factor authentication' }, status: 403
|
||||
elsif !current_user.functional?
|
||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||
end
|
||||
|
||||
@@ -42,7 +42,7 @@ module ChallengableConcern
|
||||
end
|
||||
|
||||
def render_challenge
|
||||
render 'auth/challenges/new', layout: 'auth'
|
||||
render 'auth/challenges/new', layout: params[:oauth] ? 'modal' : 'auth'
|
||||
end
|
||||
|
||||
def challenge_passed?
|
||||
|
||||
@@ -24,4 +24,8 @@ class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
def truthy_param?(key)
|
||||
ActiveModel::Type::Boolean.new.cast(params[key])
|
||||
end
|
||||
|
||||
def mfa_setup_path
|
||||
super({ oauth: true })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
module TwoFactorAuthentication
|
||||
class BaseController < ::Settings::BaseController
|
||||
layout -> { truthy_param?(:oauth) ? 'modal' : 'admin' }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,12 +4,15 @@ module Settings
|
||||
module TwoFactorAuthentication
|
||||
class ConfirmationsController < BaseController
|
||||
include ChallengableConcern
|
||||
include Devise::Controllers::StoreLocation
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_challenge!
|
||||
before_action :ensure_otp_secret
|
||||
|
||||
helper_method :return_to_app_url
|
||||
|
||||
def new
|
||||
prepare_two_factor_form
|
||||
end
|
||||
@@ -37,6 +40,10 @@ module Settings
|
||||
|
||||
private
|
||||
|
||||
def return_to_app_url
|
||||
stored_location_for(:user)
|
||||
end
|
||||
|
||||
def confirmation_params
|
||||
params.expect(form_two_factor_confirmation: [:otp_attempt])
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ module Settings
|
||||
def create
|
||||
session[:new_otp_secret] = User.generate_otp_secret
|
||||
|
||||
redirect_to new_settings_two_factor_authentication_confirmation_path
|
||||
redirect_to new_settings_two_factor_authentication_confirmation_path(params.permit(:oauth))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -22,7 +22,7 @@ module Settings
|
||||
private
|
||||
|
||||
def require_otp_enabled
|
||||
redirect_to settings_otp_authentication_path unless current_user.otp_enabled?
|
||||
redirect_to settings_otp_authentication_path(params.permit(:oauth)) unless current_user.otp_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,6 +70,10 @@ module JsonLdHelper
|
||||
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
||||
end
|
||||
|
||||
def supported_security_context?(json)
|
||||
!json.nil? && equals_or_includes?(json['@context'], 'https://w3id.org/security/v1')
|
||||
end
|
||||
|
||||
def unsupported_uri_scheme?(uri)
|
||||
uri.nil? || !uri.start_with?('http://', 'https://')
|
||||
end
|
||||
|
||||
@@ -6,7 +6,10 @@ import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
import { unreblog, reblog } from './interactions_typed';
|
||||
import { openModal } from './modal';
|
||||
import { timelineExpandPinnedFromStatus } from './timelines_typed';
|
||||
import {
|
||||
insertPinnedStatusIntoTimelines,
|
||||
removePinnedStatusFromTimelines,
|
||||
} from './timelines_typed';
|
||||
|
||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
@@ -369,7 +372,7 @@ export function pin(status) {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(pinSuccess(status));
|
||||
dispatch(timelineExpandPinnedFromStatus(status));
|
||||
dispatch(insertPinnedStatusIntoTimelines(status));
|
||||
}).catch(error => {
|
||||
dispatch(pinFail(status, error));
|
||||
});
|
||||
@@ -408,7 +411,7 @@ export function unpin (status) {
|
||||
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unpinSuccess(status));
|
||||
dispatch(timelineExpandPinnedFromStatus(status));
|
||||
dispatch(removePinnedStatusFromTimelines(status));
|
||||
}).catch(error => {
|
||||
dispatch(unpinFail(status, error));
|
||||
});
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { Status } from '../models/status';
|
||||
import { createAppThunk } from '../store/typed_functions';
|
||||
|
||||
import {
|
||||
expandAccountFeaturedTimeline,
|
||||
expandTimeline,
|
||||
insertIntoTimeline,
|
||||
TIMELINE_NON_STATUS_MARKERS,
|
||||
} from './timelines';
|
||||
|
||||
@@ -173,9 +173,13 @@ export function parseTimelineKey(key: string): TimelineParams | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isTimelineKeyPinned(key: string) {
|
||||
export function isTimelineKeyPinned(key: string, accountId?: string) {
|
||||
const parsedKey = parseTimelineKey(key);
|
||||
return parsedKey?.type === 'account' && parsedKey.pinned;
|
||||
const isPinned = parsedKey?.type === 'account' && parsedKey.pinned;
|
||||
if (!accountId || !isPinned) {
|
||||
return isPinned;
|
||||
}
|
||||
return parsedKey.userId === accountId;
|
||||
}
|
||||
|
||||
export function isNonStatusId(value: unknown) {
|
||||
@@ -199,52 +203,71 @@ export const timelineDelete = createAction<{
|
||||
reblogOf: string | null;
|
||||
}>('timelines/delete');
|
||||
|
||||
export const timelineExpandPinnedFromStatus = createAppThunk(
|
||||
export const timelineDeleteStatus = createAction<{
|
||||
statusId: string;
|
||||
timelineKey: string;
|
||||
}>('timelines/deleteStatus');
|
||||
|
||||
export const insertPinnedStatusIntoTimelines = createAppThunk(
|
||||
(status: Status, { dispatch, getState }) => {
|
||||
const accountId = status.getIn(['account', 'id']) as string;
|
||||
if (!accountId) {
|
||||
const currentAccountId = getState().meta.get('me', null) as string | null;
|
||||
if (!currentAccountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify that any of the relevant timelines are actually expanded before dispatching, to avoid unnecessary API calls.
|
||||
const tags =
|
||||
(
|
||||
status.get('tags') as
|
||||
| ImmutableList<ImmutableMap<'name', string>> // We only care about the tag name.
|
||||
| undefined
|
||||
)
|
||||
?.map((tag) => tag.get('name') as string)
|
||||
.toArray() ?? [];
|
||||
|
||||
const timelines = getState().timelines as ImmutableMap<string, unknown>;
|
||||
if (!timelines.some((_, key) => key.startsWith(`account:${accountId}:`))) {
|
||||
return;
|
||||
}
|
||||
|
||||
void dispatch(
|
||||
expandTimelineByParams({
|
||||
type: 'account',
|
||||
userId: accountId,
|
||||
pinned: true,
|
||||
}),
|
||||
);
|
||||
void dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
|
||||
// Iterate over tags and clear those too.
|
||||
const tags = status.get('tags') as
|
||||
| ImmutableList<ImmutableMap<'name', string>> // We only care about the tag name.
|
||||
| undefined;
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
tags.forEach((tag) => {
|
||||
const tagName = tag.get('name');
|
||||
if (!tagName) {
|
||||
return;
|
||||
const accountTimelines = timelines.filter((_, key) => {
|
||||
if (!key.startsWith(`account:${currentAccountId}:`)) {
|
||||
return false;
|
||||
}
|
||||
const parsed = parseTimelineKey(key);
|
||||
const isPinned = parsed?.type === 'account' && parsed.pinned;
|
||||
if (!isPinned) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void dispatch(
|
||||
expandTimelineByParams({
|
||||
type: 'account',
|
||||
userId: accountId,
|
||||
pinned: true,
|
||||
tagged: tagName,
|
||||
}),
|
||||
);
|
||||
void dispatch(
|
||||
expandAccountFeaturedTimeline(accountId, { tagged: tagName }),
|
||||
);
|
||||
return !parsed.tagged || tags.includes(parsed.tagged);
|
||||
});
|
||||
|
||||
accountTimelines.forEach((_, key) => {
|
||||
dispatch(insertIntoTimeline(key, status.get('id') as string, 0));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const removePinnedStatusFromTimelines = createAppThunk(
|
||||
(status: Status, { dispatch, getState }) => {
|
||||
const currentAccountId = getState().meta.get('me', null) as string | null;
|
||||
if (!currentAccountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusId = status.get('id') as string;
|
||||
const timelines = getState().timelines as ImmutableMap<
|
||||
string,
|
||||
ImmutableMap<'items' | 'pendingItems', ImmutableList<string>>
|
||||
>;
|
||||
|
||||
timelines.forEach((timeline, key) => {
|
||||
if (!isTimelineKeyPinned(key, currentAccountId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
timeline.get('items')?.includes(statusId) ||
|
||||
timeline.get('pendingItems')?.includes(statusId)
|
||||
) {
|
||||
dispatch(timelineDeleteStatus({ statusId, timelineKey: key }));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ExitAnimationWrapper: React.FC<{
|
||||
* Render prop that provides the nested component with the `delayedIsActive` flag
|
||||
*/
|
||||
children: (delayedIsActive: boolean) => React.ReactNode;
|
||||
}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => {
|
||||
}> = ({ isActive, delayMs = 500, withEntryDelay, children }) => {
|
||||
const [delayedIsActive, setDelayedIsActive] = useState(
|
||||
isActive && !withEntryDelay,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -36,12 +36,26 @@ TextAreaField.displayName = 'TextAreaField';
|
||||
export const TextArea = forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
ComponentPropsWithoutRef<'textarea'>
|
||||
>(({ className, ...otherProps }, ref) => (
|
||||
<textarea
|
||||
{...otherProps}
|
||||
className={classNames(className, classes.input)}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
>(({ className, onKeyDown, ...otherProps }, ref) => {
|
||||
const handleSubmitHotkey = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
onKeyDown?.(e);
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
const targetForm = e.currentTarget.form;
|
||||
targetForm?.requestSubmit();
|
||||
}
|
||||
},
|
||||
[onKeyDown],
|
||||
);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
{...otherProps}
|
||||
onKeyDown={handleSubmitHotkey}
|
||||
className={classNames(className, classes.input)}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
TextArea.displayName = 'TextArea';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useStorage } from '@/mastodon/hooks/useStorage';
|
||||
import { useStorageState } from '@/mastodon/hooks/useStorage';
|
||||
|
||||
interface AccountTimelineContextValue {
|
||||
accountId: string;
|
||||
@@ -26,30 +26,34 @@ export const AccountTimelineProvider: FC<{
|
||||
accountId: string;
|
||||
children: ReactNode;
|
||||
}> = ({ accountId, children }) => {
|
||||
const { getItem, setItem } = useStorage({
|
||||
const storageOptions = {
|
||||
type: 'session',
|
||||
prefix: `filters-${accountId}:`,
|
||||
});
|
||||
const [boosts, setBoosts] = useState(
|
||||
() => (getItem('boosts') === '0' ? false : true), // Default to enabled.
|
||||
} as const;
|
||||
|
||||
const [boosts, setBoosts] = useStorageState<boolean>(
|
||||
'boosts',
|
||||
true,
|
||||
storageOptions,
|
||||
);
|
||||
const [replies, setReplies] = useState(() =>
|
||||
getItem('replies') === '1' ? true : false,
|
||||
|
||||
const [replies, setReplies] = useStorageState<boolean>(
|
||||
'replies',
|
||||
false,
|
||||
storageOptions,
|
||||
);
|
||||
|
||||
const handleSetBoosts = useCallback(
|
||||
(value: boolean) => {
|
||||
setBoosts(value);
|
||||
setItem('boosts', value ? '1' : '0');
|
||||
},
|
||||
[setBoosts, setItem],
|
||||
[setBoosts],
|
||||
);
|
||||
const handleSetReplies = useCallback(
|
||||
(value: boolean) => {
|
||||
setReplies(value);
|
||||
setItem('replies', value ? '1' : '0');
|
||||
},
|
||||
[setReplies, setItem],
|
||||
[setReplies],
|
||||
);
|
||||
|
||||
const [showAllPinned, setShowAllPinned] = useState(false);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { LimitedAccountHint } from '@/mastodon/features/account_timeline/components/limited_account_hint';
|
||||
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
|
||||
import { RemoteHint } from './remote';
|
||||
|
||||
interface BaseEmptyMessageProps {
|
||||
account?: Account;
|
||||
defaultMessage: ReactNode;
|
||||
}
|
||||
export type EmptyMessageProps = Omit<BaseEmptyMessageProps, 'defaultMessage'>;
|
||||
|
||||
export const BaseEmptyMessage: FC<BaseEmptyMessageProps> = ({
|
||||
account,
|
||||
defaultMessage,
|
||||
}) => {
|
||||
const { blockedBy, hidden, suspended } = useAccountVisibility(account?.id);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (suspended) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_suspended'
|
||||
defaultMessage='Account suspended'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return <LimitedAccountHint accountId={account.id} />;
|
||||
}
|
||||
|
||||
if (blockedBy) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_unavailable'
|
||||
defaultMessage='Profile unavailable'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (account.hide_collections) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='empty_column.account_hides_collections'
|
||||
defaultMessage='This user has chosen to not make this information available'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const domain = account.acct.split('@')[1];
|
||||
if (domain) {
|
||||
return <RemoteHint domain={domain} url={account.url} />;
|
||||
}
|
||||
|
||||
return defaultMessage;
|
||||
};
|
||||
104
app/javascript/mastodon/features/followers/components/list.tsx
Normal file
104
app/javascript/mastodon/features/followers/components/list.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
import { Account } from '@/mastodon/components/account';
|
||||
import { Column } from '@/mastodon/components/column';
|
||||
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import ScrollableList from '@/mastodon/components/scrollable_list';
|
||||
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
|
||||
import { useLayout } from '@/mastodon/hooks/useLayout';
|
||||
|
||||
import { AccountHeader } from '../../account_timeline/components/account_header';
|
||||
|
||||
import { RemoteHint } from './remote';
|
||||
|
||||
export interface AccountList {
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
interface AccountListProps {
|
||||
accountId?: string | null;
|
||||
append?: ReactNode;
|
||||
emptyMessage: ReactNode;
|
||||
footer?: ReactNode;
|
||||
list?: AccountList | null;
|
||||
loadMore: () => void;
|
||||
prependAccountId?: string | null;
|
||||
scrollKey: string;
|
||||
}
|
||||
|
||||
export const AccountList: FC<AccountListProps> = ({
|
||||
accountId,
|
||||
append,
|
||||
emptyMessage,
|
||||
footer,
|
||||
list,
|
||||
loadMore,
|
||||
prependAccountId,
|
||||
scrollKey,
|
||||
}) => {
|
||||
const account = useAccount(accountId);
|
||||
|
||||
const { blockedBy, hidden, suspended } = useAccountVisibility(accountId);
|
||||
const forceEmptyState = blockedBy || hidden || suspended;
|
||||
|
||||
const children = useMemo(() => {
|
||||
if (forceEmptyState) {
|
||||
return [];
|
||||
}
|
||||
const children =
|
||||
list?.items.map((followerId) => (
|
||||
<Account key={followerId} id={followerId} />
|
||||
)) ?? [];
|
||||
|
||||
if (prependAccountId) {
|
||||
children.unshift(
|
||||
<Account key={prependAccountId} id={prependAccountId} minimal />,
|
||||
);
|
||||
}
|
||||
return children;
|
||||
}, [prependAccountId, list, forceEmptyState]);
|
||||
|
||||
const { multiColumn } = useLayout();
|
||||
|
||||
// Null means accountId does not exist (e.g. invalid acct). Undefined means loading.
|
||||
if (accountId === null) {
|
||||
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||
}
|
||||
|
||||
if (!accountId || !account) {
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const domain = account.acct.split('@')[1];
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey={scrollKey}
|
||||
hasMore={!forceEmptyState && list?.hasMore}
|
||||
isLoading={list?.isLoading ?? true}
|
||||
onLoadMore={loadMore}
|
||||
prepend={<AccountHeader accountId={accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
append={append ?? <RemoteHint domain={domain} url={account.url} />}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
footer={footer}
|
||||
>
|
||||
{children}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { TimelineHint } from '@/mastodon/components/timeline_hint';
|
||||
|
||||
export const RemoteHint: FC<{ domain?: string; url: string }> = ({
|
||||
domain,
|
||||
url,
|
||||
}) => {
|
||||
if (!domain) {
|
||||
return null;
|
||||
}
|
||||
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> }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,187 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import {
|
||||
lookupAccount,
|
||||
fetchAccount,
|
||||
fetchFollowers,
|
||||
expandFollowers,
|
||||
} from '../../actions/accounts';
|
||||
import { ColumnBackButton } from '../../components/column_back_button';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
const accountId = id || state.accounts_map[normalizeForLookup(acct)];
|
||||
|
||||
if (!accountId) {
|
||||
return {
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
|
||||
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
};
|
||||
|
||||
const RemoteHint = ({ accountId, url }) => {
|
||||
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 = {
|
||||
url: PropTypes.string.isRequired,
|
||||
accountId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
class Followers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.shape({
|
||||
acct: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
accountId: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
remote: PropTypes.bool,
|
||||
remoteUrl: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
_load () {
|
||||
const { accountId, isAccount, dispatch } = this.props;
|
||||
|
||||
if (!isAccount) dispatch(fetchAccount(accountId));
|
||||
dispatch(fetchFollowers(accountId));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (accountId) {
|
||||
this._load();
|
||||
} else {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (prevProps.accountId !== accountId && accountId) {
|
||||
this._load();
|
||||
} else if (prevProps.params.acct !== acct) {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowers(this.props.accountId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
let emptyMessage;
|
||||
|
||||
const forceEmptyState = blockedBy || suspended || hidden;
|
||||
|
||||
if (suspended) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||
} else if (hidden) {
|
||||
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||
} else if (hideCollections && accountIds.isEmpty()) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
|
||||
} else if (remote && accountIds.isEmpty()) {
|
||||
emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
|
||||
} else {
|
||||
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
|
||||
}
|
||||
|
||||
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='followers'
|
||||
hasMore={!forceEmptyState && hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{forceEmptyState ? [] : accountIds.map(id =>
|
||||
<Account key={id} id={id} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Followers);
|
||||
90
app/javascript/mastodon/features/followers/index.tsx
Normal file
90
app/javascript/mastodon/features/followers/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { expandFollowers, fetchFollowers } from '@/mastodon/actions/accounts';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||
import { selectUserListWithoutMe } from '@/mastodon/selectors/user_lists';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import type { EmptyMessageProps } from './components/empty';
|
||||
import { BaseEmptyMessage } from './components/empty';
|
||||
import { AccountList } from './components/list';
|
||||
|
||||
const Followers: FC = () => {
|
||||
const accountId = useAccountId();
|
||||
const account = useAccount(accountId);
|
||||
const currentAccountId = useAppSelector(
|
||||
(state) => (state.meta.get('me') as string | null) ?? null,
|
||||
);
|
||||
const followerList = useAppSelector((state) =>
|
||||
selectUserListWithoutMe(state, 'followers', accountId),
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
if (!followerList && accountId) {
|
||||
dispatch(fetchFollowers(accountId));
|
||||
}
|
||||
}, [accountId, dispatch, followerList]);
|
||||
|
||||
const loadMore = useDebouncedCallback(
|
||||
() => {
|
||||
if (accountId) {
|
||||
dispatch(expandFollowers(accountId));
|
||||
}
|
||||
},
|
||||
300,
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
const relationship = useRelationship(accountId);
|
||||
|
||||
const followerId = relationship?.following ? currentAccountId : null;
|
||||
const followersExceptMeHidden = !!(
|
||||
account?.hide_collections &&
|
||||
followerList?.items.length === 0 &&
|
||||
followerId
|
||||
);
|
||||
|
||||
const footer = followersExceptMeHidden && (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='followers.hide_other_followers'
|
||||
defaultMessage='This user has chosen to not make their other followers visible'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<AccountList
|
||||
accountId={accountId}
|
||||
footer={footer}
|
||||
emptyMessage={<EmptyMessage account={account} />}
|
||||
list={followerList}
|
||||
loadMore={loadMore}
|
||||
prependAccountId={followerId}
|
||||
scrollKey='followers'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyMessage: FC<EmptyMessageProps> = (props) => (
|
||||
<BaseEmptyMessage
|
||||
{...props}
|
||||
defaultMessage={
|
||||
<FormattedMessage
|
||||
id='account.followers.empty'
|
||||
defaultMessage='No one follows this user yet.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- Used by async components.
|
||||
export default Followers;
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { TimelineHint } from '@/mastodon/components/timeline_hint';
|
||||
|
||||
export const RemoteHint: FC<{ domain?: string; url: string }> = ({
|
||||
domain,
|
||||
url,
|
||||
}) => {
|
||||
if (!domain) {
|
||||
return null;
|
||||
}
|
||||
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> }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,187 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import {
|
||||
lookupAccount,
|
||||
fetchAccount,
|
||||
fetchFollowing,
|
||||
expandFollowing,
|
||||
} from '../../actions/accounts';
|
||||
import { ColumnBackButton } from '../../components/column_back_button';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
const accountId = id || state.accounts_map[normalizeForLookup(acct)];
|
||||
|
||||
if (!accountId) {
|
||||
return {
|
||||
isLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
|
||||
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
};
|
||||
|
||||
const RemoteHint = ({ accountId, url }) => {
|
||||
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 = {
|
||||
url: PropTypes.string.isRequired,
|
||||
accountId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
class Following extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.shape({
|
||||
acct: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
accountId: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
remote: PropTypes.bool,
|
||||
remoteUrl: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
_load () {
|
||||
const { accountId, isAccount, dispatch } = this.props;
|
||||
|
||||
if (!isAccount) dispatch(fetchAccount(accountId));
|
||||
dispatch(fetchFollowing(accountId));
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (accountId) {
|
||||
this._load();
|
||||
} else {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (prevProps.accountId !== accountId && accountId) {
|
||||
this._load();
|
||||
} else if (prevProps.params.acct !== acct) {
|
||||
dispatch(lookupAccount(acct));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowing(this.props.accountId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
let emptyMessage;
|
||||
|
||||
const forceEmptyState = blockedBy || suspended || hidden;
|
||||
|
||||
if (suspended) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||
} else if (hidden) {
|
||||
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||
} else if (hideCollections && accountIds.isEmpty()) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
|
||||
} else if (remote && accountIds.isEmpty()) {
|
||||
emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
|
||||
} else {
|
||||
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
|
||||
}
|
||||
|
||||
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='following'
|
||||
hasMore={!forceEmptyState && hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{forceEmptyState ? [] : accountIds.map(id =>
|
||||
<Account key={id} id={id} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Following);
|
||||
94
app/javascript/mastodon/features/following/index.tsx
Normal file
94
app/javascript/mastodon/features/following/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { expandFollowing, fetchFollowing } from '@/mastodon/actions/accounts';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||
import { selectUserListWithoutMe } from '@/mastodon/selectors/user_lists';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import type { EmptyMessageProps } from '../followers/components/empty';
|
||||
import { BaseEmptyMessage } from '../followers/components/empty';
|
||||
import { AccountList } from '../followers/components/list';
|
||||
|
||||
import { RemoteHint } from './components/remote';
|
||||
|
||||
const Followers: FC = () => {
|
||||
const accountId = useAccountId();
|
||||
const account = useAccount(accountId);
|
||||
const currentAccountId = useAppSelector(
|
||||
(state) => (state.meta.get('me') as string | null) ?? null,
|
||||
);
|
||||
const followingList = useAppSelector((state) =>
|
||||
selectUserListWithoutMe(state, 'following', accountId),
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
if (!followingList && accountId) {
|
||||
dispatch(fetchFollowing(accountId));
|
||||
}
|
||||
}, [accountId, dispatch, followingList]);
|
||||
|
||||
const loadMore = useDebouncedCallback(
|
||||
() => {
|
||||
if (accountId) {
|
||||
dispatch(expandFollowing(accountId));
|
||||
}
|
||||
},
|
||||
300,
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
const relationship = useRelationship(accountId);
|
||||
|
||||
const followedId = relationship?.followed_by ? currentAccountId : null;
|
||||
const followingExceptMeHidden = !!(
|
||||
account?.hide_collections &&
|
||||
followingList?.items.length === 0 &&
|
||||
followedId
|
||||
);
|
||||
|
||||
const footer = followingExceptMeHidden && (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='following.hide_other_following'
|
||||
defaultMessage='This user has chosen to not make the rest of who they follow visible'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const domain = account?.acct.split('@')[1];
|
||||
return (
|
||||
<AccountList
|
||||
accountId={accountId}
|
||||
append={domain && <RemoteHint domain={domain} url={account.url} />}
|
||||
emptyMessage={<EmptyMessage account={account} />}
|
||||
footer={footer}
|
||||
list={followingList}
|
||||
loadMore={loadMore}
|
||||
prependAccountId={currentAccountId}
|
||||
scrollKey='following'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyMessage: FC<EmptyMessageProps> = (props) => (
|
||||
<BaseEmptyMessage
|
||||
{...props}
|
||||
defaultMessage={
|
||||
<FormattedMessage
|
||||
id='account.follows.empty'
|
||||
defaultMessage="This user doesn't follow anyone yet."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- Used by async components.
|
||||
export default Followers;
|
||||
@@ -8,6 +8,7 @@ export const useLayout = () => {
|
||||
|
||||
return {
|
||||
singleColumn: layout === 'single-column' || layout === 'mobile',
|
||||
multiColumn: layout === 'multi-column',
|
||||
layout,
|
||||
};
|
||||
};
|
||||
|
||||
19
app/javascript/mastodon/hooks/useRelationship.ts
Normal file
19
app/javascript/mastodon/hooks/useRelationship.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { fetchRelationships } from '../actions/accounts';
|
||||
import { useAppDispatch, useAppSelector } from '../store';
|
||||
|
||||
export function useRelationship(accountId?: string | null) {
|
||||
const relationship = useAppSelector((state) =>
|
||||
accountId ? state.relationships.get(accountId) : null,
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
if (accountId && !relationship) {
|
||||
dispatch(fetchRelationships([accountId]));
|
||||
}
|
||||
}, [accountId, dispatch, relationship]);
|
||||
|
||||
return relationship;
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
interface StorageOptions {
|
||||
type?: 'local' | 'session';
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
export function useStorage({
|
||||
type = 'local',
|
||||
prefix = '',
|
||||
}: { type?: 'local' | 'session'; prefix?: string } = {}) {
|
||||
}: StorageOptions = {}) {
|
||||
const storageType = type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||
const isAvailable = useMemo(
|
||||
() => storageAvailable(storageType),
|
||||
@@ -23,6 +28,7 @@ export function useStorage({
|
||||
},
|
||||
[isAvailable, storageType, prefix],
|
||||
);
|
||||
|
||||
const setItem = useCallback(
|
||||
(key: string, value: string) => {
|
||||
if (!isAvailable) {
|
||||
@@ -35,13 +41,52 @@ export function useStorage({
|
||||
[isAvailable, storageType, prefix],
|
||||
);
|
||||
|
||||
const removeItem = useCallback(
|
||||
(key: string) => {
|
||||
if (!isAvailable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window[storageType].removeItem(prefix ? `${prefix};${key}` : key);
|
||||
} catch {}
|
||||
},
|
||||
[isAvailable, storageType, prefix],
|
||||
);
|
||||
|
||||
return {
|
||||
isAvailable,
|
||||
getItem,
|
||||
setItem,
|
||||
removeItem,
|
||||
};
|
||||
}
|
||||
|
||||
export function useStorageState<T extends string | boolean>(
|
||||
key: string,
|
||||
initialState: T,
|
||||
options?: StorageOptions,
|
||||
) {
|
||||
const { getItem, setItem, removeItem } = useStorage(options);
|
||||
const [state, setState] = useState<T>(
|
||||
() => (retrieveBooleanOrString(getItem(key)) as T | null) ?? initialState,
|
||||
);
|
||||
|
||||
const handleSetState = useCallback(
|
||||
(newValue: T) => {
|
||||
setItem(key, castToString(newValue));
|
||||
setState(newValue);
|
||||
},
|
||||
[key, setItem],
|
||||
);
|
||||
|
||||
const removeState = useCallback(() => {
|
||||
removeItem(key);
|
||||
setState(initialState);
|
||||
}, [initialState, key, removeItem]);
|
||||
|
||||
return [state, handleSetState, removeState] as const;
|
||||
}
|
||||
|
||||
// Tests the storage availability for the given type. Taken from MDN:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
|
||||
export function storageAvailable(type: 'localStorage' | 'sessionStorage') {
|
||||
@@ -62,3 +107,21 @@ export function storageAvailable(type: 'localStorage' | 'sessionStorage') {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function castToString(value: string | boolean) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '1' : '0';
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function retrieveBooleanOrString(value: string | null) {
|
||||
if (value === '1') {
|
||||
return true;
|
||||
} else if (value === '0') {
|
||||
return false;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"about.powered_by": "شبكة اجتماعية لامركزية مدعومة من {mastodon}",
|
||||
"about.rules": "قواعد الخادم",
|
||||
"account.account_note_header": "ملاحظة شخصية",
|
||||
"account.activity": "النشاط",
|
||||
"account.add_note": "إضافة ملاحظة شخصية",
|
||||
"account.add_or_remove_from_list": "الإضافة أو الإزالة من القائمة",
|
||||
"account.badges.admin": "مدير",
|
||||
"account.badges.bot": "آلي",
|
||||
"account.badges.group": "فريق",
|
||||
"account.block": "احجب @{name}",
|
||||
@@ -38,8 +41,18 @@
|
||||
"account.featured.hashtags": "هاشتاقات",
|
||||
"account.featured_tags.last_status_at": "آخر منشور في {date}",
|
||||
"account.featured_tags.last_status_never": "لا توجد رسائل",
|
||||
"account.fields.scroll_next": "أظْهِر التَّالي",
|
||||
"account.fields.scroll_prev": "أظْهِر السَّابِق",
|
||||
"account.filters.all": "جميع الأنشطة",
|
||||
"account.filters.posts_only": "منشورات",
|
||||
"account.filters.posts_replies": "المنشورات والردود",
|
||||
"account.filters.replies_toggle": "اعرض الردود",
|
||||
"account.follow": "متابعة",
|
||||
"account.follow_back": "تابعه بالمثل",
|
||||
"account.follow_back_short": "تابعه بالمثل",
|
||||
"account.follow_request": "طلب المتابعة",
|
||||
"account.follow_request_cancel": "إلغاء الطلب",
|
||||
"account.follow_request_cancel_short": "إلغاء",
|
||||
"account.followers": "مُتابِعون",
|
||||
"account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.",
|
||||
"account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}",
|
||||
@@ -57,6 +70,7 @@
|
||||
"account.locked_info": "تم ضبط حالة خصوصية هذا الحساب على أنه مؤمّن. إذ يراجع صاحبه يدويًا من يُسمح له بالمتابعة.",
|
||||
"account.media": "وسائط",
|
||||
"account.mention": "أذكُر @{name}",
|
||||
"account.menu.copy": "نسخ الرابط",
|
||||
"account.moved_to": "أشار {name} إلى أن حسابه الجديد الآن:",
|
||||
"account.mute": "أكتم @{name}",
|
||||
"account.mute_notifications_short": "كتم الإشعارات",
|
||||
@@ -65,6 +79,11 @@
|
||||
"account.muting": "مكتوم",
|
||||
"account.mutual": "أنتم تتابعون بعضكم البعض",
|
||||
"account.no_bio": "لم يتم تقديم وصف.",
|
||||
"account.node_modal.field_label": "ملاحظة شخصية",
|
||||
"account.node_modal.save": "حفظ",
|
||||
"account.node_modal.title": "إضافة ملاحظة شخصية",
|
||||
"account.note.edit_button": "تعديل",
|
||||
"account.note.title": "ملاحظة شخصية (مرئية لك فقط)",
|
||||
"account.open_original_page": "افتح الصفحة الأصلية",
|
||||
"account.posts": "منشورات",
|
||||
"account.posts_with_replies": "المنشورات والرُدود",
|
||||
@@ -107,11 +126,16 @@
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "قم بوصفها للأشخاص ذوي الإعاقة البصرية…",
|
||||
"alt_text_modal.done": "تمّ",
|
||||
"announcement.announcement": "إعلان",
|
||||
"annual_report.summary.archetype.replier.name": "الفراشة",
|
||||
"annual_report.summary.close": "اغلق",
|
||||
"annual_report.summary.copy_link": "نسخ الرابط",
|
||||
"annual_report.summary.most_used_app.most_used_app": "التطبيق الأكثر استخداماً",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "الهاشتاق الأكثر استخداماً",
|
||||
"annual_report.summary.new_posts.new_posts": "المنشورات الجديدة",
|
||||
"annual_report.summary.percentile.text": "<topLabel>هذا يجعلك من بين أكثر </topLabel><percentage></percentage><bottomLabel>مستخدمي {domain} نشاطاً </bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "سيبقى هذا الأمر بيننا.",
|
||||
"annual_report.summary.share_elsewhere": "شاركها في مكان آخر",
|
||||
"annual_report.summary.share_on_mastodon": "شاركها على ماستدون",
|
||||
"attachments_list.unprocessed": "(غير معالَج)",
|
||||
"audio.hide": "إخفاء المقطع الصوتي",
|
||||
"block_modal.remote_users_caveat": "سوف نطلب من الخادم {domain} أن يحترم قرارك، لكن الالتزام غير مضمون لأن بعض الخواديم قد تتعامل مع نصوص الكتل بشكل مختلف. قد تظل المنشورات العامة مرئية للمستخدمين غير المسجلين الدخول.",
|
||||
@@ -137,11 +161,21 @@
|
||||
"bundle_modal_error.close": "إغلاق",
|
||||
"bundle_modal_error.message": "حدث خطأ أثناء تحميل هذه الشاشة.",
|
||||
"bundle_modal_error.retry": "إعادة المُحاولة",
|
||||
"callout.dismiss": "تجاهل",
|
||||
"closed_registrations.other_server_instructions": "بما أن ماستدون لامركزي، يمكنك إنشاء حساب على خادم آخر للاستمرار في التفاعل مع هذا الخادم.",
|
||||
"closed_registrations_modal.description": "لا يمكن إنشاء حساب على {domain} حاليا، ولكن على فكرة لست بحاجة إلى حساب على {domain} بذاته لاستخدام ماستدون.",
|
||||
"closed_registrations_modal.find_another_server": "ابحث على خادم آخر",
|
||||
"closed_registrations_modal.preamble": "ماستدون لامركزي، لذلك بغض النظر عن مكان إنشاء حسابك، سيكون بإمكانك المتابعة والتفاعل مع أي شخص على هذا الخادم. يمكنك حتى أن تستضيفه ذاتياً!",
|
||||
"closed_registrations_modal.title": "إنشاء حساب على ماستدون",
|
||||
"collections.collection_description": "الوصف",
|
||||
"collections.collection_name": "الاسم",
|
||||
"collections.collection_topic": "الموضوع",
|
||||
"collections.content_warning": "تحذير عن المحتوى",
|
||||
"collections.continue": "مواصلة",
|
||||
"collections.create.settings_title": "الإعدادات",
|
||||
"collections.create.steps": "الخطوة {step}/{total}",
|
||||
"collections.edit_settings": "تعديل الإعدادات",
|
||||
"collections.manage_accounts": "إدارة الحسابات",
|
||||
"column.about": "عن",
|
||||
"column.blocks": "المُستَخدِمون المَحظورون",
|
||||
"column.bookmarks": "الفواصل المرجعية",
|
||||
@@ -201,6 +235,7 @@
|
||||
"confirmations.delete.confirm": "حذف",
|
||||
"confirmations.delete.message": "هل أنتَ مُتأكدٌ أنك تُريدُ حَذفَ هذا المنشور؟",
|
||||
"confirmations.delete.title": "أتريد حذف المنشور؟",
|
||||
"confirmations.delete_collection.confirm": "حذف",
|
||||
"confirmations.delete_list.confirm": "حذف",
|
||||
"confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمة بشكلٍ دائم؟",
|
||||
"confirmations.delete_list.title": "أتريد حذف القائمة؟",
|
||||
@@ -375,6 +410,9 @@
|
||||
"follow_suggestions.who_to_follow": "حسابات للمُتابَعة",
|
||||
"followed_tags": "الوسوم المتابَعة",
|
||||
"footer.about": "عن",
|
||||
"footer.about_mastodon": "عن ماستدون",
|
||||
"footer.about_server": "عن {domain}",
|
||||
"footer.about_this_server": "عن",
|
||||
"footer.directory": "دليل الصفحات التعريفية",
|
||||
"footer.get_app": "احصل على التطبيق",
|
||||
"footer.keyboard_shortcuts": "اختصارات لوحة المفاتيح",
|
||||
@@ -382,6 +420,7 @@
|
||||
"footer.source_code": "الاطلاع على الشفرة المصدرية",
|
||||
"footer.status": "الحالة",
|
||||
"footer.terms_of_service": "شروط الخدمة",
|
||||
"form_field.optional": "(اختياري)",
|
||||
"generic.saved": "تم الحفظ",
|
||||
"getting_started.heading": "استعدّ للبدء",
|
||||
"hashtag.admin_moderation": "افتح الواجهة الإشراف لـ #{name}",
|
||||
@@ -448,6 +487,7 @@
|
||||
"keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
|
||||
"keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
|
||||
"keyboard_shortcuts.description": "الوصف",
|
||||
"keyboard_shortcuts.direct": "لفتح عمود الإشارات الخاصة",
|
||||
"keyboard_shortcuts.down": "للانتقال إلى أسفل القائمة",
|
||||
"keyboard_shortcuts.enter": "لفتح المنشور",
|
||||
"keyboard_shortcuts.favourite": "لإضافة المنشور إلى المفضلة",
|
||||
@@ -457,6 +497,7 @@
|
||||
"keyboard_shortcuts.home": "لفتح الخيط الرئيسي",
|
||||
"keyboard_shortcuts.hotkey": "مفتاح الاختصار",
|
||||
"keyboard_shortcuts.legend": "لعرض هذا المفتاح",
|
||||
"keyboard_shortcuts.load_more": "للتركيز على زر \"تحميل المزيد\"",
|
||||
"keyboard_shortcuts.local": "لفتح الخيط العام المحلي",
|
||||
"keyboard_shortcuts.mention": "لذِكر الناشر",
|
||||
"keyboard_shortcuts.muted": "لفتح قائمة المستخدِمين المكتومين",
|
||||
@@ -873,6 +914,7 @@
|
||||
"status.quote_error.revoked": "تمت إزالة المنشور من قبل صاحبه",
|
||||
"status.quote_followers_only": "يمكن فقط للمتابعين اقتباس هذا المنشور",
|
||||
"status.quote_manual_review": "الكاتب سوف يراجع يدوياً",
|
||||
"status.quote_noun": "اقتباس",
|
||||
"status.quote_policy_change": "تغيير من يمكنه الاقتباس",
|
||||
"status.quote_post_author": "اقتبس @{name} منشورا",
|
||||
"status.quote_private": "المنشورات الخاصة لا يمكن اقتباسها",
|
||||
@@ -913,6 +955,7 @@
|
||||
"tabs_bar.notifications": "الإشعارات",
|
||||
"tabs_bar.publish": "منشور جديد",
|
||||
"tabs_bar.search": "ابحث",
|
||||
"tag.remove": "إزالة",
|
||||
"terms_of_service.effective_as_of": "مطبق اعتباراً من {date}",
|
||||
"terms_of_service.title": "شروط الخدمة",
|
||||
"terms_of_service.upcoming_changes_on": "تغييرات قادمة في تاريخ {date}",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Схаваць пашырэнні ў стужцы",
|
||||
"account.menu.mention": "Згадаць",
|
||||
"account.menu.mute": "Ігнараваць уліковы запіс",
|
||||
"account.menu.note.description": "Бачна толькі Вам",
|
||||
"account.menu.open_original_page": "Паказаць на {domain}",
|
||||
"account.menu.remove_follower": "Выдаліць падпісчыка",
|
||||
"account.menu.report": "Паскардзіцца на профіль",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Ігнаруецца",
|
||||
"account.muting": "Ігнараванне",
|
||||
"account.mutual": "Вы падпісаныя адно на аднаго",
|
||||
"account.name.help.domain": "{domain} — сервер, які ўтрымлівае профіль і допісы гэтага карыстальніка.",
|
||||
"account.name.help.domain_self": "{domain} — Ваш сервер, які ўтрымлівае Ваш профіль і допісы.",
|
||||
"account.name.help.footer": "Гэтак жа, як Вы можаце адпраўляць электронныя лісты людзям праз розныя кліенты электроннай пошты, Вы таксама можаце ўзаемадзейнічаць з людзьмі з іншых сервераў Mastodon, а таксама з усімі, хто карыстаецца іншымі сацыяльнымі сеткамі, якія працуюць па тых жа правілах, што і Mastodon (пратакол ActivityPub).",
|
||||
"account.name.help.header": "Ідэнтыфікатар карыстальніка падобны да адраса электроннай пошты",
|
||||
"account.name.help.username": "{username} — імя карыстальніка гэтага ўліковага запісу на гэтым серверы. У некага на іншым серверы яно можа быць такім жа.",
|
||||
"account.name.help.username_self": "{username} — Вашае імя карыстальніка на гэтым серверы. У некага на іншым серверы яно можа быць такім жа.",
|
||||
"account.name_info": "Што гэта азначае?",
|
||||
"account.no_bio": "Апісанне адсутнічае.",
|
||||
"account.node_modal.callout": "Асабістыя нататкі бачныя толькі Вам.",
|
||||
"account.node_modal.edit_title": "Рэдагаваць асабістую нататку",
|
||||
@@ -312,7 +320,7 @@
|
||||
"compose.published.open": "Адкрыць",
|
||||
"compose.saved.body": "Допіс захаваны.",
|
||||
"compose_form.direct_message_warning_learn_more": "Даведацца больш",
|
||||
"compose_form.encryption_warning": "Допісы ў Mastodon не абаронены скразным шыфраваннем. Не дзяліцеся ніякай канфідэнцыяльнай інфармацыяй праз Mastodon.",
|
||||
"compose_form.encryption_warning": "Допісы ў Mastodon не абароненыя скразным шыфраваннем. Не дзяліцеся ніякай канфідэнцыяльнай інфармацыяй праз Mastodon.",
|
||||
"compose_form.hashtag_warning": "Гэты допіс не будзе паказаны пад аніякім хэштэгам, бо ён не публічны. Толькі публічныя допісы можна знайсці па хэштэгу.",
|
||||
"compose_form.lock_disclaimer": "Ваш уліковы запіс не {locked}. Усе могуць падпісацца на вас, каб бачыць допісы толькі для падпісчыкаў.",
|
||||
"compose_form.lock_disclaimer.lock": "закрыты",
|
||||
@@ -526,7 +534,7 @@
|
||||
"follow_suggestions.similar_to_recently_followed_longer": "Падобныя профілі, за якімі Вы нядаўна сачылі",
|
||||
"follow_suggestions.view_all": "Праглядзець усё",
|
||||
"follow_suggestions.who_to_follow": "На каго падпісацца",
|
||||
"followed_tags": "Падпіскі",
|
||||
"followed_tags": "Падпіскі на хэштэгі",
|
||||
"footer.about": "Пра нас",
|
||||
"footer.about_mastodon": "Пра Mastodon",
|
||||
"footer.about_server": "Пра {domain}",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Skjul fremhævelser på tidslinje",
|
||||
"account.menu.mention": "Omtal",
|
||||
"account.menu.mute": "Skjul konto",
|
||||
"account.menu.note.description": "Kun synlig for dig",
|
||||
"account.menu.open_original_page": "Vis på {domain}",
|
||||
"account.menu.remove_follower": "Fjern følger",
|
||||
"account.menu.report": "Anmeld konto",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Skjult",
|
||||
"account.muting": "Skjuler",
|
||||
"account.mutual": "I følger hinanden",
|
||||
"account.name.help.domain": "{domain} er den server, der er vært for brugerens profil og indlæg.",
|
||||
"account.name.help.domain_self": "{domain} er din server, der er vært for din profil og indlæg.",
|
||||
"account.name.help.footer": "Ligesom du kan sende e-mails til personer, der bruger forskellige e-mail-klienter, kan du interagere med personer på andre Mastodon-servere – og med alle på andre sociale apps, der bruger de samme regler som Mastodon (ActivityPub-protokollen).",
|
||||
"account.name.help.header": "Et handle svarer til en e-mailadresse",
|
||||
"account.name.help.username": "{username} er denne kontos brugernavn på deres server. En konto på en anden server kan have det samme brugernavn.",
|
||||
"account.name.help.username_self": "{username} er dit brugernavn på denne server. En konto på en anden server kan have det samme brugernavn.",
|
||||
"account.name_info": "Hvad betyder dette?",
|
||||
"account.no_bio": "Ingen beskrivelse til rådighed.",
|
||||
"account.node_modal.callout": "Personlige noter er kun synlige for dig.",
|
||||
"account.node_modal.edit_title": "Rediger personlig note",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Geteilte Beiträge in der Timeline ausblenden",
|
||||
"account.menu.mention": "Erwähnen",
|
||||
"account.menu.mute": "Konto stummschalten",
|
||||
"account.menu.note.description": "Nur für dich sichtbar",
|
||||
"account.menu.open_original_page": "Auf {domain} ansehen",
|
||||
"account.menu.remove_follower": "Follower entfernen",
|
||||
"account.menu.report": "Konto melden",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Stummgeschaltet",
|
||||
"account.muting": "Stummgeschaltet",
|
||||
"account.mutual": "Ihr folgt einander",
|
||||
"account.name.help.domain": "{domain} ist der Server, auf dem das Profil registriert ist und die Beiträge verwaltet werden.",
|
||||
"account.name.help.domain_self": "{domain} ist der Server, auf dem du registriert bist und deine Beiträge verwaltet werden.",
|
||||
"account.name.help.footer": "So wie du E-Mails an andere trotz unterschiedlicher E-Mail-Clients senden kannst, so kannst du auch mit anderen Profilen auf unterschiedlichen Mastodon-Servern interagieren. Wenn andere soziale Apps die gleichen Kommunikationsregeln (das ActivityPub-Protokoll) wie Mastodon verwenden, dann funktioniert die Kommunikation auch dort.",
|
||||
"account.name.help.header": "Deine Adresse im Fediverse ist wie eine E-Mail-Adresse",
|
||||
"account.name.help.username": "{username} ist der Profilname auf deren Server. Es ist möglich, dass jemand auf einem anderen Server den gleichen Profilnamen hat.",
|
||||
"account.name.help.username_self": "{username} ist dein Profilname auf diesem Server. Es ist möglich, dass jemand auf einem anderen Server den gleichen Profilnamen hat.",
|
||||
"account.name_info": "Was bedeutet das?",
|
||||
"account.no_bio": "Keine Beschreibung verfügbar.",
|
||||
"account.node_modal.callout": "Persönliche Notizen sind nur für dich sichtbar.",
|
||||
"account.node_modal.edit_title": "Persönliche Notiz bearbeiten",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Απόκρυψη ενισχύσεων στο χρονολόγιο",
|
||||
"account.menu.mention": "Επισήμανση",
|
||||
"account.menu.mute": "Σίγαση λογαριασμού",
|
||||
"account.menu.note.description": "Ορατή μόνο σε εσάς",
|
||||
"account.menu.open_original_page": "Προβολή στο {domain}",
|
||||
"account.menu.remove_follower": "Αφαίρεση ακολούθου",
|
||||
"account.menu.report": "Αναφορά λογαριασμού",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Σε σίγαση",
|
||||
"account.muting": "Σίγαση",
|
||||
"account.mutual": "Ακολουθείτε ο ένας τον άλλο",
|
||||
"account.name.help.domain": "Το {domain} είναι ο διακομιστής που φιλοξενεί το προφίλ και τις αναρτήσεις του χρήστη.",
|
||||
"account.name.help.domain_self": "Το {domain} είναι ο διακομιστής σας που φιλοξενεί το προφίλ και τις αναρτήσεις σας.",
|
||||
"account.name.help.footer": "Ακριβώς όπως μπορείτε να στείλετε μηνύματα ηλεκτρονικού ταχυδρομείου σε άτομα χρησιμοποιώντας διαφορετικούς παρόχους email, μπορείτε να αλληλεπιδράσετε με άτομα σε άλλους διακομιστές Mastodon – και με οποιονδήποτε σε άλλες κοινωνικές εφαρμογές που τροφοδοτούνται από το ίδιο σύνολο κανόνων όπως χρησιμοποιεί το Mastodon (το πρωτόκολλο ActivityPub).",
|
||||
"account.name.help.header": "Ένα πλήρες όνομα χρήστη είναι σαν μια διεύθυνση email",
|
||||
"account.name.help.username": "Το {username} είναι το όνομα χρήστη αυτού του λογαριασμού στο διακομιστή τους. Κάποιος σε άλλο διακομιστή μπορεί να έχει το ίδιο όνομα χρήστη.",
|
||||
"account.name.help.username_self": "Το {username} είναι το όνομα χρήστη σας σε αυτόν το διακομιστή. Κάποιος σε άλλο διακομιστή μπορεί να έχει το ίδιο όνομα χρήστη.",
|
||||
"account.name_info": "Τι σημαίνει αυτό;",
|
||||
"account.no_bio": "Δεν υπάρχει περιγραφή.",
|
||||
"account.node_modal.callout": "Οι προσωπικές σημειώσεις είναι ορατές μόνο σε εσάς.",
|
||||
"account.node_modal.edit_title": "Επεξεργασία προσωπικής σημείωσης",
|
||||
@@ -421,7 +429,7 @@
|
||||
"domain_pill.their_username": "Το μοναδικό του αναγνωριστικό στο διακομιστή του. Είναι πιθανό να βρεις χρήστες με το ίδιο όνομα χρήστη σε διαφορετικούς διακομιστές.",
|
||||
"domain_pill.username": "Όνομα χρήστη",
|
||||
"domain_pill.whats_in_a_handle": "Τί υπάρχει σε ένα πλήρες όνομα χρήστη;",
|
||||
"domain_pill.who_they_are": "Από τη στιγμή που τα πλήρη ονόματα λένε ποιος είναι κάποιος και πού είναι, μπορείς να αλληλεπιδράσεις με άτομα απ' όλο τον κοινωνικό ιστό των <button> πλατφορμών που στηρίζονται στο ActivityPub</button>.",
|
||||
"domain_pill.who_they_are": "Από τη στιγμή που τα πλήρη ονόματα χρηστών λένε ποιος είναι κάποιος και πού είναι, μπορείς να αλληλεπιδράσεις με άτομα απ' όλο τον κοινωνικό ιστό των <button>πλατφορμών που στηρίζονται στο ActivityPub</button>.",
|
||||
"domain_pill.who_you_are": "Επειδή το πλήρες όνομα χρήστη σου λέει ποιος είσαι και πού βρίσκεσαι, άτομα μπορούν να αλληλεπιδράσουν μαζί σου στον κοινωνικό ιστό των <button>πλατφορμών που στηρίζονται στο ActivityPub</button>.",
|
||||
"domain_pill.your_handle": "Το πλήρες όνομα χρήστη σου:",
|
||||
"domain_pill.your_server": "Το ψηφιακό σου σπίτι, όπου ζουν όλες σου οι αναρτήσεις. Δε σ' αρέσει αυτός; Μετακινήσου σε διακομιστές ανά πάσα στιγμή και πάρε και τους ακόλουθούς σου.",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Hide boosts in timeline",
|
||||
"account.menu.mention": "Mention",
|
||||
"account.menu.mute": "Mute account",
|
||||
"account.menu.note.description": "Visible only to you",
|
||||
"account.menu.open_original_page": "View on {domain}",
|
||||
"account.menu.remove_follower": "Remove follower",
|
||||
"account.menu.report": "Report account",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Muted",
|
||||
"account.muting": "Muting",
|
||||
"account.mutual": "You follow each other",
|
||||
"account.name.help.domain": "{domain} is the server that hosts the user’s profile and posts.",
|
||||
"account.name.help.domain_self": "{domain} is your server that hosts your profile and posts.",
|
||||
"account.name.help.footer": "Just like you can send emails to people using different email clients, you can interact with people on other Mastodon servers – and with anyone on other social apps powered by the same set of rules as Mastodon uses (the ActivityPub protocol).",
|
||||
"account.name.help.header": "A handle is like an email address",
|
||||
"account.name.help.username": "{username} is this account’s username on their server. Someone on another server might have the same username.",
|
||||
"account.name.help.username_self": "{username} is your username on this server. Someone on another server might have the same username.",
|
||||
"account.name_info": "What does this mean?",
|
||||
"account.no_bio": "No description provided.",
|
||||
"account.node_modal.callout": "Personal notes are visible only to you.",
|
||||
"account.node_modal.edit_title": "Edit personal note",
|
||||
|
||||
@@ -535,6 +535,8 @@
|
||||
"follow_suggestions.view_all": "View all",
|
||||
"follow_suggestions.who_to_follow": "Who to follow",
|
||||
"followed_tags": "Followed hashtags",
|
||||
"followers.hide_other_followers": "This user has chosen to not make their other followers visible",
|
||||
"following.hide_other_following": "This user has chosen to not make the rest of who they follow visible",
|
||||
"footer.about": "About",
|
||||
"footer.about_mastodon": "About Mastodon",
|
||||
"footer.about_server": "About {domain}",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Ocultar adhesiones en la línea temporal",
|
||||
"account.menu.mention": "Mención",
|
||||
"account.menu.mute": "Silenciar cuenta",
|
||||
"account.menu.note.description": "Visible solo para vos",
|
||||
"account.menu.open_original_page": "Ver en {domain}",
|
||||
"account.menu.remove_follower": "Quitar seguidor",
|
||||
"account.menu.report": "Denunciar cuenta",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Silenciado",
|
||||
"account.muting": "Silenciada",
|
||||
"account.mutual": "Se siguen mutuamente",
|
||||
"account.name.help.domain": "«{domain}» es el servidor que hospeda el perfil y los mensajes del usuario.",
|
||||
"account.name.help.domain_self": "«{domain}» es tu servidor que hospeda tu perfil y tus mensajes.",
|
||||
"account.name.help.footer": "Así como podés enviar correos electrónicos a personas usando diferentes clientes de email, del mismo modo podés interactuar con cuentas en otros servidores de Mastodon; y con cuentas en otras aplicaciones sociales que funcionen bajo las mismas normas que Mastodon (esto es, el protocolo ActivityPub).",
|
||||
"account.name.help.header": "Un alias es similar a una dirección de correo electrónico",
|
||||
"account.name.help.username": "«{username}» es el nombre de usuario de esta cuenta en su servidor. Alguien en otro servidor puede tener el mismo nombre de usuario.",
|
||||
"account.name.help.username_self": "«{username}» es tu nombre de usuario en este servidor. Alguien en otro servidor puede tener el mismo nombre de usuario.",
|
||||
"account.name_info": "¿Qué significa?",
|
||||
"account.no_bio": "Sin descripción provista.",
|
||||
"account.node_modal.callout": "Las notas personales son visibles solo para vos.",
|
||||
"account.node_modal.edit_title": "Editar nota personal",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Ocultar impulsos en la cronología",
|
||||
"account.menu.mention": "Mencionar",
|
||||
"account.menu.mute": "Silenciar cuenta",
|
||||
"account.menu.note.description": "Visible solo para ti",
|
||||
"account.menu.open_original_page": "Ver en {domain}",
|
||||
"account.menu.remove_follower": "Eliminar seguidor",
|
||||
"account.menu.report": "Denunciar cuenta",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Silenciado",
|
||||
"account.muting": "Silenciando",
|
||||
"account.mutual": "Se siguen el uno al otro",
|
||||
"account.name.help.domain": "{domain} es el servidor que aloja el perfil y las publicaciones del usuario.",
|
||||
"account.name.help.domain_self": "{domain} es el servidor que aloja tu perfil y tus publicaciones.",
|
||||
"account.name.help.footer": "Al igual que puedes enviar correos electrónicos a personas que utilizan diferentes clientes de correo electrónico, puedes interactuar con personas en otros servidores Mastodon, y con cualquier persona en otras aplicaciones sociales que funcionen con el mismo conjunto de reglas que utiliza Mastodon (el protocolo ActivityPub).",
|
||||
"account.name.help.header": "Un identificador es como una dirección de correo electrónico",
|
||||
"account.name.help.username": "{username} es el nombre de usuario de esta cuenta en su servidor. Es posible que alguien en otro servidor tenga el mismo nombre de usuario.",
|
||||
"account.name.help.username_self": "{username} es tu nombre de usuario en este servidor. Es posible que alguien en otro servidor tenga el mismo nombre de usuario.",
|
||||
"account.name_info": "¿Qué significa esto?",
|
||||
"account.no_bio": "Sin biografía.",
|
||||
"account.node_modal.callout": "Las notas personales solo son visibles para ti.",
|
||||
"account.node_modal.edit_title": "Editar nota personal",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Ocultar impulsos en la cronología",
|
||||
"account.menu.mention": "Mencionar",
|
||||
"account.menu.mute": "Silenciar cuenta",
|
||||
"account.menu.note.description": "Visible sólo para ti",
|
||||
"account.menu.open_original_page": "Ver en {domain}",
|
||||
"account.menu.remove_follower": "Eliminar seguidor",
|
||||
"account.menu.report": "Denunciar cuenta",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Silenciado",
|
||||
"account.muting": "Silenciando",
|
||||
"account.mutual": "Os seguís mutuamente",
|
||||
"account.name.help.domain": "{domain} es el servidor que alberga el perfil y las publicaciones del usuario.",
|
||||
"account.name.help.domain_self": "{domain} es tu servidor que aloja tu perfil y publicaciones.",
|
||||
"account.name.help.footer": "Al igual que puedes enviar correos electrónicos a personas usando diferentes clientes de correo electrónico, puedes interactuar con personas en otros servidores de Mastodon – y con personas en otras aplicaciones sociales que hablen el mismo idioma que Mastodon (el protocolo ActivityPub).",
|
||||
"account.name.help.header": "Un alias es como una dirección de correo electrónico",
|
||||
"account.name.help.username": "{username} es el nombre de usuario de esta cuenta en su servidor. Alguien en otro servidor puede tener el mismo nombre de usuario.",
|
||||
"account.name.help.username_self": "{username} es tu nombre de usuario en este servidor. Alguien en otro servidor puede tener el mismo nombre de usuario.",
|
||||
"account.name_info": "¿Qué significa esto?",
|
||||
"account.no_bio": "Sin biografía.",
|
||||
"account.node_modal.callout": "Las notas personales solo son visibles para ti.",
|
||||
"account.node_modal.edit_title": "Editar nota personal",
|
||||
@@ -242,7 +250,7 @@
|
||||
"collections.content_warning": "Advertencia de contenido",
|
||||
"collections.continue": "Continuar",
|
||||
"collections.create.accounts_subtitle": "Solo pueden añadirse cuentas que sigues y que han activado el descubrimiento.",
|
||||
"collections.create.accounts_title": "¿A quién presentarás en esta colección?",
|
||||
"collections.create.accounts_title": "¿A quién vas a destacar en esta colección?",
|
||||
"collections.create.basic_details_title": "Datos básicos",
|
||||
"collections.create.settings_title": "Ajustes",
|
||||
"collections.create.steps": "Paso {step}/{total}",
|
||||
@@ -250,7 +258,7 @@
|
||||
"collections.create_collection": "Crear colección",
|
||||
"collections.delete_collection": "Eliminar colección",
|
||||
"collections.description_length_hint": "Limitado a 100 caracteres",
|
||||
"collections.edit_details": "Cambiar datos básicos",
|
||||
"collections.edit_details": "Editar datos básicos",
|
||||
"collections.edit_settings": "Cambiar ajustes",
|
||||
"collections.error_loading_collections": "Se ha producido un error al intentar cargar tus colecciones.",
|
||||
"collections.manage_accounts": "Administrar cuentas",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Piilota tehostukset aikajanalta",
|
||||
"account.menu.mention": "Mainitse",
|
||||
"account.menu.mute": "Mykistä tili",
|
||||
"account.menu.note.description": "Näkyvät vain sinulle",
|
||||
"account.menu.open_original_page": "Näytä palvelimella {domain}",
|
||||
"account.menu.remove_follower": "Poista seuraaja",
|
||||
"account.menu.report": "Raportoi tili",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Fjal stimbranir í tíðarlinju",
|
||||
"account.menu.mention": "Umrøð",
|
||||
"account.menu.mute": "Doyv kontu",
|
||||
"account.menu.note.description": "Einans sjónligt fyri teg",
|
||||
"account.menu.open_original_page": "Vís á {domain}",
|
||||
"account.menu.remove_follower": "Strika fylgjara",
|
||||
"account.menu.report": "Melda kontu",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Sløkt/ur",
|
||||
"account.muting": "Doyvir",
|
||||
"account.mutual": "Tit fylgja hvønn annan",
|
||||
"account.name.help.domain": "{domain} er ambætarin, sum hýsir vangan og postarnar hjá brúkaranum.",
|
||||
"account.name.help.domain_self": "{domain} er tín ambætari, sum hýsir tín vanga og postar.",
|
||||
"account.name.help.footer": "Eins og tú kanst senda teldubrøv til fólk, sum brúka ymiskar teldupostskipanir, so kanst tú samvirka við fólk á øðrum Mastodon ambætarum - og við ein og hvønn á øðrum sosialum appum, ið eru bygdar á sama sett av reglum, sum Mastodon brúkar (ActivityPub protokollin).",
|
||||
"account.name.help.header": "Eitt hald er sum ein teldupostbústaður",
|
||||
"account.name.help.username": "{username} er brúkaranavnið hjá hesari kontuni á teirra ambætara. Onkur á einum øðrum ambætara kann hava sama brúkaranavn.",
|
||||
"account.name.help.username_self": "{username} er brúkaranavnið hjá tær á hesum ambætaranum. Onkur á einum øðrum ambætara kann hava sama brúkaranavn.",
|
||||
"account.name_info": "Hvat merkir hetta?",
|
||||
"account.no_bio": "Lýsing vantar.",
|
||||
"account.node_modal.callout": "Einans tú sær tínar persónligu notur.",
|
||||
"account.node_modal.edit_title": "Rætta persónliga notu",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Masquer les partages dans le fil d’actualité",
|
||||
"account.menu.mention": "Mentionner",
|
||||
"account.menu.mute": "Masquer le compte",
|
||||
"account.menu.note.description": "Visible uniquement par vous",
|
||||
"account.menu.open_original_page": "Voir sur {domain}",
|
||||
"account.menu.remove_follower": "Supprimer des abonné·e·s",
|
||||
"account.menu.report": "Signaler le compte",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Masqué·e",
|
||||
"account.muting": "Masqué",
|
||||
"account.mutual": "Vous vous suivez mutuellement",
|
||||
"account.name.help.domain": "{domain} est le serveur qui héberge le profil et les messages du compte.",
|
||||
"account.name.help.domain_self": "{domain} est le serveur qui héberge votre profil et vos messages.",
|
||||
"account.name.help.footer": "Tout comme vous pouvez envoyer des courriels à des personnes utilisant différents logiciels de messagerie, vous pouvez interagir avec des personnes sur d'autres serveurs Mastodon — et avec n'importe qui sur d'autres applications sociales propulsées par le même ensemble de règles que Mastodon utilise (le protocole ActivityPub).",
|
||||
"account.name.help.header": "Un identifiant est comme une adresse de courriel",
|
||||
"account.name.help.username": "{username} est le nom d'utilisateur·ice de ce compte sur son serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
|
||||
"account.name.help.username_self": "{username} est votre nom d'utilisateur·ice sur ce serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
|
||||
"account.name_info": "Qu'est-ce que cela signifie ?",
|
||||
"account.no_bio": "Description manquante.",
|
||||
"account.node_modal.callout": "Les notes personnelles sont ne sont visibles que pour vous.",
|
||||
"account.node_modal.edit_title": "Modifier la note personnelle",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Masquer les partages dans le fil d’actualité",
|
||||
"account.menu.mention": "Mentionner",
|
||||
"account.menu.mute": "Masquer le compte",
|
||||
"account.menu.note.description": "Visible uniquement par vous",
|
||||
"account.menu.open_original_page": "Voir sur {domain}",
|
||||
"account.menu.remove_follower": "Supprimer des abonné·e·s",
|
||||
"account.menu.report": "Signaler le compte",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Masqué·e",
|
||||
"account.muting": "Masqué",
|
||||
"account.mutual": "Vous vous suivez mutuellement",
|
||||
"account.name.help.domain": "{domain} est le serveur qui héberge le profil et les messages du compte.",
|
||||
"account.name.help.domain_self": "{domain} est le serveur qui héberge votre profil et vos messages.",
|
||||
"account.name.help.footer": "Tout comme vous pouvez envoyer des courriels à des personnes utilisant différents logiciels de messagerie, vous pouvez interagir avec des personnes sur d'autres serveurs Mastodon — et avec n'importe qui sur d'autres applications sociales propulsées par le même ensemble de règles que Mastodon utilise (le protocole ActivityPub).",
|
||||
"account.name.help.header": "Un identifiant est comme une adresse de courriel",
|
||||
"account.name.help.username": "{username} est le nom d'utilisateur·ice de ce compte sur son serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
|
||||
"account.name.help.username_self": "{username} est votre nom d'utilisateur·ice sur ce serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
|
||||
"account.name_info": "Qu'est-ce que cela signifie ?",
|
||||
"account.no_bio": "Aucune description fournie.",
|
||||
"account.node_modal.callout": "Les notes personnelles sont ne sont visibles que pour vous.",
|
||||
"account.node_modal.edit_title": "Modifier la note personnelle",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Folaigh borradh sa líne ama",
|
||||
"account.menu.mention": "Luaigh",
|
||||
"account.menu.mute": "Balbhaigh cuntas",
|
||||
"account.menu.note.description": "Le feiceáil agatsa amháin",
|
||||
"account.menu.open_original_page": "Féach ar {domain}",
|
||||
"account.menu.remove_follower": "Bain leantóir",
|
||||
"account.menu.report": "Tuairiscigh cuntas",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Balbhaithe",
|
||||
"account.muting": "Ag balbhaigh",
|
||||
"account.mutual": "Leanann sibh a chéile",
|
||||
"account.name.help.domain": "Is é {domain} an freastalaí a óstálann próifíl agus poist an úsáideora.",
|
||||
"account.name.help.domain_self": "Is é {domain} an freastalaí a óstálann do phróifíl agus do phoist.",
|
||||
"account.name.help.footer": "Díreach mar is féidir leat ríomhphoist a sheoladh chuig daoine ag baint úsáide as cliaint ríomhphoist éagsúla, is féidir leat idirghníomhú le daoine ar fhreastalaithe Mastodon eile – agus le duine ar bith ar aipeanna sóisialta eile atá faoi thiomáint ag an tsraith rialacha chéanna a úsáideann Mastodon (an prótacal ActivityPub).",
|
||||
"account.name.help.header": "Is cosúil le seoladh ríomhphoist é láimhseáil",
|
||||
"account.name.help.username": "Is é {username} ainm úsáideora an chuntais seo ar a bhfreastalaí. D’fhéadfadh an t-ainm úsáideora céanna a bheith ag duine éigin ar fhreastalaí eile.",
|
||||
"account.name.help.username_self": "Is é {username} d'ainm úsáideora ar an bhfreastalaí seo. D'fhéadfadh an t-ainm úsáideora céanna a bheith ag duine éigin ar fhreastalaí eile.",
|
||||
"account.name_info": "Cad is brí leis seo?",
|
||||
"account.no_bio": "Níor tugadh tuairisc.",
|
||||
"account.node_modal.callout": "Ní féidir ach leatsa nótaí pearsanta a fheiceáil.",
|
||||
"account.node_modal.edit_title": "Cuir nóta pearsanta in eagar",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Ocultar as promocións na cronoloxía",
|
||||
"account.menu.mention": "Mencionar a",
|
||||
"account.menu.mute": "Silenciar conta",
|
||||
"account.menu.note.description": "Só ti podes vela",
|
||||
"account.menu.open_original_page": "Ver en {domain}",
|
||||
"account.menu.remove_follower": "Quitar seguidora",
|
||||
"account.menu.report": "Denunciar conta",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Acalada",
|
||||
"account.muting": "Silenciamento",
|
||||
"account.mutual": "Seguimento mútuo",
|
||||
"account.name.help.domain": "{domain} é o servidor que aloxa o perfil e as publicacións da usuaria.",
|
||||
"account.name.help.domain_self": "{domain} é o servidor que aloxa o teu perfil e publicacións.",
|
||||
"account.name.help.footer": "Ao igual que envías correos electrónicos a persoas que usan diferentes provedores, podes interactuar con persoas en outros servidores Mastodon ― e con calquera outra que use aplicacións que sigan o mesmo sistema que usa Mastodon (o protocolo ActivityPub).",
|
||||
"account.name.help.header": "Un alcume é o como o enderezo de correo",
|
||||
"account.name.help.username": "{username} é o nome de usuaria da conta no seu servidor. Alguén noutro servidor podería ter o mesmo nome de usuaria.",
|
||||
"account.name.help.username_self": "{username} é o teu nome de usuaria nete servidor. Alguén noutro sevidor podería ter o mesmo nome de usuaria.",
|
||||
"account.name_info": "Isto que significa?",
|
||||
"account.no_bio": "Sen descrición.",
|
||||
"account.node_modal.callout": "As notas persoais só as podes ver ti.",
|
||||
"account.node_modal.edit_title": "Editar nota persoal",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "הסתר הדהודים בקו הזמן",
|
||||
"account.menu.mention": "אזכור",
|
||||
"account.menu.mute": "השתקת חשבון",
|
||||
"account.menu.note.description": "גלוי לך בלבד",
|
||||
"account.menu.open_original_page": "לצפות אצל {domain}",
|
||||
"account.menu.remove_follower": "הסרת עוקב",
|
||||
"account.menu.report": "דווח על חשבון",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "מושתק",
|
||||
"account.muting": "רשימת החשבונות המושתקים",
|
||||
"account.mutual": "אתם עוקביםות הדדית",
|
||||
"account.name.help.domain": "{domain} הוא השרת שמארח את פרופיל המשתמש(ת) וההודעות שכתב(ה).",
|
||||
"account.name.help.domain_self": "{domain} הוא השרת שמארח את פרופיל המשתמש(ת) שלך ואת ההודעות שכתבת.",
|
||||
"account.name.help.footer": "בדיוק כמו שתוכלו לשלוח דואל לאנשים דרך תוכנות דואל שונות, ניתן להיות בקשר עם אנשים על שרתי מסטודון וכל אדם בשרתי פדיברס (המשתמשים בפרוטוכול אקטיביטיפאב ActivityPub).",
|
||||
"account.name.help.header": "כינוי הוא כמו כתובת דואל",
|
||||
"account.name.help.username": "{username} הוא שם המשתמש של החשבון בשרת שלהם. מישהו משרת אחר יכול להחזיק באותו שם משתמש.",
|
||||
"account.name.help.username_self": "{username} הוא שם המשתמש של החשבון שלכם בשרת זה. מישהו משרת אחר יכול להחזיק באותו שם משתמש.",
|
||||
"account.name_info": "מה זה אומר?",
|
||||
"account.no_bio": "לא סופק תיאור.",
|
||||
"account.node_modal.callout": "הערות פרטיות גלויות לך בלבד.",
|
||||
"account.node_modal.edit_title": "עריכת הערה פרטית",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Fela endurbirtingar í tímalínu",
|
||||
"account.menu.mention": "Minnst á",
|
||||
"account.menu.mute": "Þagga niður í aðgangi",
|
||||
"account.menu.note.description": "Einungis sýnilegt þér",
|
||||
"account.menu.open_original_page": "Skoða á {domain}",
|
||||
"account.menu.remove_follower": "Fjarlægja fylgjanda",
|
||||
"account.menu.report": "Kæra aðgang",
|
||||
@@ -104,7 +105,14 @@
|
||||
"account.muted": "Þaggaður",
|
||||
"account.muting": "Þöggun",
|
||||
"account.mutual": "Þið fylgist með hvor öðrum",
|
||||
"account.no_bio": "Engri lýsingu útvegað.",
|
||||
"account.name.help.domain": "{domain} er netþjónninn sem hýsir upplýsingasnið um notandann og færslurnar hans.",
|
||||
"account.name.help.domain_self": "{domain} er netþjónninn þinn sem hýsir upplýsingasniðið þitt og færslurnar þínar.",
|
||||
"account.name.help.footer": "Rétt eins og að þú getur sent tölvupóst á fólk í gegnum mismunandi þjónustur og forrit, þá getur þú átt í samskiptum við fólk á öðrum Mastodon-netþjónum – og reyndar við hverja þá sem eru á öðrum þeim samfélagsmiðlum sem nota sömu samskiptareglur og Mastodon notar (sem er ActivityPub-samskiptamátinn).",
|
||||
"account.name.help.header": "Kennislóð (handle) líkist tölvupóstfangi",
|
||||
"account.name.help.username": "{username} er notandanafn þessa aðgangs á netþjóni viðkomandi. Einhver annar á öðrum netþjóni getur verið með sama notandanafnið.",
|
||||
"account.name.help.username_self": "{username} er notandanafnið þitt á þessum netþjóni. Einhver annar á öðrum netþjóni getur verið með sama notandanafnið.",
|
||||
"account.name_info": "Hvað þýðir þetta?",
|
||||
"account.no_bio": "Engin lýsing gefin upp.",
|
||||
"account.node_modal.callout": "Persónulegir einkaminnispunktar eru einungis sýnilegir þér.",
|
||||
"account.node_modal.edit_title": "Breyta einkaminnispunkti",
|
||||
"account.node_modal.error_unknown": "Tókst ekki að vista minnispunktinn",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"about.domain_blocks.suspended.title": "Sospeso",
|
||||
"about.language_label": "Lingua",
|
||||
"about.not_available": "Queste informazioni non sono state rese disponibili su questo server.",
|
||||
"about.powered_by": "Social media decentralizzato alimentato da {mastodon}",
|
||||
"about.powered_by": "Social media decentralizzato basato su {mastodon}",
|
||||
"about.rules": "Regole del server",
|
||||
"account.account_note_header": "Note personali",
|
||||
"account.activity": "Attività",
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Nascondi le condivisioni nella timeline",
|
||||
"account.menu.mention": "Menziona",
|
||||
"account.menu.mute": "Silenzia l'account",
|
||||
"account.menu.note.description": "Visibile solo a te",
|
||||
"account.menu.open_original_page": "Visualizza su {domain}",
|
||||
"account.menu.remove_follower": "Rimuovi il seguace",
|
||||
"account.menu.report": "Segnala l'account",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Mutato",
|
||||
"account.muting": "Account silenziato",
|
||||
"account.mutual": "Vi seguite a vicenda",
|
||||
"account.name.help.domain": "{domain} è il server che ospita il profilo e i post dell'utente.",
|
||||
"account.name.help.domain_self": "{domain} è il tuo server che ospita il tuo profilo e i post.",
|
||||
"account.name.help.footer": "Proprio come puoi inviare email a persone che usano diversi client di posta elettronica, così puoi interagire con le persone su altri server Mastodon — e con chiunque utilizzi altre app social basate sulle stesse regole che Mastodon usa (il protocollo ActivityPub).",
|
||||
"account.name.help.header": "Un nome univoco è come un indirizzo email",
|
||||
"account.name.help.username": "{username} è il nome utente di questo account sul suo server. Qualcuno su un altro server potrebbe avere lo stesso nome utente.",
|
||||
"account.name.help.username_self": "{username} è il tuo nome utente su questo server. Qualcuno su un altro server potrebbe avere lo stesso nome utente.",
|
||||
"account.name_info": "Cosa significa questo?",
|
||||
"account.no_bio": "Nessuna descrizione fornita.",
|
||||
"account.node_modal.callout": "Le note personali sono visibili solo per te.",
|
||||
"account.node_modal.edit_title": "Modifica la nota personale",
|
||||
|
||||
@@ -83,20 +83,21 @@
|
||||
"account.menu.add_to_list": "Aan lijst toevoegen…",
|
||||
"account.menu.block": "Account blokkeren",
|
||||
"account.menu.block_domain": "{domain} blokkeren",
|
||||
"account.menu.copied": "Accountlink gekopieerd naar het klembord",
|
||||
"account.menu.copied": "Accountlink naar het klembord gekopieerd",
|
||||
"account.menu.copy": "Link kopiëren",
|
||||
"account.menu.direct": "Privé vermelden",
|
||||
"account.menu.hide_reblogs": "Boosts verbergen in tijdlijn",
|
||||
"account.menu.direct": "Privébericht",
|
||||
"account.menu.hide_reblogs": "Boosts in tijdlijn verbergen",
|
||||
"account.menu.mention": "Vermelding",
|
||||
"account.menu.mute": "Account negeren",
|
||||
"account.menu.open_original_page": "Bekijken op {domain}",
|
||||
"account.menu.note.description": "Alleen voor jou zichtbaar",
|
||||
"account.menu.open_original_page": "Op {domain} bekijken",
|
||||
"account.menu.remove_follower": "Volger verwijderen",
|
||||
"account.menu.report": "Account rapporteren",
|
||||
"account.menu.share": "Delen…",
|
||||
"account.menu.show_reblogs": "Boosts in tijdlijn tonen",
|
||||
"account.menu.unblock": "Account deblokkeren",
|
||||
"account.menu.unblock_domain": "{domain} deblokkeren",
|
||||
"account.menu.unmute": "Negeren van account opheffen",
|
||||
"account.menu.unmute": "Account niet langer negeren",
|
||||
"account.moved_to": "{name} is verhuisd naar:",
|
||||
"account.mute": "@{name} negeren",
|
||||
"account.mute_notifications_short": "Meldingen negeren",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Genegeerd",
|
||||
"account.muting": "Genegeerd",
|
||||
"account.mutual": "Jullie volgen elkaar",
|
||||
"account.name.help.domain": "{domain} is de server waar zich het profiel en de berichten van de gebruiker bevinden.",
|
||||
"account.name.help.domain_self": "{domain} is jouw server waar zich jouw profiel en berichten bevinden.",
|
||||
"account.name.help.footer": "Net zoals je e-mails kunt verzenden naar mensen met verschillende e-mailproviders, kun je interactie hebben met mensen op andere Mastodon-servers – en met iedereen op andere social apps, die met behulp van het ActivityPub-protocol met Mastodon kunnen communiceren.",
|
||||
"account.name.help.header": "Een fediverse-adres valt te vergelijken met een e-mailadres",
|
||||
"account.name.help.username": "{username} is de gebruikersnaam van het account op hun server. Het is mogelijk dat iemand anders dezelfde gebruikersnaam op een andere server heeft.",
|
||||
"account.name.help.username_self": "{username} is jouw gebruikersnaam op deze server. Het is mogelijk dat iemand anders dezelfde gebruikersnaam op een andere server heeft.",
|
||||
"account.name_info": "Wat betekent dit?",
|
||||
"account.no_bio": "Geen beschrijving opgegeven.",
|
||||
"account.node_modal.callout": "Persoonlijke opmerkingen zijn alleen zichtbaar voor jou.",
|
||||
"account.node_modal.edit_title": "Persoonlijke opmerking bewerken",
|
||||
@@ -241,15 +249,15 @@
|
||||
"collections.collection_topic": "Onderwerp",
|
||||
"collections.content_warning": "Inhoudswaarschuwing",
|
||||
"collections.continue": "Doorgaan",
|
||||
"collections.create.accounts_subtitle": "Alleen accounts die je volgt die voor ontdekking gekozen hebben, kunnen worden toegevoegd.",
|
||||
"collections.create.accounts_title": "Wie ga je laten zien in deze verzameling?",
|
||||
"collections.create.accounts_subtitle": "Alleen accounts die je volgt en ontdekt willen worden, kunnen worden toegevoegd.",
|
||||
"collections.create.accounts_title": "Wie wil je aan deze verzameling toevoegen?",
|
||||
"collections.create.basic_details_title": "Basisgegevens",
|
||||
"collections.create.settings_title": "Instellingen",
|
||||
"collections.create.steps": "Stap {step}/{total}",
|
||||
"collections.create_a_collection_hint": "Een verzameling aanmaken om je favoriete accounts aan te bevelen of te delen met anderen.",
|
||||
"collections.create_collection": "Verzameling aanmaken",
|
||||
"collections.delete_collection": "Verzameling verwijderen",
|
||||
"collections.description_length_hint": "100 tekens limiet",
|
||||
"collections.description_length_hint": "Maximaal 100 karakters",
|
||||
"collections.edit_details": "Basisgegevens bewerken",
|
||||
"collections.edit_settings": "Instellingen bewerken",
|
||||
"collections.error_loading_collections": "Er is een fout opgetreden bij het laden van je verzamelingen.",
|
||||
@@ -263,10 +271,10 @@
|
||||
"collections.topic_hint": "Voeg een hashtag toe die anderen helpt het hoofdonderwerp van deze collectie te begrijpen.",
|
||||
"collections.view_collection": "Verzameling bekijken",
|
||||
"collections.visibility_public": "Openbaar",
|
||||
"collections.visibility_public_hint": "Ontdekbaar in zoekresultaten en andere gebieden waar aanbevelingen verschijnen.",
|
||||
"collections.visibility_title": "Zichbaarheid",
|
||||
"collections.visibility_unlisted": "Niet weergegeven",
|
||||
"collections.visibility_unlisted_hint": "Zichtbaar voor iedereen met een link. Verborgen voor zoekresultaten en aanbevelingen.",
|
||||
"collections.visibility_public_hint": "Te zien onder zoekresultaten en in andere gebieden waar aanbevelingen verschijnen.",
|
||||
"collections.visibility_title": "Zichtbaarheid",
|
||||
"collections.visibility_unlisted": "Niet zichtbaar",
|
||||
"collections.visibility_unlisted_hint": "Zichtbaar voor iedereen met een link. Verborgen onder zoekresultaten en aanbevelingen.",
|
||||
"column.about": "Over",
|
||||
"column.blocks": "Geblokkeerde gebruikers",
|
||||
"column.bookmarks": "Bladwijzers",
|
||||
@@ -301,7 +309,7 @@
|
||||
"combobox.loading": "Laden",
|
||||
"combobox.no_results_found": "Geen resultaten voor deze zoekopdracht",
|
||||
"combobox.open_results": "Resultaten openen",
|
||||
"combobox.results_available": "{count, plural, one {# suggesties} other {# suggesties}} beschikbaar. Gebruik de pijltjes omhoog en omlaag om te navigeren. Druk op Enter om te selecteren.",
|
||||
"combobox.results_available": "{count, plural, one {# suggestie} other {# suggesties}} beschikbaar. Gebruik de pijltjes omhoog en omlaag om te navigeren. Druk op Enter om te selecteren.",
|
||||
"community.column_settings.local_only": "Alleen lokaal",
|
||||
"community.column_settings.media_only": "Alleen media",
|
||||
"community.column_settings.remote_only": "Alleen andere servers",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Zaman çizelgesinde yeniden gönderimleri gizle",
|
||||
"account.menu.mention": "Bahset",
|
||||
"account.menu.mute": "Hesabı sessize al",
|
||||
"account.menu.note.description": "Sadece sizin tarafınızdan görülebilir",
|
||||
"account.menu.open_original_page": "{domain} alan adında görüntüle",
|
||||
"account.menu.remove_follower": "Takipçiyi kaldır",
|
||||
"account.menu.report": "Hesabı bildir",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Susturuldu",
|
||||
"account.muting": "Sessize alınıyor",
|
||||
"account.mutual": "Birbirinizi takip ediyorsunuz",
|
||||
"account.name.help.domain": "{domain} kullanıcının profilini ve gönderilerini barındıran sunucudur.",
|
||||
"account.name.help.domain_self": "{domain} profilinizi ve gönderilerinizi barındıran sunucunuzdur.",
|
||||
"account.name.help.footer": "Farklı e-posta istemcileri kullanan kişilere e-posta gönderebileceğiniz gibi, diğer Mastodon sunucularındaki kişilerle ve Mastodon'un kullandığı kurallarla (ActivityPub protokolü) çalışan diğer sosyal uygulamalardaki herkesle etkileşim kurabilirsiniz.",
|
||||
"account.name.help.header": "Takma ad bir e-posta adresi gibidir",
|
||||
"account.name.help.username": "{username} bu hesabın sunucudaki kullanıcı adıdır. Başka bir sunucudaki başka bir kişi de aynı kullanıcı adına sahip olabilir.",
|
||||
"account.name.help.username_self": "{username} bu sunucudaki kullanıcı adınızdır. Başka bir sunucudaki başka bir kullanıcı da aynı kullanıcı adına sahip olabilir.",
|
||||
"account.name_info": "Bu ne anlama geliyor?",
|
||||
"account.no_bio": "Herhangi bir açıklama belirtilmedi.",
|
||||
"account.node_modal.callout": "Kişisel notları sadece siz görüntüleyebilirsiniz.",
|
||||
"account.node_modal.edit_title": "Kişisel notu düzenle",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "Ẩn đăng lại trên bảng tin",
|
||||
"account.menu.mention": "Nhắc đến",
|
||||
"account.menu.mute": "Phớt lờ tài khoản",
|
||||
"account.menu.note.description": "Chỉ hiển thị với bạn",
|
||||
"account.menu.open_original_page": "Xem trên {domain}",
|
||||
"account.menu.remove_follower": "Xóa người theo dõi",
|
||||
"account.menu.report": "Báo cáo tài khoản",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "Đã phớt lờ",
|
||||
"account.muting": "Đang ẩn",
|
||||
"account.mutual": "Theo dõi nhau",
|
||||
"account.name.help.domain": "{domain} là máy chủ lưu trữ hồ sơ và tút của người dùng.",
|
||||
"account.name.help.domain_self": "{domain} là máy chủ lưu trữ hồ sơ và tút của bạn.",
|
||||
"account.name.help.footer": "Giống như bạn có thể gửi email cho mọi người bằng các dịch vụ email khác nhau, bạn có thể tương tác với mọi người trên các máy chủ Mastodon khác – và với bất kỳ ai trên các ứng dụng xã hội khác được cung cấp bởi cùng một bộ quy tắc mà Mastodon sử dụng (giao thức ActivityPub).",
|
||||
"account.name.help.header": "Một địa chỉ giống như một địa chỉ email",
|
||||
"account.name.help.username": "{username} là tên người dùng của tài khoản này trên máy chủ của họ. Trên máy chủ khác có thể có tên người dùng giống vậy.",
|
||||
"account.name.help.username_self": "{username} là tên người dùng của bạn trên máy chủ này. Trên máy chủ khác có thể có tên người dùng giống bạn.",
|
||||
"account.name_info": "Điều này nghĩa là gì?",
|
||||
"account.no_bio": "Chưa có miêu tả.",
|
||||
"account.node_modal.callout": "Các ghi chú chỉ hiển thị với bạn.",
|
||||
"account.node_modal.edit_title": "Sửa ghi chú",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"account.menu.hide_reblogs": "於時間軸中隱藏轉嘟",
|
||||
"account.menu.mention": "提及",
|
||||
"account.menu.mute": "靜音帳號",
|
||||
"account.menu.note.description": "僅有您可見",
|
||||
"account.menu.open_original_page": "於 {domain} 檢視",
|
||||
"account.menu.remove_follower": "移除跟隨者",
|
||||
"account.menu.report": "檢舉帳號",
|
||||
@@ -104,6 +105,13 @@
|
||||
"account.muted": "已靜音",
|
||||
"account.muting": "靜音",
|
||||
"account.mutual": "跟隨彼此",
|
||||
"account.name.help.domain": "{domain} 為託管此使用者個人檔案與嘟文之伺服器。",
|
||||
"account.name.help.domain_self": "{domain} 為託管您個人檔案與嘟文之伺服器。",
|
||||
"account.name.help.footer": "就像是您的以不同 email 程式寄信給他人一樣,您能與其他 Mastodon 伺服器中的人們互動,並且能和使用與 Mastodon 一樣規則 (ActivityPub 協定) 的其他社群軟體上任何人互動。",
|
||||
"account.name.help.header": "@handle 就像是 email 地址一樣",
|
||||
"account.name.help.username": "{username} 為該使用者於他們伺服器上的帳號名稱。其他伺服器上的某個人可能擁有一樣的使用者名稱。",
|
||||
"account.name.help.username_self": "{username} 為您於此伺服器上的帳號名稱。其他伺服器上的某個人可能擁有一樣的使用者名稱。",
|
||||
"account.name_info": "這是什麼?",
|
||||
"account.no_bio": "無個人檔案描述。",
|
||||
"account.node_modal.callout": "僅有您能見到您的個人備註。",
|
||||
"account.node_modal.edit_title": "編輯個人備註",
|
||||
|
||||
@@ -292,7 +292,7 @@ function shouldMarkNewNotificationsAsRead(
|
||||
|
||||
function updateLastReadId(
|
||||
state: NotificationGroupsState,
|
||||
group: NotificationGroup | undefined = undefined,
|
||||
group?: NotificationGroup,
|
||||
) {
|
||||
if (shouldMarkNewNotificationsAsRead(state)) {
|
||||
group = group ?? state.groups.find(isNotificationGroup);
|
||||
|
||||
@@ -20,7 +20,12 @@ import {
|
||||
TIMELINE_GAP,
|
||||
disconnectTimeline,
|
||||
} from '../actions/timelines';
|
||||
import { timelineDelete, isTimelineKeyPinned, isNonStatusId } from '../actions/timelines_typed';
|
||||
import {
|
||||
timelineDelete,
|
||||
timelineDeleteStatus,
|
||||
isTimelineKeyPinned,
|
||||
isNonStatusId,
|
||||
} from '../actions/timelines_typed';
|
||||
import { compareId } from '../compare_id';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
@@ -145,6 +150,11 @@ const deleteStatus = (state, id, references, exclude_account = null) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
const deleteStatusFromTimeline = (state, statusId, timelineKey) => {
|
||||
const helper = list => list.filterNot((status) => status === statusId);
|
||||
return state.updateIn([timelineKey, 'items'], helper).updateIn([timelineKey, 'pendingItems'], helper);
|
||||
}
|
||||
|
||||
const clearTimeline = (state, timeline) => {
|
||||
return state.set(timeline, initialTimeline);
|
||||
};
|
||||
@@ -200,25 +210,12 @@ export default function timelines(state = initialState, action) {
|
||||
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
|
||||
case TIMELINE_UPDATE:
|
||||
return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
|
||||
case timelineDelete.type:
|
||||
return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf);
|
||||
case TIMELINE_CLEAR:
|
||||
return clearTimeline(state, action.timeline);
|
||||
case blockAccountSuccess.type:
|
||||
case muteAccountSuccess.type:
|
||||
return filterTimelines(state, action.payload.relationship, action.payload.statuses);
|
||||
case unfollowAccountSuccess.type:
|
||||
return filterTimeline('home', state, action.payload.relationship, action.payload.statuses);
|
||||
case TIMELINE_SCROLL_TOP:
|
||||
return updateTop(state, action.timeline, action.top);
|
||||
case TIMELINE_CONNECT:
|
||||
return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems));
|
||||
case disconnectTimeline.type:
|
||||
return state.update(
|
||||
action.payload.timeline,
|
||||
initialTimeline,
|
||||
map => map.set('online', false).update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items),
|
||||
);
|
||||
case TIMELINE_MARK_AS_PARTIAL:
|
||||
return state.update(
|
||||
action.timeline,
|
||||
@@ -238,6 +235,29 @@ export default function timelines(state = initialState, action) {
|
||||
})
|
||||
);
|
||||
default:
|
||||
if (timelineDelete.match(action)) {
|
||||
return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf);
|
||||
} else if (timelineDeleteStatus.match(action)) {
|
||||
return deleteStatusFromTimeline(state, action.payload.statusId, action.payload.timelineKey);
|
||||
} else if (blockAccountSuccess.match(action) || muteAccountSuccess.match(action)) {
|
||||
return filterTimelines(state, action.payload.relationship, action.payload.statuses);
|
||||
} else if (unfollowAccountSuccess.match(action)) {
|
||||
return filterTimeline('home', state, action.payload.relationship, action.payload.statuses);
|
||||
} else if (disconnectTimeline.match(action)) {
|
||||
return state.update(
|
||||
action.payload.timeline,
|
||||
initialTimeline,
|
||||
(map) => map.set('online', false).update(
|
||||
action.payload.usePendingItems
|
||||
? 'pendingItems'
|
||||
: 'items',
|
||||
items => items.first()
|
||||
? items.unshift(TIMELINE_GAP)
|
||||
: items
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,5 +47,5 @@ export function toTypedTimeline(timeline?: ImmutableMap<string, unknown>) {
|
||||
emptyList,
|
||||
) as ImmutableList<string>,
|
||||
items: timeline.get('items', emptyList) as ImmutableList<string>,
|
||||
} as TimelineShape;
|
||||
} satisfies TimelineShape;
|
||||
}
|
||||
|
||||
35
app/javascript/mastodon/selectors/user_lists.ts
Normal file
35
app/javascript/mastodon/selectors/user_lists.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { createAppSelector } from '../store';
|
||||
|
||||
export const selectUserListWithoutMe = createAppSelector(
|
||||
[
|
||||
(state) => state.user_lists,
|
||||
(state) => (state.meta.get('me') as string | null) ?? null,
|
||||
(_state, listName: string) => listName,
|
||||
(_state, _listName, accountId?: string | null) => accountId ?? null,
|
||||
],
|
||||
(lists, currentAccountId, listName, accountId) => {
|
||||
if (!accountId || !listName) {
|
||||
return null;
|
||||
}
|
||||
const list = lists.getIn([listName, accountId]) as
|
||||
| ImmutableMap<string, unknown>
|
||||
| undefined;
|
||||
if (!list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Returns the list except the current account.
|
||||
return {
|
||||
items: (
|
||||
list.get('items', ImmutableList<string>()) as ImmutableList<string>
|
||||
)
|
||||
.filter((id) => id !== currentAccountId)
|
||||
.toArray(),
|
||||
isLoading: !!list.get('isLoading', true),
|
||||
hasMore: !!list.get('hasMore', false),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -226,7 +226,11 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def functional_or_moved?
|
||||
confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial?
|
||||
confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial? && !missing_2fa?
|
||||
end
|
||||
|
||||
def missing_2fa?
|
||||
!two_factor_enabled? && role.require_2fa?
|
||||
end
|
||||
|
||||
def unconfirmed_or_pending?
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
# Table name: user_roles
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# name :string default(""), not null
|
||||
# color :string default(""), not null
|
||||
# position :integer default(0), not null
|
||||
# permissions :bigint(8) default(0), not null
|
||||
# highlighted :boolean default(FALSE), not null
|
||||
# name :string default(""), not null
|
||||
# permissions :bigint(8) default(0), not null
|
||||
# position :integer default(0), not null
|
||||
# require_2fa :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
@@ -160,7 +161,7 @@ class UserRole < ApplicationRecord
|
||||
@computed_permissions ||= begin
|
||||
permissions = self.class.everyone.permissions | self.permissions
|
||||
|
||||
if permissions & FLAGS[:administrator] == FLAGS[:administrator]
|
||||
if administrator?
|
||||
Flags::ALL
|
||||
else
|
||||
permissions
|
||||
@@ -172,6 +173,10 @@ class UserRole < ApplicationRecord
|
||||
name
|
||||
end
|
||||
|
||||
def administrator?
|
||||
permissions & FLAGS[:administrator] == FLAGS[:administrator]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def in_permissions?(privilege)
|
||||
@@ -189,6 +194,7 @@ class UserRole < ApplicationRecord
|
||||
|
||||
errors.add(:permissions_as_keys, :own_role) if permissions_changed?
|
||||
errors.add(:position, :own_role) if position_changed?
|
||||
errors.add(:require_2fa, :own_role) if require_2fa_changed? && !administrator?
|
||||
end
|
||||
|
||||
def validate_permissions_elevation
|
||||
|
||||
26
app/serializers/activitypub/add_featured_item_serializer.rb
Normal file
26
app/serializers/activitypub/add_featured_item_serializer.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::AddFeaturedItemSerializer < ActivityPub::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :type, :actor, :target
|
||||
has_one :object, serializer: ActivityPub::FeaturedItemSerializer
|
||||
|
||||
def type
|
||||
'Add'
|
||||
end
|
||||
|
||||
def actor
|
||||
ActivityPub::TagManager.instance.uri_for(collection.account)
|
||||
end
|
||||
|
||||
def target
|
||||
ActivityPub::TagManager.instance.uri_for(collection)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection
|
||||
@collection ||= object.collection
|
||||
end
|
||||
end
|
||||
@@ -1,22 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
||||
class FeaturedItemSerializer < ActivityPub::Serializer
|
||||
attributes :type, :featured_object, :featured_object_type
|
||||
|
||||
def type
|
||||
'FeaturedItem'
|
||||
end
|
||||
|
||||
def featured_object
|
||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
||||
end
|
||||
|
||||
def featured_object_type
|
||||
object.account.actor_type || 'Person'
|
||||
end
|
||||
end
|
||||
|
||||
attributes :id, :type, :total_items, :name, :attributed_to,
|
||||
:sensitive, :discoverable, :published, :updated
|
||||
|
||||
@@ -25,7 +9,7 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer
|
||||
|
||||
has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer
|
||||
|
||||
has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer
|
||||
has_many :collection_items, key: :ordered_items, serializer: ActivityPub::FeaturedItemSerializer
|
||||
|
||||
def id
|
||||
ActivityPub::TagManager.instance.uri_for(object)
|
||||
|
||||
17
app/serializers/activitypub/featured_item_serializer.rb
Normal file
17
app/serializers/activitypub/featured_item_serializer.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer
|
||||
attributes :type, :featured_object, :featured_object_type
|
||||
|
||||
def type
|
||||
'FeaturedItem'
|
||||
end
|
||||
|
||||
def featured_object
|
||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
||||
end
|
||||
|
||||
def featured_object_type
|
||||
object.account.actor_type || 'Person'
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
||||
@json = fetch_resource(uri, false)
|
||||
|
||||
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
|
||||
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
|
||||
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json) || (supported_security_context?(@json) && @json['owner'].present? && !actor_type?)
|
||||
raise Error, "Unexpected object type for key #{uri}" unless expected_type?
|
||||
return find_actor(@json['id'], @json, suppress_errors) if actor_type?
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ class AddAccountToCollectionService
|
||||
|
||||
raise Mastodon::NotPermittedError, I18n.t('accounts.errors.cannot_be_added_to_collections') unless AccountPolicy.new(@collection.account, @account).feature?
|
||||
|
||||
create_collection_item
|
||||
@collection_item = create_collection_item
|
||||
|
||||
distribute_add_activity if @account.local? && Mastodon::Feature.collections_federation_enabled?
|
||||
|
||||
@collection_item
|
||||
end
|
||||
|
||||
private
|
||||
@@ -20,4 +24,12 @@ class AddAccountToCollectionService
|
||||
state: :accepted
|
||||
)
|
||||
end
|
||||
|
||||
def distribute_add_activity
|
||||
ActivityPub::AccountRawDistributionWorker.perform_async(activity_json, @collection.account_id)
|
||||
end
|
||||
|
||||
def activity_json
|
||||
ActiveModelSerializers::SerializableResource.new(@collection_item, serializer: ActivityPub::AddFeaturedItemSerializer, adapter: ActivityPub::Adapter).to_json
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
= form.input :highlighted,
|
||||
wrapper: :with_label
|
||||
|
||||
- if current_user.role.administrator? || current_user.role != form.object
|
||||
.fields-group
|
||||
= form.input :require_2fa,
|
||||
wrapper: :with_label
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- unless current_user.role == form.object
|
||||
|
||||
@@ -21,9 +21,13 @@
|
||||
.announcements-list__item__meta
|
||||
- if role.everyone?
|
||||
= t('admin.roles.everyone_full_description_html')
|
||||
= t('admin.roles.requires_2fa') if role.require_2fa?
|
||||
- else
|
||||
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id)
|
||||
·
|
||||
%abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
|
||||
- if role.require_2fa?
|
||||
·
|
||||
= t('admin.roles.requires_2fa')
|
||||
.announcements-list__item__actions
|
||||
= table_link_to 'edit', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role)
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
%samp.qr-alternative__code= @new_otp_secret.scan(/.{4}/).join(' ')
|
||||
|
||||
.fields-group
|
||||
= hidden_field_tag :oauth, params[:oauth]
|
||||
|
||||
= f.input :otp_attempt,
|
||||
hint: t('otp_authentication.code_hint'),
|
||||
input_html: { autocomplete: 'off' },
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
- content_for :page_title do
|
||||
= t('settings.two_factor_authentication')
|
||||
|
||||
- if current_user.role.require_2fa?
|
||||
.flash-message= t('two_factor_authentication.role_requirement', domain: site_hostname)
|
||||
|
||||
.simple_form
|
||||
%p.hint= t('otp_authentication.description_html')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= link_to t('otp_authentication.setup'), settings_otp_authentication_path, data: { method: :post }, class: 'button button--block'
|
||||
= link_to t('otp_authentication.setup'), settings_otp_authentication_path(params.permit(:oauth)), data: { method: :post }, class: 'button button--block'
|
||||
|
||||
@@ -7,3 +7,9 @@
|
||||
- @recovery_codes.each do |code|
|
||||
%li<
|
||||
%samp= code
|
||||
|
||||
- if params[:oauth]
|
||||
%hr.spacer/
|
||||
|
||||
.simple_form
|
||||
= link_to t('two_factor_authentication.resume_app_authorization'), return_to_app_url, class: 'button button--block'
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
= t('settings.two_factor_authentication')
|
||||
|
||||
- content_for :heading_actions do
|
||||
= link_to t('two_factor_authentication.disable'), disable_settings_two_factor_authentication_methods_path, class: 'button button--destructive', method: :post
|
||||
= link_to t('two_factor_authentication.disable'), disable_settings_two_factor_authentication_methods_path, class: 'button button--destructive', method: :post unless current_user.role.require_2fa?
|
||||
|
||||
- if current_user.role.require_2fa?
|
||||
.flash-message= t('two_factor_authentication.role_requirement', domain: site_hostname)
|
||||
|
||||
%p.hint
|
||||
%span.positive-hint
|
||||
|
||||
@@ -1444,10 +1444,6 @@ an:
|
||||
too_late: Ye masiau tarde pa apelar esta amonestación
|
||||
tags:
|
||||
does_not_match_previous_name: no coincide con o nombre anterior
|
||||
themes:
|
||||
contrast: Mastodon (Alto contraste)
|
||||
default: Mastodon (Fosco)
|
||||
mastodon-light: Mastodon (claro)
|
||||
time:
|
||||
formats:
|
||||
default: "%d de %b de %Y, %H:%M"
|
||||
|
||||
@@ -898,6 +898,10 @@ ar:
|
||||
all: للجميع
|
||||
disabled: لا أحد
|
||||
users: للمستخدمين المتصلين محليا
|
||||
landing_page:
|
||||
values:
|
||||
about: عن
|
||||
trends: المتداوَلة
|
||||
registrations:
|
||||
moderation_recommandation: الرجاء التأكد من أن لديك فريق إشراف كافي وفعال قبل فتح التسجيلات للجميع!
|
||||
preamble: تحكّم في مَن الذي يمكنه إنشاء حساب على خادمك الخاص.
|
||||
@@ -951,6 +955,7 @@ ar:
|
||||
no_status_selected: لم يطرأ أي تغيير على أي منشور بما أنه لم يتم اختيار أي واحد
|
||||
open: افتح المنشور
|
||||
original_status: المنشور الأصلي
|
||||
quotes: الاقتباسات
|
||||
reblogs: المعاد تدوينها
|
||||
replied_to_html: تم الرد على %{acct_link}
|
||||
status_changed: عُدّل المنشور
|
||||
@@ -958,6 +963,7 @@ ar:
|
||||
title: منشورات الحساب - @%{name}
|
||||
trending: المتداولة
|
||||
view_publicly: رؤية الجزء العلني
|
||||
view_quoted_post: عرض المنشور المقتبس
|
||||
visibility: مدى الظهور
|
||||
with_media: تحتوي على وسائط
|
||||
strikes:
|
||||
@@ -1251,6 +1257,7 @@ ar:
|
||||
hint_html: إذا كنت ترغب في الانتقال من حساب آخر إلى هذا الحساب الحالي، يمكنك إنشاء اسم مستعار هنا، والذي هو مطلوب قبل أن تتمكن من المضي قدما في نقل متابِعيك من الحساب القديم إلى هذا الحساب. هذا الإجراء بحد ذاته هو <strong>غير مؤذي و قابل للعكس</strong>. <strong>تتم بداية تهجير الحساب من الحساب القديم</strong>.
|
||||
remove: إلغاء ربط الكنية
|
||||
appearance:
|
||||
advanced_settings: الإعدادات المتقدمة
|
||||
animations_and_accessibility: الإتاحة والحركة
|
||||
discovery: الاستكشاف
|
||||
localization:
|
||||
@@ -1368,6 +1375,12 @@ ar:
|
||||
hint_html: "<strong>توصية:</strong> لن نطلب منك ثانية كلمتك السرية في غضون الساعة اللاحقة."
|
||||
invalid_password: الكلمة السرية خاطئة
|
||||
prompt: أكِّد الكلمة السرية للمواصلة
|
||||
color_scheme:
|
||||
auto: تلقائي
|
||||
dark: داكن
|
||||
light: فاتح
|
||||
contrast:
|
||||
auto: تلقائي
|
||||
crypto:
|
||||
errors:
|
||||
invalid_key: ليس بمفتاح Ed25519 أو Curve25519 صالح
|
||||
@@ -1733,6 +1746,9 @@ ar:
|
||||
expires_at: تنتهي مدة صلاحيتها في
|
||||
uses: عدد الاستخدامات
|
||||
title: دعوة أشخاص
|
||||
link_preview:
|
||||
potentially_sensitive_content:
|
||||
hide_button: إخفاء
|
||||
lists:
|
||||
errors:
|
||||
limit: لقد بلغت الحد الأقصى للقوائم
|
||||
@@ -2052,6 +2068,9 @@ ar:
|
||||
zero: "%{count} فيديوهات"
|
||||
boosted_from_html: تم إعادة ترقيته مِن %{acct_link}
|
||||
content_warning: 'تحذير عن المحتوى: %{warning}'
|
||||
content_warnings:
|
||||
hide: إخفاء المنشور
|
||||
show: إظهار المزيد
|
||||
default_language: نفس لغة الواجهة
|
||||
disallowed_hashtags:
|
||||
few: 'يحتوي على وسوم غير مسموح بها: %{tags}'
|
||||
@@ -2128,11 +2147,6 @@ ar:
|
||||
past_preamble_html: لقد غيرنا شروط خدمتنا منذ زيارتكم الأخيرة. نشجعكم على مراجعة الشروط المحدثة.
|
||||
review_link: مراجعة شروط الخدمة
|
||||
title: شروط خدمة النطاق %{domain} ستتغير
|
||||
themes:
|
||||
contrast: ماستدون (تباين عالٍ)
|
||||
default: ماستدون (داكن)
|
||||
mastodon-light: ماستدون (فاتح)
|
||||
system: تلقائي (استخدم سمة النظام)
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
@@ -2304,6 +2318,7 @@ ar:
|
||||
error: حدثت مشكلة ما خلال حذف مفتاح أمانك. الرجاء المحاولة مرة أخرى.
|
||||
success: تم حذف مفتاح الأمان الخاص بك.
|
||||
invalid_credential: مفتاح الأمان غير صالح
|
||||
nickname: الكنية
|
||||
nickname_hint: أدخل اسم مستعار لمفتاح الأمان الجديد الخاص بك
|
||||
not_enabled: لم تقم بتفعيل WebAuthn بعد
|
||||
not_supported: هذا المتصفح لا يدعم مفاتيح الأمان
|
||||
|
||||
@@ -834,10 +834,6 @@ ast:
|
||||
sensitive_content: Conteníu sensible
|
||||
tags:
|
||||
does_not_match_previous_name: nun concasa col nome anterior
|
||||
themes:
|
||||
contrast: Mastodon (contraste altu)
|
||||
default: Mastodon (escuridá)
|
||||
mastodon-light: Mastodon (claridá)
|
||||
two_factor_authentication:
|
||||
enabled: L'autenticación en dos pasos ta activada
|
||||
enabled_success: L'autenticación en dos pasos activóse correutamente
|
||||
|
||||
@@ -1834,7 +1834,7 @@ be:
|
||||
trillion: трлн
|
||||
otp_authentication:
|
||||
code_hint: Каб пацвердзіць, увядзіце код, згенераваны праграмай-аўтэнтыфікатарам
|
||||
description_html: Калі вы ўключыце <strong>двухфактарную аўтэнтыфікацыю</strong> праз праграму-аўтэнтыфікатар, то каб увайсці ва ўліковы запіс, вам трэба мець тэлефон, які будзе генераваць токены для ўводу.
|
||||
description_html: Калі вы ўключыце <strong>двухфактарную аўтэнтыфікацыю</strong> праз праграму-аўтэнтыфікатар, то для ўваходу ва ўліковы запіс вам трэба будзе мець тэлефон, які будзе генераваць токены для ўводу.
|
||||
enable: Уключыць
|
||||
instructions_html: "<strong>Скануйце гэты QR-код у Google Authenticator або ў аналагічнай TOTP-праграме на вашым тэлефоне</strong>. З гэтага моманту праграма будзе генераваць токены, якія вам трэба будзе ўводзіць пры ўваходзе ва ўліковы запіс."
|
||||
manual_instructions: 'Калі вы не можаце сканаваць QR-код і трэба ўвесці яго ўручную, вось ключ у форме тэксту:'
|
||||
@@ -2109,10 +2109,7 @@ be:
|
||||
review_link: Прачытаць умовы карыстання
|
||||
title: На %{domain} змяняюцца ўмовы карыстання
|
||||
themes:
|
||||
contrast: Mastodon (высокі кантраст)
|
||||
default: Mastodon (цёмная)
|
||||
mastodon-light: Mastodon (светлая)
|
||||
system: Аўтаматычна (выкарыстоўваць сістэмную тэму)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d.%m.%Y %H:%M"
|
||||
|
||||
@@ -1975,11 +1975,6 @@ bg:
|
||||
past_preamble_html: Променихме условията на услугата ни от последното ви посещение. Насърчаваме ви да разгледате обновените условия.
|
||||
review_link: Разглеждане на условията на услугата
|
||||
title: Условията на услугата на %{domain} се променят
|
||||
themes:
|
||||
contrast: Mastodon (висок контраст)
|
||||
default: Mastodon (тъмно)
|
||||
mastodon-light: Mastodon (светло)
|
||||
system: Самодейно (употреба на системната тема)
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b, %Y, %H:%M"
|
||||
|
||||
@@ -867,11 +867,6 @@ br:
|
||||
'7889238': 3 mizvezh
|
||||
terms_of_service:
|
||||
title: Divizoù implijout
|
||||
themes:
|
||||
contrast: Mastodon (Dargemm kreñv)
|
||||
default: Mastodon (Teñval)
|
||||
mastodon-light: Mastodon (Sklaer)
|
||||
system: Emgefreek (implijout neuz ar reizhiad)
|
||||
time:
|
||||
formats:
|
||||
default: "%d a viz %b %Y, %H:%M"
|
||||
|
||||
@@ -1995,11 +1995,6 @@ ca:
|
||||
past_preamble_html: Hem fet canvis a les condicions de servei des de la vostra darrera visita. Us recomanem que hi doneu un cop d'ull.
|
||||
review_link: Revisió de les condicions de servei
|
||||
title: Les condicions de servei de %{domain} han canviat
|
||||
themes:
|
||||
contrast: Mastodon (alt contrast)
|
||||
default: Mastodon (fosc)
|
||||
mastodon-light: Mastodon (clar)
|
||||
system: Automàtic (utilitza el tema del sistema)
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
||||
@@ -939,10 +939,6 @@ ckb:
|
||||
sensitive_content: ناوەڕۆکی هەستیار
|
||||
tags:
|
||||
does_not_match_previous_name: لەگەڵ ناوی پێشوو یەک ناگرێتەوە
|
||||
themes:
|
||||
contrast: ماستۆدۆن (کۆنتراستی بەرز)
|
||||
default: ماستۆدۆن (ڕەش)
|
||||
mastodon-light: ماستۆدۆن (کاڵ)
|
||||
two_factor_authentication:
|
||||
add: زیادکردن
|
||||
disable: لەکارخستنی 2FA
|
||||
|
||||
@@ -950,10 +950,6 @@ co:
|
||||
sensitive_content: Cuntenutu sensibile
|
||||
tags:
|
||||
does_not_match_previous_name: ùn currisponde micca à l'anzianu nome
|
||||
themes:
|
||||
contrast: Mastodon (Cuntrastu altu)
|
||||
default: Mastodon (Scuru)
|
||||
mastodon-light: Mastodon (Chjaru)
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
||||
@@ -2103,11 +2103,6 @@ cs:
|
||||
past_preamble_html: Od vaší poslední návštěvy jsme změnili podmínky služby. Doporučujeme vám zkontrolovat aktualizované podmínky.
|
||||
review_link: Zkontrolovat podmínky užívání služby
|
||||
title: Podmínky služby %{domain} se mění
|
||||
themes:
|
||||
contrast: Mastodon (vysoký kontrast)
|
||||
default: Mastodon (tmavý)
|
||||
mastodon-light: Mastodon (světlý)
|
||||
system: Automaticky (dle motivu systému)
|
||||
time:
|
||||
formats:
|
||||
default: "%d. %b %Y, %H:%M"
|
||||
|
||||
@@ -2189,11 +2189,6 @@ cy:
|
||||
past_preamble_html: Rydym wedi newid ein telerau gwasanaeth ers eich ymweliad diwethaf. Rydym yn eich annog i ddarllen y telerau wedi'u diweddaru.
|
||||
review_link: Darllen y telerau gwasanaeth
|
||||
title: Mae telerau gwasanaeth %{domain} yn newid
|
||||
themes:
|
||||
contrast: Mastodon (Cyferbyniad uchel)
|
||||
default: Mastodon (Tywyll)
|
||||
mastodon-light: Mastodon (Golau)
|
||||
system: Awtomatig (defnyddio thema system)
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ da:
|
||||
review_link: Gennemgå Tjenestevilkår
|
||||
title: Tjenestevilkårene for %{domain} ændres
|
||||
themes:
|
||||
contrast: Mastodon (høj kontrast)
|
||||
default: Mastodon (mørkt)
|
||||
mastodon-light: Mastodon (lyst)
|
||||
system: Automatisk (benyt systemtema)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d. %b %Y, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ de:
|
||||
review_link: Nutzungsbedingungen lesen
|
||||
title: Die Nutzungsbedingungen von %{domain} ändern sich
|
||||
themes:
|
||||
contrast: Mastodon (Hoher Kontrast)
|
||||
default: Mastodon (Dunkel)
|
||||
mastodon-light: Mastodon (Hell)
|
||||
system: Automatisch (wie Betriebssystem)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d. %b %Y, %H:%M Uhr"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ el:
|
||||
review_link: Επισκόπηση όρων υπηρεσίας
|
||||
title: Οι όροι παροχής υπηρεσιών του %{domain} αλλάζουν
|
||||
themes:
|
||||
contrast: Mastodon (Υψηλή αντίθεση)
|
||||
default: Mastodon (Σκοτεινό)
|
||||
mastodon-light: Mastodon (Ανοιχτόχρωμο)
|
||||
system: Αυτόματο (θέμα συστήματος)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ en-GB:
|
||||
review_link: Review terms of service
|
||||
title: The terms of service of %{domain} are changing
|
||||
themes:
|
||||
contrast: Mastodon (High contrast)
|
||||
default: Mastodon (Dark)
|
||||
mastodon-light: Mastodon (Light)
|
||||
system: Automatic (use system theme)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
||||
@@ -802,6 +802,7 @@ en:
|
||||
view_devops_description: Allows users to access Sidekiq and pgHero dashboards
|
||||
view_feeds: View live and topic feeds
|
||||
view_feeds_description: Allows users to access the live and topic feeds regardless of server settings
|
||||
requires_2fa: Requires two-factor authentication
|
||||
title: Roles
|
||||
rules:
|
||||
add_new: Add rule
|
||||
@@ -2047,6 +2048,8 @@ en:
|
||||
recovery_codes: Backup recovery codes
|
||||
recovery_codes_regenerated: Recovery codes successfully regenerated
|
||||
recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
|
||||
resume_app_authorization: Resume application authorization
|
||||
role_requirement: "%{domain} requires you to set up Two-Factor Authentication before you can use Mastodon."
|
||||
webauthn: Security keys
|
||||
user_mailer:
|
||||
announcement_published:
|
||||
|
||||
@@ -1957,11 +1957,6 @@ eo:
|
||||
terms_of_service_interstitial:
|
||||
past_preamble_html: Ni ŝanĝis niajn servkondiĉojn ekde via lasta vizito. Ni instigas vin revizii la ĝisdatigitajn kondiĉojn.
|
||||
title: La servkondiĉoj de %{domain} ŝanĝiĝas
|
||||
themes:
|
||||
contrast: Mastodon (Forta kontrasto)
|
||||
default: Mastodon (Malhela)
|
||||
mastodon-light: Mastodon (Hela)
|
||||
system: Aŭtomata (uzu sisteman temon)
|
||||
time:
|
||||
formats:
|
||||
default: "%Y.%b.%d, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ es-AR:
|
||||
review_link: Revisar los términos del servicio
|
||||
title: Los términos del servicio de %{domain} están cambiando
|
||||
themes:
|
||||
contrast: Alto contraste
|
||||
default: Oscuro
|
||||
mastodon-light: Claro
|
||||
system: Automático (usar tema del sistema)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%Y.%b.%d, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ es-MX:
|
||||
review_link: Revisar los términos de servicio
|
||||
title: Los términos de servicio de %{domain} van a cambiar
|
||||
themes:
|
||||
contrast: Alto contraste
|
||||
default: Mastodon
|
||||
mastodon-light: Mastodon (claro)
|
||||
system: Automático (usar tema del sistema)
|
||||
time:
|
||||
formats:
|
||||
default: "%d de %b del %Y, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ es:
|
||||
review_link: Revisar los términos de servicio
|
||||
title: Los términos de servicio de %{domain} van a cambiar
|
||||
themes:
|
||||
contrast: Mastodon (alto contraste)
|
||||
default: Mastodon (oscuro)
|
||||
mastodon-light: Mastodon (claro)
|
||||
system: Automático (usar tema del sistema)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d de %b del %Y, %H:%M"
|
||||
|
||||
@@ -2022,11 +2022,6 @@ et:
|
||||
past_preamble_html: Peale sinu viimast külastust oleme muutnud oma kasutustingimusi. Palun vaata muutunud tingimused üle.
|
||||
review_link: Vaata üle kasutustingimused
|
||||
title: "%{domain} saidi kasutustingimused muutuvad"
|
||||
themes:
|
||||
contrast: Mastodon (Kõrge kontrast)
|
||||
default: Mastodon (Tume)
|
||||
mastodon-light: Mastodon (Hele)
|
||||
system: Automaatne (kasuta süsteemi teemat)
|
||||
time:
|
||||
formats:
|
||||
default: "%d. %B, %Y. aastal, kell %H:%M"
|
||||
|
||||
@@ -1921,11 +1921,6 @@ eu:
|
||||
future_preamble_html: Gure zerbitzu-baldintzetan aldaketa batzuk egiten ari gara, eta <strong>%{date}</strong>-tik aurrera jarriko dira indarrean. Eguneratutako baldintzak berrikustea gomendatzen dizugu.
|
||||
past_preamble_html: Zerbitzu-baldintzak aldatu ditugu zure azken bisitatik. Baldintza eguneratuak berrikustera animatzen zaitugu.
|
||||
review_link: Berrikusi zerbitzu-baldintzak
|
||||
themes:
|
||||
contrast: Mastodon (Kontraste altua)
|
||||
default: Mastodon (Iluna)
|
||||
mastodon-light: Mastodon (Argia)
|
||||
system: Automatikoa (erabili sistemaren gaia)
|
||||
time:
|
||||
formats:
|
||||
default: "%Y(e)ko %b %d, %H:%M"
|
||||
|
||||
@@ -2016,11 +2016,6 @@ fa:
|
||||
past_preamble_html: شرایط خدماتمان را تغییر دادهایم. تشویقتان میکنیم که شرایط بهروز شده را بازبینی کنید.
|
||||
review_link: بازبینی شرایط استفاده
|
||||
title: شرایط خدمات %{domain} در حال تغییر است
|
||||
themes:
|
||||
contrast: ماستودون (سایهروشن بالا)
|
||||
default: ماستودون (تیره)
|
||||
mastodon-light: ماستودون (روشن)
|
||||
system: خودکار (استفاده از زمینهٔ سامانه)
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ fi:
|
||||
review_link: Käy käyttöehdot läpi
|
||||
title: Palvelimen %{domain} käyttöehdot muuttuvat
|
||||
themes:
|
||||
contrast: Mastodon (suuri kontrasti)
|
||||
default: Mastodon (tumma)
|
||||
mastodon-light: Mastodon (vaalea)
|
||||
system: Automaattinen (käytä järjestelmän teemaa)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%-d.%-m.%Y klo %-H.%M"
|
||||
|
||||
@@ -2018,10 +2018,7 @@ fo:
|
||||
review_link: Eftirkanna tænastutreytir
|
||||
title: Tænastutreytirnar á %{domain} verða broyttar
|
||||
themes:
|
||||
contrast: Mastodon (høgur kontrastur)
|
||||
default: Mastodon (myrkt)
|
||||
mastodon-light: Mastodon (ljóst)
|
||||
system: Sjálvvirkandi (brúka vanligt uppsetingareyðkenni)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
||||
@@ -2024,10 +2024,7 @@ fr-CA:
|
||||
review_link: Vérifier les conditions d'utilisation
|
||||
title: Les conditions d'utilisation de %{domain} ont changées
|
||||
themes:
|
||||
contrast: Mastodon (Contraste élevé)
|
||||
default: Mastodon (Sombre)
|
||||
mastodon-light: Mastodon (Clair)
|
||||
system: Automatique (utiliser le thème système)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
||||
@@ -2024,10 +2024,7 @@ fr:
|
||||
review_link: Vérifier les conditions d'utilisation
|
||||
title: Les conditions d'utilisation de %{domain} ont changées
|
||||
themes:
|
||||
contrast: Mastodon (Contraste élevé)
|
||||
default: Mastodon (Sombre)
|
||||
mastodon-light: Mastodon (Clair)
|
||||
system: Automatique (utiliser le thème système)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
||||
@@ -1948,11 +1948,6 @@ fy:
|
||||
past_preamble_html: Wy hawwe ús gebrûksbetingsten wizige sûnt jo lêste besite. Wy rekommandearje jo oan om de bywurke betingsten troch te nimmen.
|
||||
review_link: Gebrûksbetingsten besjen
|
||||
title: De gebrûksbetingsten fan %{domain} wizigje
|
||||
themes:
|
||||
contrast: Mastodon (heech kontrast)
|
||||
default: Mastodon (donker)
|
||||
mastodon-light: Mastodon (ljocht)
|
||||
system: Automatysk (systeemtema brûke)
|
||||
time:
|
||||
formats:
|
||||
default: "%d %B %Y om %H:%M"
|
||||
|
||||
@@ -2155,10 +2155,7 @@ ga:
|
||||
review_link: Athbhreithnigh téarmaí seirbhíse
|
||||
title: Tá téarmaí seirbhíse %{domain} ag athrú
|
||||
themes:
|
||||
contrast: Mastodon (Codarsnacht ard)
|
||||
default: Mastodon (Dorcha)
|
||||
mastodon-light: Mastodon (Solas)
|
||||
system: Uathoibríoch (úsáid téama córais)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
||||
@@ -2103,11 +2103,6 @@ gd:
|
||||
past_preamble_html: Dh’atharraich sinn teirmichean na seirbheise againn on turas mu dheireadh a thadhail thu oirnn. Mholamaid gun dèan thu lèirmheas air na teirmichean ùra.
|
||||
review_link: Dèan lèirmheas air teirmichean na seirbheise
|
||||
title: Tha teirmichean na seirbheise aig %{domain} gu bhith atharrachadh
|
||||
themes:
|
||||
contrast: Mastodon (iomsgaradh àrd)
|
||||
default: Mastodon (dorcha)
|
||||
mastodon-light: Mastodon (soilleir)
|
||||
system: Fèin-obrachail (cleachd ùrlar an t-siostaim)
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
||||
@@ -2021,10 +2021,7 @@ gl:
|
||||
review_link: Revisar as condicións do sevizo
|
||||
title: Cambios nas condicións do servizo de %{domain}
|
||||
themes:
|
||||
contrast: Mastodon (Alto contraste)
|
||||
default: Mastodon (Escuro)
|
||||
mastodon-light: Mastodon (Claro)
|
||||
system: Automático (seguir ao sistema)
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b, %Y, %H:%M"
|
||||
|
||||
@@ -2109,10 +2109,7 @@ he:
|
||||
review_link: קריאת תנאי השירות
|
||||
title: תנאי השירות של %{domain} משתנים
|
||||
themes:
|
||||
contrast: מסטודון (ניגודיות גבוהה)
|
||||
default: מסטודון (כהה)
|
||||
mastodon-light: מסטודון (בהיר)
|
||||
system: אוטומטי (לפי המערכת)
|
||||
default: מסטודון
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
||||
@@ -2020,11 +2020,6 @@ hu:
|
||||
past_preamble_html: A legutóbbi látogatásod óta módosítottunk a felhasználási feltétleinken. Azt javasoljuk, hogy tekintsd át a frissített feltételeket.
|
||||
review_link: Felhasználási feltételek áttekintése
|
||||
title: A(z) %{domain} felhasználási feltételei megváltoznak
|
||||
themes:
|
||||
contrast: Mastodon (nagy kontrasztú)
|
||||
default: Mastodon (sötét)
|
||||
mastodon-light: Mastodon (világos)
|
||||
system: Automatikus (rendszertéma használata)
|
||||
time:
|
||||
formats:
|
||||
default: "%Y. %b %d., %H:%M"
|
||||
|
||||
@@ -786,10 +786,6 @@ hy:
|
||||
'7889238': 3 ամիս
|
||||
stream_entries:
|
||||
sensitive_content: Կասկածելի բովանդակութիւն
|
||||
themes:
|
||||
contrast: Mastodon (բարձր կոնտրաստով)
|
||||
default: Mastodon (Մուգ)
|
||||
mastodon-light: Mastodon (Լուսավոր)
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
||||
@@ -1997,11 +1997,6 @@ ia:
|
||||
past_preamble_html: Nos ha cambiate nostre conditiones de servicio desde tu ultime visita. Nos te invita a revider le conditiones actualisate.
|
||||
review_link: Revider le conditiones de servicio
|
||||
title: Le conditiones de servicio de %{domain} cambia
|
||||
themes:
|
||||
contrast: Mastodon (Alte contrasto)
|
||||
default: Mastodon (Obscur)
|
||||
mastodon-light: Mastodon (Clar)
|
||||
system: Automatic (usar thema del systema)
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user