Merge commit '9e0a3aaf08e7d4fc700f7f19d61f5f8fee346b6a' into glitch-soc/merge-upstream
This commit is contained in:
commit
89c2ac444f
@ -210,7 +210,7 @@ FROM media-build AS libvips
|
||||
|
||||
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
||||
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
||||
ARG VIPS_VERSION=8.18.2
|
||||
ARG VIPS_VERSION=8.18.3
|
||||
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
||||
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ class CollectionsController < ApplicationController
|
||||
|
||||
before_action :require_account_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_collection
|
||||
before_action :redirect_to_canonical_url
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode?
|
||||
@ -18,7 +19,6 @@ class CollectionsController < ApplicationController
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
expires_in expiration_duration, public: true unless user_signed_in?
|
||||
render template: 'home/index'
|
||||
end
|
||||
|
||||
format.json do
|
||||
@ -46,6 +46,10 @@ class CollectionsController < ApplicationController
|
||||
not_found
|
||||
end
|
||||
|
||||
def redirect_to_canonical_url
|
||||
redirect_to collection_path(@collection) if request.format.html? && request.path.starts_with?('/ap/')
|
||||
end
|
||||
|
||||
def expiration_duration
|
||||
recently_updated = @collection.updated_at > 15.minutes.ago
|
||||
recently_updated ? 30.seconds : 5.minutes
|
||||
|
||||
@ -93,7 +93,7 @@ const messages = defineMessages({
|
||||
|
||||
export const ensureComposeIsVisible = (getState) => {
|
||||
if (!getState().getIn(['compose', 'mounted'])) {
|
||||
browserHistory.push('/publish');
|
||||
browserHistory.push('/publish', { focusTarget: false });
|
||||
}
|
||||
};
|
||||
|
||||
@ -294,7 +294,10 @@ export function submitCompose(successCallback) {
|
||||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
dismissAfter: 10000,
|
||||
onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||
onClick: () => browserHistory.push(
|
||||
`/@${response.data.account.username}/${response.data.id}`,
|
||||
{ focusTarget: 'detailed-status' }
|
||||
),
|
||||
}));
|
||||
}).catch(function (error) {
|
||||
dispatch(submitComposeFail(error));
|
||||
@ -341,11 +344,6 @@ export function uploadCompose(files) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getState().getIn(['compose', 'poll'])) {
|
||||
dispatch(showAlert({ message: messages.uploadErrorPoll }));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(uploadComposeRequest());
|
||||
|
||||
for (const [i, file] of Array.from(files).entries()) {
|
||||
|
||||
@ -18,7 +18,7 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) {
|
||||
);
|
||||
|
||||
// If there's a mismatch, re-import all custom emojis.
|
||||
if (existingEmojis.length < emojis.length) {
|
||||
if (existingEmojis.length > 0 && existingEmojis.length < emojis.length) {
|
||||
await clearCache('custom');
|
||||
await loadCustomEmoji();
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ interface OpenModalPayload {
|
||||
modalType: ModalType;
|
||||
modalProps: ModalProps;
|
||||
previousModalProps?: ModalProps;
|
||||
ignoreFocus?: boolean;
|
||||
}
|
||||
export const openModal = createAction<OpenModalPayload>('MODAL_OPEN');
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import { FollowsYouBadge } from '../badge';
|
||||
import { CopyButton } from '../copy_button';
|
||||
import { DisplayName } from '../display_name';
|
||||
import { Icon } from '../icon';
|
||||
import { NavigationFocusTarget } from '../navigation_focus_target';
|
||||
|
||||
import { AccountBadges } from './badges';
|
||||
import classes from './styles.module.scss';
|
||||
@ -56,9 +57,9 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
return (
|
||||
<div className={classes.nameWrapper}>
|
||||
<div className={classes.name}>
|
||||
<h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</h1>
|
||||
</NavigationFocusTarget>
|
||||
{relationship?.followed_by && <FollowsYouBadge />}
|
||||
</div>
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
text-wrap: nowrap;
|
||||
|
||||
@ -19,6 +19,7 @@ import { useIdentity } from 'mastodon/identity_context';
|
||||
import { useColumnIndexContext } from '../features/ui/components/columns_area';
|
||||
import { getColumnSkipLinkId } from '../features/ui/components/skip_links';
|
||||
|
||||
import { NavigationFocusTarget } from './navigation_focus_target';
|
||||
import { useAppHistory } from './router';
|
||||
|
||||
export const messages = defineMessages({
|
||||
@ -272,7 +273,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
{!backButton && hasIcon && (
|
||||
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
|
||||
)}
|
||||
{title}
|
||||
<span className='column-header__text'>{title}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -285,7 +286,10 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
<div className={headingClassName}>
|
||||
{backButton}
|
||||
{hasTitle && (
|
||||
<h1 className='column-header__title-wrapper'>
|
||||
<NavigationFocusTarget
|
||||
as='h1'
|
||||
className='column-header__title-wrapper'
|
||||
>
|
||||
{onClick ? (
|
||||
<button
|
||||
onClick={handleTitleClick}
|
||||
@ -304,7 +308,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
{titleContents}
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
</NavigationFocusTarget>
|
||||
)}
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
|
||||
@ -7,6 +7,7 @@ import { multiply } from 'color-blend';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||
import { IGNORE_FOCUS_ON_OPEN } from '../reducers/modal';
|
||||
|
||||
class ModalRoot extends PureComponent {
|
||||
|
||||
@ -21,7 +22,10 @@ class ModalRoot extends PureComponent {
|
||||
b: PropTypes.number,
|
||||
}),
|
||||
]),
|
||||
ignoreFocus: PropTypes.bool,
|
||||
ignoreFocus: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.string, // 'on-open', see IGNORE_FOCUS_ON_OPEN
|
||||
]),
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
|
||||
@ -118,7 +122,11 @@ class ModalRoot extends PureComponent {
|
||||
_ensureHistoryBuffer () {
|
||||
const { pathname, search, hash, state } = this.history.location;
|
||||
if (!state || state.mastodonModalKey !== this._modalHistoryKey) {
|
||||
this.history.push({ pathname, search, hash }, { ...state, mastodonModalKey: this._modalHistoryKey });
|
||||
this.history.push({ pathname, search, hash }, {
|
||||
...state,
|
||||
focusTarget: this.props.ignoreFocus !== IGNORE_FOCUS_ON_OPEN,
|
||||
mastodonModalKey: this._modalHistoryKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||
|
||||
import type { MastodonLocation } from '../router';
|
||||
|
||||
export const FOCUS_TARGET = {
|
||||
POST: 'detailed-status',
|
||||
} as const;
|
||||
|
||||
export type FocusTarget =
|
||||
| boolean
|
||||
| (typeof FOCUS_TARGET)[keyof typeof FOCUS_TARGET];
|
||||
|
||||
const FocusTargetContext =
|
||||
createContext<React.MutableRefObject<FocusTarget> | null>(null);
|
||||
|
||||
/**
|
||||
* `FocusTargetProvider` keeps track of whether focus should be
|
||||
* set after a navigation. By default, any navigation will set the
|
||||
* current value of `focusTargetRef` to `true`, which will cause
|
||||
* the `NavigationFocusTarget` component to focus itself when it mounts.
|
||||
*
|
||||
* To disable this behaviour for a navigation, the focus target can be
|
||||
* set to `false` using location state, for example:
|
||||
* ```
|
||||
* location.push(url, { focusTarget: false });
|
||||
* ```
|
||||
*
|
||||
* If the target page contains multiple `NavigationFocusTarget` components
|
||||
* (e.g. a main heading and a post that should be focused), give the more
|
||||
* specific `NavigationFocusTarget` instance a name, and pass the same name
|
||||
* via location state:
|
||||
* ```
|
||||
* location.push(url, { focusTarget: 'detailed-status' });
|
||||
* ```
|
||||
*/
|
||||
|
||||
export const FocusTargetProvider: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
const focusTargetRef = useRef<FocusTarget>(false);
|
||||
const previousLocationRef = useRef<
|
||||
| (Pick<MastodonLocation, 'pathname' | 'search'> & {
|
||||
focusTarget?: FocusTarget;
|
||||
})
|
||||
| null
|
||||
>(null);
|
||||
|
||||
const {
|
||||
pathname,
|
||||
search,
|
||||
state = {},
|
||||
} = useLocation<{ focusTarget?: FocusTarget } | undefined>();
|
||||
|
||||
const { focusTarget } = state;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// We never want to set focus on page load, so we keep
|
||||
// track of whether a manual navigation has occurred by comparing
|
||||
// our current with the previous location:
|
||||
const previous = previousLocationRef.current;
|
||||
|
||||
// Bail out on the first render, populate previousLocationRef
|
||||
if (previous === null) {
|
||||
previousLocationRef.current = { pathname, search, focusTarget };
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail out if location hasn't changed
|
||||
if (
|
||||
previous.pathname === pathname &&
|
||||
previous.search === search &&
|
||||
previous.focusTarget === focusTarget
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Location has changed:
|
||||
// - Set focusTarget
|
||||
// – Store current location as previous
|
||||
// (We store `focusTarget` as `false` to allow overriding it.)
|
||||
previousLocationRef.current = { pathname, search, focusTarget: false };
|
||||
focusTargetRef.current = focusTarget ?? true;
|
||||
}, [pathname, search, focusTarget]);
|
||||
|
||||
return (
|
||||
<FocusTargetContext.Provider value={focusTargetRef}>
|
||||
{children}
|
||||
</FocusTargetContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useFocusOnNavigation(targetName?: string) {
|
||||
const focusTargetRef = useContext(FocusTargetContext);
|
||||
|
||||
if (focusTargetRef === null) {
|
||||
throw Error(
|
||||
'useFocusTargetContext must be used inside of a FocusTargetProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return useCallback(
|
||||
(element: HTMLElement | null) => {
|
||||
const focusTarget = focusTargetRef.current;
|
||||
|
||||
// Bail out if focusTarget was set to `false`
|
||||
if (!element || !focusTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusTarget === true || focusTarget === targetName) {
|
||||
setTimeout(() => {
|
||||
element.focus({ preventScroll: true });
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
[focusTargetRef, targetName],
|
||||
);
|
||||
}
|
||||
|
||||
interface FocusTargetElementProps extends React.ComponentPropsWithoutRef<'h1'> {
|
||||
focusTargetName?: string;
|
||||
}
|
||||
|
||||
export const NavigationFocusTarget = polymorphicForwardRef<
|
||||
'h1',
|
||||
FocusTargetElementProps
|
||||
>(({ as: Component = 'h1', focusTargetName, children, ...otherProps }) => {
|
||||
const focusOnNavigation = useFocusOnNavigation(focusTargetName);
|
||||
|
||||
return (
|
||||
<Component ref={focusOnNavigation} tabIndex={-1} {...otherProps}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
@ -14,9 +14,14 @@ import { createBrowserHistory } from 'history';
|
||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||
import { isDevelopment } from 'mastodon/utils/environment';
|
||||
|
||||
import type { FocusTarget } from './navigation_focus_target';
|
||||
|
||||
interface MastodonLocationState {
|
||||
fromMastodon?: boolean;
|
||||
mastodonModalKey?: string;
|
||||
// Controls which element is focused after a navigation.
|
||||
// Set to `false` to prevent navigation focus.
|
||||
focusTarget?: FocusTarget;
|
||||
// Prevent the rightmost column in advanced UI from scrolling
|
||||
// into view on location changes
|
||||
preventMultiColumnAutoScroll?: string;
|
||||
|
||||
@ -33,6 +33,7 @@ import StatusContent from './status_content';
|
||||
import { StatusThreadLabel } from './status_thread_label';
|
||||
import { CollectionPreviewCard } from '../features/collections/components/collection_preview_card';
|
||||
import { compareUrls } from '../utils/compare_urls';
|
||||
import { FOCUS_TARGET } from './navigation_focus_target';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
@ -311,9 +312,9 @@ class Status extends ImmutablePureComponent {
|
||||
window.open(path, '_blank', 'noopener');
|
||||
} else {
|
||||
if (history.location.pathname.replace('/deck/', '/') === path) {
|
||||
history.replace(path);
|
||||
history.replace(path, {focusTarget: FOCUS_TARGET.POST});
|
||||
} else {
|
||||
history.push(path);
|
||||
history.push(path, {focusTarget: FOCUS_TARGET.POST});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { connectUserStream } from 'mastodon/actions/streaming';
|
||||
import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||
import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
import UI from 'mastodon/features/ui';
|
||||
import { IdentityContext, createIdentityContext } from 'mastodon/identity_context';
|
||||
@ -49,7 +50,9 @@ export default class Mastodon extends PureComponent {
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
<ScrollContext>
|
||||
<Route path='/' component={UI} />
|
||||
<FocusTargetProvider>
|
||||
<Route path='/' component={UI} />
|
||||
</FocusTargetProvider>
|
||||
</ScrollContext>
|
||||
<BodyScrollLock />
|
||||
</Router>
|
||||
|
||||
@ -8,10 +8,13 @@ import { Helmet } from '@unhead/react/helmet';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
|
||||
import { injectIntl } from '@/mastodon/components/intl';
|
||||
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
import { LinkFooter} from 'mastodon/features/ui/components/link_footer';
|
||||
@ -91,7 +94,9 @@ class About extends PureComponent {
|
||||
srcSet={Object.keys(server.item?.thumbnail.versions ?? {}).map((key) => `${server.item?.thumbnail.versions && server.item.thumbnail.versions[key]} ${key.replace('@', '')}`).join(', ')}
|
||||
className='about__header__hero'
|
||||
/>
|
||||
<h1>{isLoading ? <Skeleton width='10ch' /> : server.domain}</h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
{isLoading ? <Skeleton width='10ch' /> : domain}
|
||||
</NavigationFocusTarget>
|
||||
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ export const AccountFieldActions: FC<{ id: string }> = ({ id }) => {
|
||||
openModal({
|
||||
modalType: 'ACCOUNT_EDIT_FIELD_EDIT',
|
||||
modalProps: { fieldKey: id },
|
||||
ignoreFocus: true,
|
||||
}),
|
||||
);
|
||||
}, [dispatch, id]);
|
||||
|
||||
@ -137,16 +137,28 @@ export const AccountEdit: FC = () => {
|
||||
);
|
||||
|
||||
const handleOpenModal = useCallback(
|
||||
(type: ModalType, props?: Record<string, unknown>) => {
|
||||
dispatch(openModal({ modalType: type, modalProps: props ?? {} }));
|
||||
(
|
||||
type: ModalType,
|
||||
{
|
||||
modalProps = {},
|
||||
ignoreFocus = false,
|
||||
}: { modalProps?: Record<string, unknown>; ignoreFocus?: boolean } = {},
|
||||
) => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: type,
|
||||
modalProps,
|
||||
ignoreFocus,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
const handleNameEdit = useCallback(() => {
|
||||
handleOpenModal('ACCOUNT_EDIT_NAME');
|
||||
handleOpenModal('ACCOUNT_EDIT_NAME', { ignoreFocus: true });
|
||||
}, [handleOpenModal]);
|
||||
const handleBioEdit = useCallback(() => {
|
||||
handleOpenModal('ACCOUNT_EDIT_BIO');
|
||||
handleOpenModal('ACCOUNT_EDIT_BIO', { ignoreFocus: true });
|
||||
}, [handleOpenModal]);
|
||||
const handleCustomFieldAdd = useCallback(() => {
|
||||
handleOpenModal('ACCOUNT_EDIT_FIELD_EDIT');
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
ModalShellActions,
|
||||
ModalShellBody,
|
||||
} from '@/mastodon/components/modal_shell';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
|
||||
import { useFieldHtml } from '../hooks/useFieldHtml';
|
||||
|
||||
@ -24,12 +25,14 @@ export const AccountFieldModal: FC<{
|
||||
return (
|
||||
<ModalShell>
|
||||
<ModalShellBody>
|
||||
<EmojiHTML
|
||||
as='h2'
|
||||
htmlString={field.name_emojified}
|
||||
onElement={handleLabelElement}
|
||||
className={classes.fieldName}
|
||||
/>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
<EmojiHTML
|
||||
as='span'
|
||||
htmlString={field.name_emojified}
|
||||
onElement={handleLabelElement}
|
||||
className={classes.fieldName}
|
||||
/>
|
||||
</NavigationFocusTarget>
|
||||
<EmojiHTML
|
||||
as='p'
|
||||
htmlString={field.value_emojified}
|
||||
|
||||
@ -7,6 +7,8 @@ import { List, Map } from 'immutable';
|
||||
import { render } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target';
|
||||
import { Router } from '@/mastodon/components/router';
|
||||
import type { RootState } from 'mastodon/store';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
@ -27,9 +29,13 @@ describe('<AltTextModal />', () => {
|
||||
|
||||
const renderComponent = () => {
|
||||
return render(
|
||||
<IntlProvider locale='en' messages={{}}>
|
||||
<AltTextModal mediaId={mediaId} onClose={handleClose} />
|
||||
</IntlProvider>,
|
||||
<Router>
|
||||
<FocusTargetProvider>
|
||||
<IntlProvider locale='en' messages={{}}>
|
||||
<AltTextModal mediaId={mediaId} onClose={handleClose} />
|
||||
</IntlProvider>
|
||||
</FocusTargetProvider>
|
||||
</Router>,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ import { changeUploadCompose } from 'mastodon/actions/compose_typed';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { GIFV } from 'mastodon/components/gifv';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
import { Audio } from 'mastodon/features/audio';
|
||||
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
|
||||
@ -412,12 +413,15 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<NavigationFocusTarget
|
||||
as='h1'
|
||||
className='dialog-modal__header__title'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='alt_text_modal.add_alt_text'
|
||||
defaultMessage='Add alt text'
|
||||
/>
|
||||
</span>
|
||||
</NavigationFocusTarget>
|
||||
|
||||
<Button secondary onClick={onClose}>
|
||||
<FormattedMessage
|
||||
|
||||
@ -10,6 +10,7 @@ import classNames from 'classnames/bind';
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { getReport } from '@/mastodon/reducers/slices/annual_report';
|
||||
import {
|
||||
createAppSelector,
|
||||
@ -83,7 +84,9 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
return (
|
||||
<div className={styles.wrapper} data-color-scheme='dark'>
|
||||
<div className={styles.header}>
|
||||
<h1>Wrapstodon {report.year}</h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
Wrapstodon {report.year}
|
||||
</NavigationFocusTarget>
|
||||
{account && <p>@{account.acct}</p>}
|
||||
{context === 'modal' && (
|
||||
<IconButton
|
||||
|
||||
@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { fetchServer } from 'mastodon/actions/server';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: state.getIn(['server', 'server', 'item', 'registrations', 'message']),
|
||||
@ -42,7 +43,9 @@ class ClosedRegistrationsModal extends ImmutablePureComponent {
|
||||
return (
|
||||
<div className='modal-root__modal interaction-modal'>
|
||||
<div className='interaction-modal__lead'>
|
||||
<h3><FormattedMessage id='closed_registrations_modal.title' defaultMessage='Signing up on Mastodon' /></h3>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
<FormattedMessage id='closed_registrations_modal.title' defaultMessage='Signing up on Mastodon' />
|
||||
</NavigationFocusTarget>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='closed_registrations_modal.preamble'
|
||||
@ -53,12 +56,12 @@ class ClosedRegistrationsModal extends ImmutablePureComponent {
|
||||
|
||||
<div className='interaction-modal__choices'>
|
||||
<div className='interaction-modal__choices__choice'>
|
||||
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
|
||||
<h2><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h2>
|
||||
{closedRegistrationsMessage}
|
||||
</div>
|
||||
|
||||
<div className='interaction-modal__choices__choice'>
|
||||
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
|
||||
<h2><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h2>
|
||||
<p className='prose'>
|
||||
<FormattedMessage
|
||||
id='closed_registrations.other_server_instructions'
|
||||
|
||||
@ -4,6 +4,7 @@ import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import {
|
||||
@ -114,13 +115,17 @@ export const CollectionAdder: React.FC<{
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title' id={titleId}>
|
||||
<NavigationFocusTarget
|
||||
as='h1'
|
||||
id={titleId}
|
||||
className='dialog-modal__header__title'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='collections.add_to_collection'
|
||||
defaultMessage='Add {name} to collections'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</NavigationFocusTarget>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
|
||||
@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { changeCompose, focusCompose } from 'mastodon/actions/compose';
|
||||
@ -66,7 +67,7 @@ export const CollectionShareModal: React.FC<{
|
||||
return (
|
||||
<ModalShell>
|
||||
<ModalShellBody>
|
||||
<h1 className={classes.heading}>
|
||||
<NavigationFocusTarget as='h1' className={classes.heading}>
|
||||
{isNew ? (
|
||||
<FormattedMessage
|
||||
id='collection.share_modal.title_new'
|
||||
@ -78,7 +79,7 @@ export const CollectionShareModal: React.FC<{
|
||||
defaultMessage='Share collection'
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
</NavigationFocusTarget>
|
||||
|
||||
<IconButton
|
||||
title={intl.formatMessage({
|
||||
|
||||
@ -4,6 +4,7 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { DisplayNameSimple } from 'mastodon/components/display_name/simple';
|
||||
@ -71,7 +72,9 @@ export const Collections: React.FC<{
|
||||
|
||||
<Scrollable>
|
||||
<header className={classes.header}>
|
||||
<h1 className={classes.heading}>{pageTitleHtml}</h1>
|
||||
<NavigationFocusTarget as='h1' className={classes.heading}>
|
||||
{pageTitleHtml}
|
||||
</NavigationFocusTarget>
|
||||
<TabList plain>
|
||||
<TabLink exact to={`/@${account?.acct}/collections`}>
|
||||
{intl.formatMessage(createdByTabMessage, {
|
||||
|
||||
@ -220,7 +220,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||
this.textareaRef.current.focus();
|
||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||
if (this.props.spoiler) {
|
||||
const mediaJustAdded = this.props.anyMedia && !prevProps.anyMedia;
|
||||
|
||||
if (this.props.spoiler && !mediaJustAdded) {
|
||||
this.spoilerText.input.focus();
|
||||
} else if (prevProps.spoiler) {
|
||||
this.textareaRef.current.focus();
|
||||
|
||||
@ -4,12 +4,10 @@ import { addPoll, removePoll } from '../../../actions/compose';
|
||||
import PollButton from '../components/poll_button';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const readyAttachmentsSize = state.compose.get('media_attachments').size ?? 0;
|
||||
const hasAttachments = readyAttachmentsSize > 0 || !!state.compose.get('is_uploading');
|
||||
const hasQuote = !!state.compose.get('quoted_status_id');
|
||||
|
||||
return ({
|
||||
disabled: hasAttachments || hasQuote,
|
||||
disabled: hasQuote,
|
||||
active: state.getIn(['compose', 'poll']) !== null,
|
||||
});
|
||||
};
|
||||
|
||||
@ -4,7 +4,6 @@ import { uploadCompose } from '../../../actions/compose';
|
||||
import UploadButton from '../components/upload_button';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const isPoll = state.getIn(['compose', 'poll']) !== null;
|
||||
const isUploading = state.getIn(['compose', 'is_uploading']);
|
||||
const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0;
|
||||
const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0;
|
||||
@ -14,7 +13,7 @@ const mapStateToProps = state => {
|
||||
const hasQuote = !!state.compose.get('quoted_status_id');
|
||||
|
||||
return {
|
||||
disabled: isPoll || isUploading || isOverLimit || hasVideoOrAudio || hasQuote,
|
||||
disabled: isUploading || isOverLimit || hasVideoOrAudio || hasQuote,
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
};
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { fetchServer } from 'mastodon/actions/server';
|
||||
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
||||
import { TabLink, TabList } from 'mastodon/components/tab_list';
|
||||
@ -40,7 +41,9 @@ export const CustomHomepage: React.FC = () => {
|
||||
/>
|
||||
|
||||
<div className={classes.topSection}>
|
||||
<h1>{server.item?.domain}</h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
{server.item?.domain}
|
||||
</NavigationFocusTarget>
|
||||
<p>{server.item?.description}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -22,6 +22,10 @@ function rawEmojiFactory(data: Partial<CompactEmoji> = {}): CompactEmoji {
|
||||
}
|
||||
|
||||
describe('emoji database', () => {
|
||||
beforeEach(async () => {
|
||||
await testGet(); // Loads the database schema.
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testClear();
|
||||
indexedDB = new IDBFactory();
|
||||
|
||||
@ -143,7 +143,28 @@ export async function search({
|
||||
return intersection;
|
||||
})
|
||||
.values(),
|
||||
).toSorted((a, b) => a.score - b.score);
|
||||
);
|
||||
|
||||
// If there are no results, try a cursor-based custom emoji search instead.
|
||||
if (results.length === 0) {
|
||||
const trx = db.transaction('custom', 'readonly');
|
||||
const foundEmojis = new Set<string>();
|
||||
for await (const cursor of trx.store) {
|
||||
const emoji = cursor.value;
|
||||
const score = getScoreForEmoji(emoji, query, false);
|
||||
if (score === null || foundEmojis.has(emoji.shortcode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({ ...emoji, score });
|
||||
foundEmojis.add(emoji.shortcode);
|
||||
}
|
||||
log('cursor search found %d results for "%s"', foundEmojis.size, query);
|
||||
await trx.done;
|
||||
}
|
||||
|
||||
// Sort by score, descending.
|
||||
results.sort((a, b) => a.score - b.score);
|
||||
|
||||
const time = performance.measure('emoji-search-end', 'emoji-search-start');
|
||||
log(
|
||||
@ -159,14 +180,19 @@ export async function search({
|
||||
return results;
|
||||
}
|
||||
|
||||
function getScoreForEmoji(emoji: AnyEmojiData, query: string) {
|
||||
function getScoreForEmoji(
|
||||
emoji: AnyEmojiData,
|
||||
query: string,
|
||||
checkTokens = true,
|
||||
) {
|
||||
const id = 'shortcode' in emoji ? emoji.shortcode : emoji.label;
|
||||
if (id === query) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let index = 1;
|
||||
for (const token of [id, ...emoji.tokens]) {
|
||||
const searchTokens = checkTokens ? [id, ...emoji.tokens] : [id];
|
||||
for (const token of searchTokens) {
|
||||
const tokenIndex = token.indexOf(query);
|
||||
if (tokenIndex !== -1) {
|
||||
return index + tokenIndex / token.length;
|
||||
@ -246,6 +272,13 @@ export async function clearCache(key: CacheKey) {
|
||||
log('Cleared cache for %s', key);
|
||||
}
|
||||
|
||||
export async function resetDatabase() {
|
||||
const db = await loadDB();
|
||||
const storeNames = [...db.objectStoreNames];
|
||||
await Promise.all(storeNames.map((storeName) => db.clear(storeName)));
|
||||
log(storeNames, 'Reset emoji database stores:');
|
||||
}
|
||||
|
||||
export async function loadEmojiByHexcode(
|
||||
hexcode: string,
|
||||
localeString: string,
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
StoreNames,
|
||||
} from 'idb';
|
||||
|
||||
import { resetDatabase } from './database';
|
||||
import type { CustomEmojiData, CacheKey, UnicodeEmojiData } from './types';
|
||||
import { emojiLogger } from './utils';
|
||||
|
||||
@ -57,7 +58,7 @@ type Transaction<Mode extends IDBTransactionMode = 'versionchange'> =
|
||||
|
||||
export type Database = IDBPDatabase<EmojiDB>;
|
||||
|
||||
const SCHEMA_VERSION = 3;
|
||||
const SCHEMA_VERSION = 4;
|
||||
|
||||
export async function openEmojiDB() {
|
||||
const db = await openDB<EmojiDB>('mastodon-emoji', SCHEMA_VERSION, {
|
||||
@ -98,6 +99,8 @@ export async function openEmojiDB() {
|
||||
});
|
||||
deleteOldIndexes(shortcodeTable, ['hexcode']);
|
||||
|
||||
void resetDatabase();
|
||||
|
||||
log(
|
||||
'Upgraded emoji database from version %d to %d',
|
||||
oldVersion,
|
||||
|
||||
@ -57,6 +57,8 @@ export const Emoji: FC<EmojiProps> = ({
|
||||
const { mode } = useEmojiAppState();
|
||||
return (
|
||||
<EmojiRaw
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated -- In React props are frozen, but the library we use is old so doesn't respect that.
|
||||
{...(EmojiRaw.defaultProps ?? {})}
|
||||
data={EmojiData}
|
||||
set={set}
|
||||
sheetSize={sheetSize}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { initialState } from '@/mastodon/initial_state';
|
||||
|
||||
import type { EMOJI_DB_NAME_SHORTCODES } from './constants';
|
||||
import { toSupportedLocale } from './locale';
|
||||
import { reloadCustomEmojis } from './picker';
|
||||
import type { LocaleOrCustom } from './types';
|
||||
import type { EmojiWorkerMessage } from './types';
|
||||
import { emojiLogger } from './utils';
|
||||
|
||||
const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
|
||||
@ -11,9 +10,10 @@ const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
|
||||
let worker: Worker | null = null;
|
||||
|
||||
const log = emojiLogger('index');
|
||||
const workerLog = emojiLogger('worker');
|
||||
|
||||
// This is too short, but better to fallback quickly than wait.
|
||||
const WORKER_TIMEOUT = 1_000;
|
||||
const WORKER_TIMEOUT = 2_000;
|
||||
|
||||
export async function initializeEmoji() {
|
||||
log('initializing emojis');
|
||||
@ -43,43 +43,44 @@ export async function initializeEmoji() {
|
||||
const { data: message } = event;
|
||||
|
||||
worker ??= tempWorker;
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (message === 'ready') {
|
||||
log('worker ready, loading data');
|
||||
clearTimeout(timeoutId);
|
||||
messageWorker('shortcodes');
|
||||
void loadCustomEmoji();
|
||||
void loadEmojiLocale(userLocale);
|
||||
} else {
|
||||
log('got worker message: %s', message);
|
||||
if (message !== 'ready') {
|
||||
workerLog(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const debugValue = localStorage.getItem('debug');
|
||||
if (debugValue) {
|
||||
messageWorker({ type: 'debug', debugValue });
|
||||
}
|
||||
|
||||
workerLog('loading data');
|
||||
messageWorker(userLocale);
|
||||
messageWorker('custom');
|
||||
messageWorker('shortcodes');
|
||||
});
|
||||
}
|
||||
|
||||
async function fallbackLoad() {
|
||||
log('falling back to main thread for loading');
|
||||
|
||||
await loadCustomEmoji();
|
||||
const { importLegacyShortcodes } = await import('./loader');
|
||||
const { importCustomEmojiData, importLegacyShortcodes, importEmojiData } =
|
||||
await import('./loader');
|
||||
|
||||
const customEmojis = await importCustomEmojiData();
|
||||
if (customEmojis && customEmojis.length > 0) {
|
||||
log('loaded %d custom emojis', customEmojis.length);
|
||||
await reloadCustomEmojis();
|
||||
}
|
||||
const shortcodes = await importLegacyShortcodes();
|
||||
if (shortcodes?.length) {
|
||||
log('loaded %d legacy shortcodes', shortcodes.length);
|
||||
}
|
||||
await loadEmojiLocale(userLocale);
|
||||
}
|
||||
|
||||
async function loadEmojiLocale(localeString: string) {
|
||||
const locale = toSupportedLocale(localeString);
|
||||
const { importEmojiData } = await import('./loader');
|
||||
|
||||
if (worker) {
|
||||
log('asking worker to load locale %s', locale);
|
||||
messageWorker(locale);
|
||||
} else {
|
||||
const emojis = await importEmojiData(locale);
|
||||
if (emojis) {
|
||||
log('loaded %d emojis to locale %s', emojis.length, locale);
|
||||
}
|
||||
const emojis = await importEmojiData(userLocale);
|
||||
if (emojis) {
|
||||
log('loaded %d emojis to locale %s', emojis.length, userLocale);
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,11 +97,16 @@ export async function loadCustomEmoji() {
|
||||
}
|
||||
}
|
||||
|
||||
function messageWorker(
|
||||
locale: LocaleOrCustom | typeof EMOJI_DB_NAME_SHORTCODES,
|
||||
) {
|
||||
function messageWorker(data: EmojiWorkerMessage | string) {
|
||||
if (!worker) {
|
||||
return;
|
||||
}
|
||||
worker.postMessage({ locale });
|
||||
if (typeof data === 'string') {
|
||||
worker.postMessage({
|
||||
type: 'load',
|
||||
storeName: data,
|
||||
} satisfies EmojiWorkerMessage);
|
||||
} else {
|
||||
worker.postMessage(data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { basename, resolve } from 'path';
|
||||
import { flattenEmojiData } from 'emojibase';
|
||||
import unicodeRawEmojis from 'emojibase-data/en/data.json';
|
||||
|
||||
import { unicodeToTwemojiHex } from './normalize';
|
||||
import { extractTokens, unicodeToTwemojiHex } from './normalize';
|
||||
|
||||
const emojiSVGFiles = await readdir(
|
||||
// This assumes tests are run from project root
|
||||
@ -33,3 +33,32 @@ describe('unicodeToTwemojiHex', () => {
|
||||
expect(svgFileNamesWithoutBorder).toContain(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTokens', () => {
|
||||
test('returns an empty array for blank input', () => {
|
||||
expect(extractTokens(' ', null)).toEqual([]);
|
||||
});
|
||||
|
||||
test('check token word breaking with Intl.Segmenter', () => {
|
||||
const segmenter = new Intl.Segmenter('en', { granularity: 'word' });
|
||||
|
||||
expect(
|
||||
extractTokens('thumbs_up smiling-face camelCase', segmenter),
|
||||
).toEqual(['thumbs', 'up', 'smiling', 'face', 'camel', 'case']);
|
||||
});
|
||||
|
||||
test('check token word breaking with regex', () => {
|
||||
expect(extractTokens('Smile_face joy-test A ok 7 z', null)).toEqual([
|
||||
'smile',
|
||||
'face',
|
||||
'joy',
|
||||
'test',
|
||||
'ok',
|
||||
]);
|
||||
});
|
||||
|
||||
test('ensure +1 and -1 are preserved', () => {
|
||||
expect(extractTokens('+1', null)).toEqual(['+1']);
|
||||
expect(extractTokens('-1', null)).toEqual(['-1']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -214,6 +214,11 @@ export function extractTokens(
|
||||
}
|
||||
const tokens: string[] = [];
|
||||
|
||||
// Handle the edge case of thumbs up and down emoticons.
|
||||
if (input === '+1' || input === '-1') {
|
||||
return [input];
|
||||
}
|
||||
|
||||
// Prefer to use Intl.Segmenter if available for better locale support.
|
||||
if (segmenter) {
|
||||
for (const { isWordLike, segment } of segmenter.segment(
|
||||
|
||||
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
||||
import type { CategoryName, CustomEmoji } from 'emoji-mart';
|
||||
|
||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||
import { createLimitedCache } from '@/mastodon/utils/cache';
|
||||
|
||||
import { emojiLogger } from './utils';
|
||||
|
||||
@ -21,6 +22,8 @@ let customCategories = [
|
||||
'flags',
|
||||
] as CategoryName[];
|
||||
|
||||
const searchCache = createLimitedCache<LegacyEmoji[]>({ maxSize: 10, log });
|
||||
|
||||
export async function fetchCustomEmojiData() {
|
||||
if (customEmojis !== null) {
|
||||
return customEmojis;
|
||||
@ -89,6 +92,7 @@ export async function reloadCustomEmojis() {
|
||||
await import('@/mastodon/hooks/useCustomEmojis');
|
||||
|
||||
await Promise.all([fetchCustomEmojiData(), loadEmojisIntoCache()]);
|
||||
searchCache.clear();
|
||||
}
|
||||
|
||||
// Replicates the old legacy search function.
|
||||
@ -102,16 +106,25 @@ export async function emojiMartSearch(
|
||||
return [];
|
||||
}
|
||||
|
||||
const cacheKey = `${query}|${locale}|${limit}`;
|
||||
const cachedResult = searchCache.get(cacheKey);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const { search } = await import('./database');
|
||||
const results = await search({ query, locale, limit });
|
||||
return results.map((emoji) =>
|
||||
const legacyResults = results.map((emoji) =>
|
||||
'shortcode' in emoji
|
||||
? { id: emoji.shortcode, custom: true }
|
||||
? ({ id: emoji.shortcode, custom: true } as const)
|
||||
: {
|
||||
id: emoji.label.replaceAll(' ', '_').toLowerCase(),
|
||||
native: emoji.unicode,
|
||||
},
|
||||
);
|
||||
searchCache.set(cacheKey, legacyResults);
|
||||
|
||||
return legacyResults;
|
||||
}
|
||||
|
||||
export function usePickerEmojis() {
|
||||
|
||||
@ -80,3 +80,13 @@ export type ExtraCustomEmojiMap = Record<
|
||||
string,
|
||||
Pick<CustomEmojiData, 'shortcode' | 'static_url' | 'url'>
|
||||
>;
|
||||
|
||||
export type EmojiWorkerMessage =
|
||||
| {
|
||||
type: 'load';
|
||||
storeName: string;
|
||||
}
|
||||
| {
|
||||
type: 'debug';
|
||||
debugValue: string;
|
||||
};
|
||||
|
||||
@ -5,6 +5,9 @@ import { emojiRegexPolyfill } from '@/mastodon/polyfills';
|
||||
import { VARIATION_SELECTOR_CODE } from './constants';
|
||||
|
||||
export function emojiLogger(segment: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return debug(`emojis:worker:${segment}`);
|
||||
}
|
||||
return debug(`emojis:${segment}`);
|
||||
}
|
||||
|
||||
|
||||
@ -1,31 +1,36 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { EMOJI_DB_NAME_SHORTCODES, EMOJI_TYPE_CUSTOM } from './constants';
|
||||
import {
|
||||
importCustomEmojiData,
|
||||
importEmojiData,
|
||||
importLegacyShortcodes,
|
||||
} from './loader';
|
||||
import type { EmojiWorkerMessage } from './types';
|
||||
|
||||
addEventListener('message', handleMessage);
|
||||
self.postMessage('ready'); // After the worker is ready, notify the main thread
|
||||
|
||||
function handleMessage(event: MessageEvent<{ locale: string }>) {
|
||||
const {
|
||||
data: { locale },
|
||||
} = event;
|
||||
void loadData(locale);
|
||||
function handleMessage(event: MessageEvent<EmojiWorkerMessage>) {
|
||||
const { data } = event;
|
||||
if (data.type === 'debug') {
|
||||
debug.enable(data.debugValue);
|
||||
} else {
|
||||
void loadData(data.storeName);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData(locale: string) {
|
||||
async function loadData(storeName: string) {
|
||||
let importCount: number | undefined;
|
||||
if (locale === EMOJI_TYPE_CUSTOM) {
|
||||
if (storeName === EMOJI_TYPE_CUSTOM) {
|
||||
importCount = (await importCustomEmojiData())?.length;
|
||||
} else if (locale === EMOJI_DB_NAME_SHORTCODES) {
|
||||
} else if (storeName === EMOJI_DB_NAME_SHORTCODES) {
|
||||
importCount = (await importLegacyShortcodes())?.length;
|
||||
} else {
|
||||
importCount = (await importEmojiData(locale))?.length;
|
||||
importCount = (await importEmojiData(storeName))?.length;
|
||||
}
|
||||
|
||||
if (importCount) {
|
||||
self.postMessage(`loaded ${importCount} emojis into ${locale}`);
|
||||
self.postMessage(`loaded ${importCount} emojis into ${storeName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { toServerSideType } from 'mastodon/utils/filters';
|
||||
|
||||
const mapStateToProps = (state, { filterId }) => ({
|
||||
@ -71,7 +72,9 @@ class AddedToFilter extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='filter_modal.added.title' defaultMessage='Filter added!' /></h3>
|
||||
<NavigationFocusTarget as='h1' className='report-dialog-modal__title'>
|
||||
<FormattedMessage id='filter_modal.added.title' defaultMessage='Filter added!' />
|
||||
</NavigationFocusTarget>
|
||||
<p className='report-dialog-modal__lead'>
|
||||
<FormattedMessage
|
||||
id='filter_modal.added.short_explanation'
|
||||
|
||||
@ -10,6 +10,7 @@ import fuzzysort from 'fuzzysort';
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { injectIntl } from '@/mastodon/components/intl';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { toServerSideType } from 'mastodon/utils/filters';
|
||||
import { loupeIcon, deleteIcon } from 'mastodon/utils/icons';
|
||||
|
||||
@ -176,7 +177,9 @@ class SelectFilter extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='filter_modal.select_filter.title' defaultMessage='Filter this post' /></h3>
|
||||
<NavigationFocusTarget as='h1' className='report-dialog-modal__title'>
|
||||
<FormattedMessage id='filter_modal.select_filter.title' defaultMessage='Filter this post' />
|
||||
</NavigationFocusTarget>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='filter_modal.select_filter.subtitle' defaultMessage='Use an existing category or create a new one' /></p>
|
||||
|
||||
<div className='emoji-mart-search'>
|
||||
|
||||
@ -8,6 +8,7 @@ import { escapeRegExp } from 'lodash';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { DisplayName } from '@/mastodon/components/display_name';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { openModal, closeModal } from 'mastodon/actions/modal';
|
||||
import { apiRequest } from 'mastodon/api';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
@ -474,12 +475,12 @@ const InteractionModal: React.FC<{
|
||||
return (
|
||||
<div className='modal-root__modal interaction-modal'>
|
||||
<div className='interaction-modal__lead'>
|
||||
<h3>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
<FormattedMessage
|
||||
id='interaction_modal.title'
|
||||
defaultMessage='Sign in to continue'
|
||||
/>
|
||||
</h3>
|
||||
</NavigationFocusTarget>
|
||||
<p>
|
||||
{intent === 'follow' ? (
|
||||
<FormattedMessage
|
||||
|
||||
@ -18,6 +18,7 @@ import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
@ -182,13 +183,13 @@ const ListAdder: React.FC<{
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<NavigationFocusTarget as='h1' className='dialog-modal__header__title'>
|
||||
<FormattedMessage
|
||||
id='lists.add_to_lists'
|
||||
defaultMessage='Add {name} to lists'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</NavigationFocusTarget>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
|
||||
@ -8,6 +8,7 @@ import type { List as ImmutableList, RecordOf } from 'immutable';
|
||||
|
||||
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
import { FOCUS_TARGET } from '@/mastodon/components/navigation_focus_target';
|
||||
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
@ -67,7 +68,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
||||
const path = `/@${account.acct}/${statusId}`;
|
||||
|
||||
if (button === 0 && !(ctrlKey || metaKey)) {
|
||||
history.push(path);
|
||||
history.push(path, { focusTarget: FOCUS_TARGET.POST });
|
||||
} else if (button === 1 || (button === 0 && (ctrlKey || metaKey))) {
|
||||
window.open(path, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
@ -4,12 +4,15 @@ import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { apiGetPrivacyPolicy } from 'mastodon/api/instance';
|
||||
import type { ApiPrivacyPolicyJSON } from 'mastodon/api_types/instance';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
|
||||
import { getColumnSkipLinkId } from '../ui/components/skip_links';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' },
|
||||
});
|
||||
@ -40,12 +43,12 @@ const PrivacyPolicy: React.FC<{
|
||||
>
|
||||
<div className='scrollable privacy-policy'>
|
||||
<div className='column-title'>
|
||||
<h3>
|
||||
<NavigationFocusTarget as='h1' id={getColumnSkipLinkId(1)}>
|
||||
<FormattedMessage
|
||||
id='privacy_policy.title'
|
||||
defaultMessage='Privacy Policy'
|
||||
/>
|
||||
</h3>
|
||||
</NavigationFocusTarget>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='privacy_policy.last_updated'
|
||||
|
||||
@ -3,11 +3,10 @@ import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { injectIntl } from '@/mastodon/components/intl';
|
||||
|
||||
import Option from './components/option';
|
||||
@ -84,7 +83,9 @@ class Category extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} /></h3>
|
||||
<NavigationFocusTarget as='h1' className='report-dialog-modal__title'>
|
||||
<FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} />
|
||||
</NavigationFocusTarget>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.category.subtitle' defaultMessage='Choose the best match' /></p>
|
||||
|
||||
<div>
|
||||
|
||||
@ -7,6 +7,7 @@ import { OrderedSet } from 'immutable';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { fetchAccount } from 'mastodon/actions/accounts';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
@ -148,14 +149,18 @@ const Comment: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title' id={titleId}>
|
||||
<NavigationFocusTarget
|
||||
as='h1'
|
||||
id={titleId}
|
||||
className='report-dialog-modal__title'
|
||||
>
|
||||
{modalTitle ?? (
|
||||
<FormattedMessage
|
||||
id='report.comment.title'
|
||||
defaultMessage='Is there anything else you think we should know?'
|
||||
/>
|
||||
)}
|
||||
</h3>
|
||||
</NavigationFocusTarget>
|
||||
|
||||
<textarea
|
||||
className='report-dialog-modal__textarea'
|
||||
|
||||
@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
|
||||
import Option from './components/option';
|
||||
|
||||
@ -40,7 +41,9 @@ class Rules extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' /></h3>
|
||||
<NavigationFocusTarget as='h1' className='report-dialog-modal__title'>
|
||||
<FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' />
|
||||
</NavigationFocusTarget>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.rules.subtitle' defaultMessage='Select all that apply' /></p>
|
||||
|
||||
<div>
|
||||
|
||||
@ -10,6 +10,7 @@ import { connect } from 'react-redux';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import StatusCheckBox from 'mastodon/features/report/containers/status_check_box_container';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])),
|
||||
@ -37,7 +38,9 @@ class Statuses extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' /></h3>
|
||||
<NavigationFocusTarget as='h1' className='report-dialog-modal__title'>
|
||||
<FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' />
|
||||
</NavigationFocusTarget>
|
||||
<p className='report-dialog-modal__lead'><FormattedMessage id='report.statuses.subtitle' defaultMessage='Select all that apply' /></p>
|
||||
|
||||
<div className='report-dialog-modal__statuses'>
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
blockAccount,
|
||||
} from 'mastodon/actions/accounts';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
@ -52,7 +53,9 @@ class Thanks extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title'>{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}</h3>
|
||||
<NavigationFocusTarget as='h1' className='report-dialog-modal__title'>
|
||||
{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}
|
||||
</NavigationFocusTarget>
|
||||
<p className='report-dialog-modal__lead'>{submitted ? <FormattedMessage id='report.thanks.take_action_actionable' defaultMessage='While we review this, you can take action against @{name}:' values={{ name: account.get('username') }} /> : <FormattedMessage id='report.thanks.take_action' defaultMessage='Here are your options for controlling what you see on Mastodon:' />}</p>
|
||||
|
||||
{account.getIn(['relationship', 'following']) && (
|
||||
|
||||
@ -72,6 +72,7 @@ import ActionBar from './components/action_bar';
|
||||
import { DetailedStatus } from './components/detailed_status';
|
||||
import { RefreshController } from './components/refresh_controller';
|
||||
import { quoteComposeById } from '@/mastodon/actions/compose_typed';
|
||||
import { FOCUS_TARGET, NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
|
||||
const messages = defineMessages({
|
||||
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
|
||||
@ -585,7 +586,13 @@ class Status extends ImmutablePureComponent {
|
||||
{ancestors}
|
||||
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader({intl, status})} ref={this.setStatusRef}>
|
||||
<NavigationFocusTarget
|
||||
as='div'
|
||||
focusTargetName={FOCUS_TARGET.POST}
|
||||
className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)}
|
||||
tabIndex={0}
|
||||
aria-label={textForScreenReader({intl, status})} ref={this.setStatusRef}
|
||||
>
|
||||
<DetailedStatus
|
||||
key={`details-${status.get('id')}`}
|
||||
status={status}
|
||||
@ -626,7 +633,7 @@ class Status extends ImmutablePureComponent {
|
||||
onPin={this.handlePin}
|
||||
onEmbed={this.handleEmbed}
|
||||
/>
|
||||
</div>
|
||||
</NavigationFocusTarget>
|
||||
</Hotkeys>
|
||||
|
||||
{descendants}
|
||||
|
||||
@ -11,11 +11,14 @@ import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { apiGetTermsOfService } from 'mastodon/api/instance';
|
||||
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { BundleColumnError } from 'mastodon/features/ui/components/bundle_column_error';
|
||||
|
||||
import { getColumnSkipLinkId } from '../ui/components/skip_links';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
|
||||
});
|
||||
@ -55,12 +58,12 @@ const TermsOfService: React.FC<{
|
||||
>
|
||||
<div className='scrollable privacy-policy'>
|
||||
<div className='column-title'>
|
||||
<h3>
|
||||
<NavigationFocusTarget as='h1' id={getColumnSkipLinkId(1)}>
|
||||
<FormattedMessage
|
||||
id='terms_of_service.title'
|
||||
defaultMessage='Terms of Service'
|
||||
/>
|
||||
</h3>
|
||||
</NavigationFocusTarget>
|
||||
<p className='prose'>
|
||||
{response?.effective ? (
|
||||
<FormattedMessage
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { render, fireEvent, screen } from '@/testing/rendering';
|
||||
|
||||
import Column from '../column';
|
||||
import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target';
|
||||
|
||||
const fakeIcon = () => <span />;
|
||||
|
||||
@ -9,9 +10,11 @@ describe('<Column />', () => {
|
||||
it('runs the scroll animation if the column contains scrollable content', () => {
|
||||
const scrollToMock = vi.fn();
|
||||
const { container } = render(
|
||||
<Column heading='notifications' icon='notifications' iconComponent={fakeIcon}>
|
||||
<div className='scrollable' />
|
||||
</Column>,
|
||||
<FocusTargetProvider>
|
||||
<Column heading='notifications' icon='notifications' iconComponent={fakeIcon}>
|
||||
<div className='scrollable' />
|
||||
</Column>
|
||||
</FocusTargetProvider>,
|
||||
);
|
||||
container.querySelector('.scrollable').scrollTo = scrollToMock;
|
||||
fireEvent.click(screen.getByText('notifications'));
|
||||
@ -19,7 +22,11 @@ describe('<Column />', () => {
|
||||
});
|
||||
|
||||
it('does not try to scroll if there is no scrollable content', () => {
|
||||
render(<Column heading='notifications' icon='notifications' iconComponent={fakeIcon} />);
|
||||
render(
|
||||
<FocusTargetProvider>
|
||||
<Column heading='notifications' icon='notifications' iconComponent={fakeIcon} />
|
||||
</FocusTargetProvider>
|
||||
);
|
||||
fireEvent.click(screen.getByText('notifications'));
|
||||
});
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
|
||||
import { blockAccount } from 'mastodon/actions/accounts';
|
||||
import { closeModal } from 'mastodon/actions/modal';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
@ -46,7 +47,9 @@ export const BlockModal = ({ accountId, acct }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1><FormattedMessage id='block_modal.title' defaultMessage='Block user?' /></h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
<FormattedMessage id='block_modal.title' defaultMessage='Block user?' />
|
||||
</NavigationFocusTarget>
|
||||
<p>@{acct}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
|
||||
import { EmbeddedStatus } from 'mastodon/features/notifications_v2/components/embedded_status';
|
||||
import type { Status, StatusVisibility } from 'mastodon/models/status';
|
||||
@ -68,7 +69,7 @@ export const BoostModal: React.FC<{
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
{status.get('reblogged') ? (
|
||||
<FormattedMessage
|
||||
id='boost_modal.undo_reblog'
|
||||
@ -80,7 +81,7 @@ export const BoostModal: React.FC<{
|
||||
defaultMessage='Boost post?'
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
</NavigationFocusTarget>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='boost_modal.combo'
|
||||
|
||||
@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import {
|
||||
ModalShell,
|
||||
@ -67,7 +68,13 @@ export const ConfirmationModal: React.FC<
|
||||
return (
|
||||
<ModalShell onSubmit={handleClick}>
|
||||
<ModalShellBody className={className}>
|
||||
<h1 id={titleId}>{title}</h1>
|
||||
{noFocusButton ? (
|
||||
<NavigationFocusTarget as='h1' id={titleId}>
|
||||
{title}
|
||||
</NavigationFocusTarget>
|
||||
) : (
|
||||
<h1>{title}</h1>
|
||||
)}
|
||||
{message && <p>{message}</p>}
|
||||
|
||||
{extraContent ?? children}
|
||||
|
||||
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
||||
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
export type { BaseConfirmationModalProps as DialogModalProps } from './confirmation_modals/confirmation_modal';
|
||||
@ -47,7 +48,9 @@ export const DialogModal: FC<DialogModalProps> = ({
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<h1 className='dialog-modal__header__title'>{title}</h1>
|
||||
<NavigationFocusTarget as='h1' className='dialog-modal__header__title'>
|
||||
{title}
|
||||
</NavigationFocusTarget>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
|
||||
@ -15,6 +15,7 @@ import { apiRequest } from 'mastodon/api';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
@ -77,12 +78,12 @@ export const DomainBlockModal: React.FC<{
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
<FormattedMessage
|
||||
id='domain_block_modal.title'
|
||||
defaultMessage='Block domain?'
|
||||
/>
|
||||
</h1>
|
||||
</NavigationFocusTarget>
|
||||
<p>{domain}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,7 @@ import { showAlertForError } from 'mastodon/actions/alerts';
|
||||
import api from 'mastodon/api';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CopyPasteText } from 'mastodon/components/copy_paste_text';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
interface OEmbedResponse {
|
||||
@ -76,9 +77,9 @@ const EmbedModal: React.FC<{
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id='report.close' defaultMessage='Done' />
|
||||
</Button>
|
||||
<span className='dialog-modal__header__title'>
|
||||
<NavigationFocusTarget as='h1' className='dialog-modal__header__title'>
|
||||
<FormattedMessage id='status.embed' defaultMessage='Get embed code' />
|
||||
</span>
|
||||
</NavigationFocusTarget>
|
||||
<Button secondary onClick={onClose}>
|
||||
<FormattedMessage
|
||||
id='confirmation_modal.cancel'
|
||||
|
||||
@ -12,6 +12,7 @@ import { closeModal } from 'mastodon/actions/modal';
|
||||
import { updateNotificationsPolicy } from 'mastodon/actions/notification_policies';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
|
||||
export const IgnoreNotificationsModal = ({ filterType }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -57,7 +58,7 @@ export const IgnoreNotificationsModal = ({ filterType }) => {
|
||||
<div className='modal-root__modal safety-action-modal'>
|
||||
<div className='safety-action-modal__top'>
|
||||
<div className='safety-action-modal__header'>
|
||||
<h1>{title}</h1>
|
||||
<NavigationFocusTarget as='h1'>{title}</NavigationFocusTarget>
|
||||
</div>
|
||||
|
||||
<ul className='safety-action-modal__bullet-points'>
|
||||
|
||||
@ -18,6 +18,7 @@ import { closeModal } from 'mastodon/actions/modal';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { RadioButton } from 'mastodon/components/radio_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -84,7 +85,9 @@ export const MuteModal = ({ accountId, acct }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1><FormattedMessage id='mute_modal.title' defaultMessage='Mute user?' /></h1>
|
||||
<NavigationFocusTarget as='h1'>
|
||||
<FormattedMessage id='mute_modal.title' defaultMessage='Mute user?' />
|
||||
</NavigationFocusTarget>
|
||||
<p>@{acct}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -9,6 +9,7 @@ import { fetchServer } from 'mastodon/actions/server';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
@ -23,12 +24,12 @@ const CollectionThanks: React.FC<{
|
||||
}> = ({ onClose }) => {
|
||||
return (
|
||||
<>
|
||||
<h3 className='report-dialog-modal__title'>
|
||||
<NavigationFocusTarget as='h1' className='report-dialog-modal__title'>
|
||||
<FormattedMessage
|
||||
id='report.thanks.title_actionable'
|
||||
defaultMessage="Thanks for reporting, we'll look into this."
|
||||
/>
|
||||
</h3>
|
||||
</NavigationFocusTarget>
|
||||
|
||||
<div className='flex-spacer' />
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import { Button } from '@/mastodon/components/button';
|
||||
import { Dropdown } from '@/mastodon/components/dropdown';
|
||||
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target';
|
||||
import { messages as privacyMessages } from '@/mastodon/features/compose/components/privacy_dropdown';
|
||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
@ -217,14 +218,15 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='visibility_modal.header'
|
||||
defaultMessage='Visibility and interaction'
|
||||
<NavigationFocusTarget
|
||||
as='h1'
|
||||
className='dialog-modal__header__title'
|
||||
>
|
||||
{(chunks) => (
|
||||
<span className='dialog-modal__header__title'>{chunks}</span>
|
||||
)}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage
|
||||
id='visibility_modal.header'
|
||||
defaultMessage='Visibility and interaction'
|
||||
/>
|
||||
</NavigationFocusTarget>
|
||||
</div>
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='dialog-modal__content__description'>
|
||||
|
||||
@ -187,7 +187,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<ColumnsContextProvider multiColumn={!singleColumn}>
|
||||
<ColumnsArea ref={this.setRef} singleColumn={singleColumn} domain={domain} minimalShell={!signedIn && landingPage === 'overview'}>
|
||||
<WrappedSwitch>
|
||||
<Redirect from='/' to={{pathname: rootRedirect, state: this.props.location.state}} exact />
|
||||
<Redirect from='/' to={{pathname: rootRedirect, state: {...this.props.location.state, focusTarget: false}}} exact />
|
||||
|
||||
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
||||
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={{...this.props.location, pathname: pathName.slice(5)}} /> : null}
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Прагледзець абнаўленні",
|
||||
"home.pending_critical_update.title": "Даступна крытычнае абнаўленне бяспекі!",
|
||||
"home.show_announcements": "Паказаць аб'явы",
|
||||
"ignore_notifications_modal.bots_title": "Ігнараваць апавяшчэнні ад ботаў?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon не можа паведамляць карыстальнікам, што Вы праігнаравалі апавяшчэнні ад іх. Ігнараванне апавяшчэнняў не спыніць адпраўку саміх паведамленняў.",
|
||||
"ignore_notifications_modal.filter_instead": "Замест гэтага адфільтраваць",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Вы па-ранейшаму зможаце прымаць, адхіляць ці скардзіцца на карыстальнікаў",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Iгнараваць",
|
||||
"notifications.policy.drop_hint": "Адправіць у бездань, адкуль больш ніколі не ўбачыце",
|
||||
"notifications.policy.filter": "Фільтраваць",
|
||||
"notifications.policy.filter_bots_hint": "Уліковыя запісы, пазначаныя як аўтаматычныя",
|
||||
"notifications.policy.filter_bots_title": "Боты",
|
||||
"notifications.policy.filter_hint": "Адправіць у скрыню адфільтраваных апавяшчэнняў",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Абмежавана мадэратарамі сервера",
|
||||
"notifications.policy.filter_limited_accounts_title": "Уліковыя запісы пад мадэрацыяй",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Se opdateringer",
|
||||
"home.pending_critical_update.title": "Kritisk sikkerhedsopdatering tilgængelig!",
|
||||
"home.show_announcements": "Vis bekendtgørelser",
|
||||
"ignore_notifications_modal.bots_title": "Ignorér notifikationer fra bots?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon kan ikke informere brugere om, at du har ignoreret deres notifikationer. At ignorere notifikationer forhindrer ikke selve beskederne i at blive sendt.",
|
||||
"ignore_notifications_modal.filter_instead": "Filtrér i stedet",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Du vil stadig kunne acceptere, afvise eller anmelde brugere",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignorér",
|
||||
"notifications.policy.drop_hint": "Send til intetheden, for aldrig at blive set igen",
|
||||
"notifications.policy.filter": "Filter",
|
||||
"notifications.policy.filter_bots_hint": "Konti markeret som automatiseret",
|
||||
"notifications.policy.filter_bots_title": "Bots",
|
||||
"notifications.policy.filter_hint": "Send til filtrerede notifikationsindbakke",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Begrænset af servermoderatorer",
|
||||
"notifications.policy.filter_limited_accounts_title": "Modererede konti",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Updates ansehen",
|
||||
"home.pending_critical_update.title": "Kritisches Sicherheitsupdate verfügbar!",
|
||||
"home.show_announcements": "Ankündigungen anzeigen",
|
||||
"ignore_notifications_modal.bots_title": "Benachrichtigungen von Bots ignorieren?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon kann anderen Nutzer*innen nicht mitteilen, dass du deren Benachrichtigungen ignorierst. Das Ignorieren von Benachrichtigungen wird nicht das Absenden der Nachricht selbst unterbinden.",
|
||||
"ignore_notifications_modal.filter_instead": "Stattdessen filtern",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Du wirst weiterhin die Möglichkeit haben, andere Nutzer*innen zu akzeptieren, abzulehnen oder zu melden",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignorieren",
|
||||
"notifications.policy.drop_hint": "Ins Nirwana befördern und auf Nimmerwiedersehen!",
|
||||
"notifications.policy.filter": "Filtern",
|
||||
"notifications.policy.filter_bots_hint": "Als automatisiert gekennzeichnete Konten",
|
||||
"notifications.policy.filter_bots_title": "Bots",
|
||||
"notifications.policy.filter_hint": "Im separaten Feed „Gefilterte Benachrichtigungen“ anzeigen",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Durch Server-Moderator*innen eingeschränkte Profile",
|
||||
"notifications.policy.filter_limited_accounts_title": "eingeschränkten Konten",
|
||||
|
||||
@ -577,7 +577,7 @@
|
||||
"content_warning.show_more": "Εμφάνιση περισσότερων",
|
||||
"content_warning.show_short": "Εμφάνιση",
|
||||
"conversation.delete": "Διαγραφή συνομιλίας",
|
||||
"conversation.mark_as_read": "Σήμανση ως αναγνωσμένη",
|
||||
"conversation.mark_as_read": "Σήμανση ως διαβασμένη",
|
||||
"conversation.open": "Προβολή συνομιλίας",
|
||||
"conversation.with": "Με {names}",
|
||||
"copy_icon_button.copied": "Αντιγράφηκε στο πρόχειρο",
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Δείτε ενημερώσεις",
|
||||
"home.pending_critical_update.title": "Κρίσιμη ενημέρωση ασφαλείας διαθέσιμη!",
|
||||
"home.show_announcements": "Εμφάνιση ανακοινώσεων",
|
||||
"ignore_notifications_modal.bots_title": "Αγνόηση ειδοποιήσεων από bots;",
|
||||
"ignore_notifications_modal.disclaimer": "Το Mastodon δε μπορεί να ενημερώσει τους χρήστες ότι αγνόησες τις ειδοποιήσεις του. Η αγνόηση ειδοποιήσεων δεν θα εμποδίσει την αποστολή των ίδιων των μηνυμάτων.",
|
||||
"ignore_notifications_modal.filter_instead": "Φίλτρο αντ' αυτού",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Θα μπορείς ακόμη να αποδεχθείς, να απορρίψεις ή να αναφέρεις χρήστες",
|
||||
@ -1011,41 +1012,43 @@
|
||||
"notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου",
|
||||
"notifications.column_settings.follow": "Νέοι ακόλουθοι:",
|
||||
"notifications.column_settings.follow_request": "Νέο αίτημα ακολούθησης:",
|
||||
"notifications.column_settings.group": "Ομάδα",
|
||||
"notifications.column_settings.group": "Ομαδοποίηση",
|
||||
"notifications.column_settings.mention": "Επισημάνσεις:",
|
||||
"notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:",
|
||||
"notifications.column_settings.push": "Ειδοποιήσεις Push",
|
||||
"notifications.column_settings.quote": "Παραθέσεις:",
|
||||
"notifications.column_settings.reblog": "Ενισχύσεις:",
|
||||
"notifications.column_settings.show": "Εμφάνισε σε στήλη",
|
||||
"notifications.column_settings.show": "Εμφάνιση σε στήλη",
|
||||
"notifications.column_settings.sound": "Αναπαραγωγή ήχου",
|
||||
"notifications.column_settings.status": "Νέες αναρτήσεις:",
|
||||
"notifications.column_settings.unread_notifications.category": "Μη αναγνωσμένες ειδοποιήσεις",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Επισήμανση μη αναγνωσμένων ειδοποιήσεων",
|
||||
"notifications.column_settings.unread_notifications.category": "Αδιάβαστες ειδοποιήσεις",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Επισήμανση αδιάβαστων ειδοποιήσεων",
|
||||
"notifications.column_settings.update": "Επεξεργασίες:",
|
||||
"notifications.filter.all": "Όλες",
|
||||
"notifications.filter.boosts": "Προωθήσεις",
|
||||
"notifications.filter.collections": "Συλλογές",
|
||||
"notifications.filter.favourites": "Αγαπημένα",
|
||||
"notifications.filter.follows": "Ακολουθείς",
|
||||
"notifications.filter.follows": "Νέοι ακόλουθοι",
|
||||
"notifications.filter.mentions": "Επισημάνσεις",
|
||||
"notifications.filter.polls": "Αποτελέσματα δημοσκόπησης",
|
||||
"notifications.filter.statuses": "Ενημερώσεις από όσους ακολουθείς",
|
||||
"notifications.grant_permission": "Χορήγηση άδειας.",
|
||||
"notifications.group": "{count} ειδοποιήσεις",
|
||||
"notifications.mark_as_read": "Σήμανε όλες τις ειδοποιήσεις ως αναγνωσμένες",
|
||||
"notifications.mark_as_read": "Σήμανε όλες τις ειδοποιήσεις ως διαβασμένες",
|
||||
"notifications.permission_denied": "Οι ειδοποιήσεις για υπολογιστή δεν είναι διαθέσιμες διότι έχει απορριφθεί κάποιο προηγούμενο αίτημα άδειας",
|
||||
"notifications.permission_denied_alert": "Δεν είναι δυνατή η ενεργοποίηση των ειδοποιήσεων για υπολογιστή, καθώς η άδεια του προγράμματος περιήγησης έχει απορριφθεί προηγουμένων",
|
||||
"notifications.permission_denied_alert": "Δεν είναι δυνατή η ενεργοποίηση των ειδοποιήσεων για υπολογιστή, καθώς η άδεια του προγράμματος περιήγησης έχει απορριφθεί προηγουμένως",
|
||||
"notifications.permission_required": "Οι ειδοποιήσεις για υπολογιστή δεν είναι διαθέσιμες επειδή δεν έχει δοθεί η απαιτούμενη άδεια.",
|
||||
"notifications.policy.accept": "Αποδοχή",
|
||||
"notifications.policy.accept_hint": "Εμφάνιση στις ειδοποιήσεις",
|
||||
"notifications.policy.drop": "Αγνόηση",
|
||||
"notifications.policy.drop_hint": "Στείλε τες στο υπερπέραν, για να μην τις ξαναδείτε",
|
||||
"notifications.policy.filter": "Φίλτρο",
|
||||
"notifications.policy.drop_hint": "Στείλε τες στο υπερπέραν, για να μην τις ξαναδείς",
|
||||
"notifications.policy.filter": "Φιλτράρισμα",
|
||||
"notifications.policy.filter_bots_hint": "Λογαριασμοί σημασμένοι ως αυτοματοποιημένοι",
|
||||
"notifications.policy.filter_bots_title": "Bots",
|
||||
"notifications.policy.filter_hint": "Αποστολή στα εισερχόμενα φιλτραρισμένων ειδοποιήσεων",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Περιορισμένη από συντονιστές διακομιστή",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Περιορισμένοι από συντονιστές διακομιστή",
|
||||
"notifications.policy.filter_limited_accounts_title": "Συντονισμένοι λογαριασμοί",
|
||||
"notifications.policy.filter_new_accounts.hint": "Δημιουργήθηκε εντός {days, plural, one {της τελευταίας ημέρας} other {των τελευταίων # ημερών}}",
|
||||
"notifications.policy.filter_new_accounts.hint": "Δημιουργημένοι εντός {days, plural, one {της τελευταίας ημέρας} other {των τελευταίων # ημερών}}",
|
||||
"notifications.policy.filter_new_accounts_title": "Νέοι λογαριασμοί",
|
||||
"notifications.policy.filter_not_followers_hint": "Συμπεριλαμβανομένων των ατόμων που σας έχουν ακολουθήσει λιγότερο από {days, plural, one {μια ημέρα} other {# ημέρες}} πριν",
|
||||
"notifications.policy.filter_not_followers_title": "Άτομα που δε σε ακολουθούν",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Ver actualizaciones",
|
||||
"home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!",
|
||||
"home.show_announcements": "Mostrar anuncios",
|
||||
"ignore_notifications_modal.bots_title": "¿Ignorar notificaciones de bots?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios que ignoraste sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.",
|
||||
"ignore_notifications_modal.filter_instead": "Filtrar en vez de ignorar",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o denunciar a usuarios",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignorar",
|
||||
"notifications.policy.drop_hint": "Enviar al vacío, no volver a mostrar nunca",
|
||||
"notifications.policy.filter": "Filtrar",
|
||||
"notifications.policy.filter_bots_hint": "Cuentas marcadas como automatizadas",
|
||||
"notifications.policy.filter_bots_title": "Botes",
|
||||
"notifications.policy.filter_hint": "Enviar a la bandeja de entrada de notificaciones filtradas",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Limitada por los moderadores del servidor",
|
||||
"notifications.policy.filter_limited_accounts_title": "Cuentas moderadas",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Ver actualizaciones",
|
||||
"home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!",
|
||||
"home.show_announcements": "Mostrar anuncios",
|
||||
"ignore_notifications_modal.bots_title": "¿Ignorar notificaciones de bots?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.",
|
||||
"ignore_notifications_modal.filter_instead": "Filtrar en su lugar",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o reportar usuarios",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignorar",
|
||||
"notifications.policy.drop_hint": "Enviar al vacío, no volver a mostrar nunca",
|
||||
"notifications.policy.filter": "Filtrar",
|
||||
"notifications.policy.filter_bots_hint": "Cuentas etiquetadas como automatizadas",
|
||||
"notifications.policy.filter_bots_title": "Bots",
|
||||
"notifications.policy.filter_hint": "Enviar a la bandeja de entrada de notificaciones filtradas",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Limitadas por los moderadores del servidor",
|
||||
"notifications.policy.filter_limited_accounts_title": "Cuentas moderadas",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Ver actualizaciones",
|
||||
"home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!",
|
||||
"home.show_announcements": "Mostrar comunicaciones",
|
||||
"ignore_notifications_modal.bots_title": "¿Ignorar notificaciones de bots?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios de que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.",
|
||||
"ignore_notifications_modal.filter_instead": "Filtrar en vez de ignorar",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o reportar usuarios",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignorar",
|
||||
"notifications.policy.drop_hint": "Enviar al vacío, no volver a mostrar nunca",
|
||||
"notifications.policy.filter": "Filtrar",
|
||||
"notifications.policy.filter_bots_hint": "Cuentas etiquetadas como automatizadas",
|
||||
"notifications.policy.filter_bots_title": "Bots",
|
||||
"notifications.policy.filter_hint": "Enviar a la bandeja de entrada de notificaciones filtradas",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Limitadas por los moderadores del servidor",
|
||||
"notifications.policy.filter_limited_accounts_title": "Cuentas moderadas",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Tutustu päivityssisältöihin",
|
||||
"home.pending_critical_update.title": "Kriittinen tietoturvapäivitys saatavilla!",
|
||||
"home.show_announcements": "Näytä tiedotteet",
|
||||
"ignore_notifications_modal.bots_title": "Sivuutetaanko ilmoitukset boteilta?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon ei voi ilmoittaa käyttäjille, että olet sivuuttanut heidän ilmoituksensa. Ilmoitusten sivuuttaminen ei lopeta itse viestien lähetystä.",
|
||||
"ignore_notifications_modal.filter_instead": "Suodata sen sijaan",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Voit kuitenkin yhä hyväksyä, hylätä tai raportoida käyttäjiä",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Sivuuta",
|
||||
"notifications.policy.drop_hint": "Lähetä tyhjyyteen, jotta et näe niitä enää koskaan",
|
||||
"notifications.policy.filter": "Suodata",
|
||||
"notifications.policy.filter_bots_hint": "Automatisoiduiksi merkityt tilit",
|
||||
"notifications.policy.filter_bots_title": "Botit",
|
||||
"notifications.policy.filter_hint": "Lähetä suodatettuihin ilmoituksiin",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Palvelimen moderaattorien rajoittamat",
|
||||
"notifications.policy.filter_limited_accounts_title": "Moderoidut tilit",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Féach nuashonruithe",
|
||||
"home.pending_critical_update.title": "Nuashonrú slándála ríthábhachtach ar fáil!",
|
||||
"home.show_announcements": "Taispeáin fógraí",
|
||||
"ignore_notifications_modal.bots_title": "Neamhaird a dhéanamh d’fhógraí ó bhotaí?",
|
||||
"ignore_notifications_modal.disclaimer": "Ní féidir le Mastodon úsáideoirí a chur ar an eolas gur thug tú neamhaird dá bhfógraí. Má dhéantar neamhaird de fhógraí, ní stopfar na teachtaireachtaí iad féin a sheoladh.",
|
||||
"ignore_notifications_modal.filter_instead": "Scag ina ionad sin",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Beidh tú fós in ann glacadh le húsáideoirí, iad a dhiúltú nó a thuairisciú",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Déan neamhaird de",
|
||||
"notifications.policy.drop_hint": "Seol chuig an neamhní, gan a bheith le feiceáil arís",
|
||||
"notifications.policy.filter": "Scagaire",
|
||||
"notifications.policy.filter_bots_hint": "Cuntais atá marcáilte mar uathoibrithe",
|
||||
"notifications.policy.filter_bots_title": "Botanna",
|
||||
"notifications.policy.filter_hint": "Seol chuig an mbosca isteach fógraí scagtha",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Teoranta ag modhnóirí freastalaí",
|
||||
"notifications.policy.filter_limited_accounts_title": "Cuntais mhodhnaithe",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Mira as actualizacións",
|
||||
"home.pending_critical_update.title": "Hai una actualización crítica de seguridade!",
|
||||
"home.show_announcements": "Amosar anuncios",
|
||||
"ignore_notifications_modal.bots_title": "Ignorar as notificacións dos robots?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon non pode informar ás usuarias de que ignoraches as súas notificacións. Ao ignorar as notificacións non evitarás que as mensaxes sexan enviadas igualmente.",
|
||||
"ignore_notifications_modal.filter_instead": "Filtrar igualmente",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Poderás seguir aceptando, rexeitando e denunciando usuarias",
|
||||
@ -846,6 +847,7 @@
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Para amosar/agochar contido multimedia",
|
||||
"keyboard_shortcuts.toot": "Para escribir unha nova publicación",
|
||||
"keyboard_shortcuts.top": "Mover arriba de todo",
|
||||
"keyboard_shortcuts.translate": "Traducir unha publicación",
|
||||
"keyboard_shortcuts.unfocus": "Para deixar de destacar a área de escritura/procura",
|
||||
"keyboard_shortcuts.up": "Para mover cara arriba na listaxe",
|
||||
"learn_more_link.got_it": "Entendo",
|
||||
@ -1041,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignorar",
|
||||
"notifications.policy.drop_hint": "Esquecer isto, non volver a velo",
|
||||
"notifications.policy.filter": "Filtrar",
|
||||
"notifications.policy.filter_bots_hint": "Contas marcadas como automatizadas",
|
||||
"notifications.policy.filter_bots_title": "Robots",
|
||||
"notifications.policy.filter_hint": "Enviar á caixa de notificacións filtradas",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Limitada pola moderación do servidor",
|
||||
"notifications.policy.filter_limited_accounts_title": "Contas moderadas",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "צפיה בעדכונים",
|
||||
"home.pending_critical_update.title": "יצא עדכון אבטחה חשוב!",
|
||||
"home.show_announcements": "הצג הכרזות",
|
||||
"ignore_notifications_modal.bots_title": "להתעלם מהתראות מחשבונות רובוטיים?",
|
||||
"ignore_notifications_modal.disclaimer": "מסטודון אינו יכול ליידע משתמשים שהתעלמתם מהתראותיהם. התעלמות מהתראות לא תחסום את ההודעות עצמן מלהשלח.",
|
||||
"ignore_notifications_modal.filter_instead": "לסנן במקום",
|
||||
"ignore_notifications_modal.filter_to_act_users": "עדיין ביכולתך לקבל, לדחות ולדווח על משתמשים אחרים",
|
||||
@ -846,6 +847,7 @@
|
||||
"keyboard_shortcuts.toggle_sensitivity": "הצגת/הסתרת מדיה",
|
||||
"keyboard_shortcuts.toot": "להתחיל חיצרוץ חדש",
|
||||
"keyboard_shortcuts.top": "העברה לראש הרשימה",
|
||||
"keyboard_shortcuts.translate": "לתרגם הודעה",
|
||||
"keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש",
|
||||
"keyboard_shortcuts.up": "לנוע במעלה הרשימה",
|
||||
"learn_more_link.got_it": "הבנתי",
|
||||
@ -1041,6 +1043,8 @@
|
||||
"notifications.policy.drop": "להתעלם",
|
||||
"notifications.policy.drop_hint": "שליחה אל מצולות הנשיה, ולא יוודעו אודותיה לעולם",
|
||||
"notifications.policy.filter": "מסנן",
|
||||
"notifications.policy.filter_bots_hint": "חשבונות המסומנים כאוטומטיים",
|
||||
"notifications.policy.filter_bots_title": "בוטים",
|
||||
"notifications.policy.filter_hint": "שליחה לתיבה נכנסת מסוננת",
|
||||
"notifications.policy.filter_limited_accounts_hint": "הוגבל על ידי מנהלי הדיונים",
|
||||
"notifications.policy.filter_limited_accounts_title": "חשבומות תחת ניהול תוכן",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Skoða uppfærslur",
|
||||
"home.pending_critical_update.title": "Áríðandi öryggisuppfærsla er tiltæk!",
|
||||
"home.show_announcements": "Birta auglýsingar",
|
||||
"ignore_notifications_modal.bots_title": "Hunsa tilkynningar frá yrkjum?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon getur ekki upplýst notendur um að þú hunsir tilkynningar frá þeim. Hunsun tilkynninga kemur ekki í veg fyrir að sjálf skilaboðin verði send.",
|
||||
"ignore_notifications_modal.filter_instead": "Sía frekar",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Þú munt áfram geta samþykkt, hafnað eða kært notendur",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Hunsa",
|
||||
"notifications.policy.drop_hint": "Senda út í tómið, svo það sjáist aldrei framar",
|
||||
"notifications.policy.filter": "Sía",
|
||||
"notifications.policy.filter_bots_hint": "Aðgangar merktir fyrir yrki",
|
||||
"notifications.policy.filter_bots_title": "Yrki",
|
||||
"notifications.policy.filter_hint": "Senda í pósthólf fyrir síaðar tilkynningar",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Takmarkað af umsjónarmönnum netþjóns",
|
||||
"notifications.policy.filter_limited_accounts_title": "Aðgangar í umsjón",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Visualizza aggiornamenti",
|
||||
"home.pending_critical_update.title": "Aggiornamento critico di sicurezza disponibile!",
|
||||
"home.show_announcements": "Mostra annunci",
|
||||
"ignore_notifications_modal.bots_title": "Ignorare le notifiche dai bot?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon non può informare gli utenti che hai ignorato le loro notifiche. Ignorare le notifiche non impedirà l'invio dei messaggi stessi.",
|
||||
"ignore_notifications_modal.filter_instead": "Filtra invece",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Potrai comunque accettare, rifiutare o segnalare gli utenti",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignora",
|
||||
"notifications.policy.drop_hint": "Scarta definitivamente, per non essere mai più visto",
|
||||
"notifications.policy.filter": "Filtrare",
|
||||
"notifications.policy.filter_bots_hint": "Account contrassegnati come automatizzati",
|
||||
"notifications.policy.filter_bots_title": "Bot",
|
||||
"notifications.policy.filter_hint": "Invia alla casella in arrivo delle notifiche filtrate",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Limitato dai moderatori del server",
|
||||
"notifications.policy.filter_limited_accounts_title": "Account moderati",
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"account.featured": "Wyróżnione",
|
||||
"account.featured.accounts": "Profile",
|
||||
"account.featured.collections": "Kolekcje",
|
||||
"account.featured.new_collection": "Nowa kolekcja",
|
||||
"account.field_overflow": "Pokaż całą zawartość",
|
||||
"account.filters.all": "Wszystkie aktywności",
|
||||
"account.filters.boosts_toggle": "Pokaż ulepszenia",
|
||||
@ -68,9 +69,13 @@
|
||||
"account.go_to_profile": "Przejdź do profilu",
|
||||
"account.hide_reblogs": "Ukryj podbicia od @{name}",
|
||||
"account.in_memoriam": "Ku pamięci.",
|
||||
"account.join_modal.day": "Dzień",
|
||||
"account.join_modal.me": "Dołączyłeś(aś) na {server}",
|
||||
"account.join_modal.me_today": "To Twój pierwszy dzień na {server}!",
|
||||
"account.join_modal.other": "{name} dołączył(a) na {server}",
|
||||
"account.join_modal.share.celebrate": "Udostępnij uroczysty post",
|
||||
"account.join_modal.share.intro": "Udostępnij post wprowadzający",
|
||||
"account.join_modal.share.welcome": "Udostępnij post powitalny",
|
||||
"account.joined_short": "Dołączył(a)",
|
||||
"account.languages": "Zmień subskrybowane języki",
|
||||
"account.last_active": "Ostatnia aktywność",
|
||||
@ -120,6 +125,7 @@
|
||||
"account.note.edit_button": "Edytuj",
|
||||
"account.note.title": "Osobista notatka (widoczna tylko dla Ciebie)",
|
||||
"account.open_original_page": "Otwórz stronę oryginalną",
|
||||
"account.pending": "Oczekuje",
|
||||
"account.posts": "Wpisy",
|
||||
"account.remove_from_followers": "Usuń {name} z obserwujących",
|
||||
"account.report": "Zgłoś @{name}",
|
||||
@ -173,9 +179,12 @@
|
||||
"account_edit.field_edit_modal.discard_confirm": "Odrzuć",
|
||||
"account_edit.field_edit_modal.discard_message": "Masz niezapisane zmiany. Czy na pewno chcesz je odrzucić?",
|
||||
"account_edit.field_edit_modal.edit_title": "Edytuj dodatkowe pole",
|
||||
"account_edit.field_edit_modal.name_hint": "Np. Strona osobista",
|
||||
"account_edit.field_edit_modal.name_label": "Etykieta",
|
||||
"account_edit.field_edit_modal.value_label": "Wartość",
|
||||
"account_edit.image_alt_modal.add_title": "Dodaj tekst alternatywny",
|
||||
"account_edit.image_alt_modal.edit_title": "Edytuj tekst alternatywny",
|
||||
"account_edit.image_alt_modal.text_hint": "Tekst alternatywny pomaga użytkownikom czytników ekranu zrozumieć twoją treść.",
|
||||
"account_edit.image_alt_modal.text_label": "Tekst alternatywny",
|
||||
"account_edit.image_delete_modal.delete_button": "Usuń",
|
||||
"account_edit.image_delete_modal.title": "Usunąć obraz?",
|
||||
@ -185,6 +194,8 @@
|
||||
"account_edit.image_edit.remove_button": "Usuń obraz",
|
||||
"account_edit.image_edit.replace_button": "Zastąp obraz",
|
||||
"account_edit.item_list.delete": "Usuń {name}",
|
||||
"account_edit.name_modal.add_title": "Dodaj nazwę wyświetlaną",
|
||||
"account_edit.name_modal.edit_title": "Edytuj nazwę wyświetlaną",
|
||||
"account_edit.profile_tab.button_label": "Dostosuj",
|
||||
"account_edit.profile_tab.hint.description": "Te ustawienia wpływają na to, co użytkownicy widzą na {server} w oficjalnych aplikacjach, ale mogą nie obowiązywać na innych serwerach i w aplikacjach zewnętrznych.",
|
||||
"account_edit.profile_tab.hint.title": "Wyświetlanie może się różnić",
|
||||
@ -204,12 +215,14 @@
|
||||
"account_edit.upload_modal.next": "Następne",
|
||||
"account_edit.upload_modal.step_crop.zoom": "Powiększenie",
|
||||
"account_edit.upload_modal.step_upload.button": "Wybierz pliki",
|
||||
"account_edit.upload_modal.step_upload.dragging": "Upuść, aby przesłać",
|
||||
"account_edit.upload_modal.step_upload.header": "Wybierz obraz",
|
||||
"account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF lub JPG, maksymalnie {limit} MB.{br}Obraz zostanie przeskalowany do {width}x{height} px.",
|
||||
"account_edit.upload_modal.title_add.avatar": "Dodaj zdjęcie profilowe",
|
||||
"account_edit.upload_modal.title_add.header": "Dodaj zdjęcie nagłówka",
|
||||
"account_edit.upload_modal.title_replace.avatar": "Zmień zdjęcie profilowe",
|
||||
"account_edit.upload_modal.title_replace.header": "Zmień zdjęcie nagłówka",
|
||||
"account_edit.verified_modal.invisible_link.summary": "Co zrobić, aby link był niewidoczny?",
|
||||
"account_edit_tags.add_tag": "Dodaj #{tagName}",
|
||||
"account_edit_tags.search_placeholder": "Dodaj hashtag...",
|
||||
"admin.dashboard.daily_retention": "Wskaźnik utrzymania użytkowników według dni od rejestracji",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Ver atualizações",
|
||||
"home.pending_critical_update.title": "Atualização de segurança crítica disponível!",
|
||||
"home.show_announcements": "Mostrar anúncios",
|
||||
"ignore_notifications_modal.bots_title": "Ignorar notificações de robôs?",
|
||||
"ignore_notifications_modal.disclaimer": "O Mastodon não informa os usuários se você ignorar as notificações deles. Ignorar notificações não impedirá as mensagens de serem enviadas.",
|
||||
"ignore_notifications_modal.filter_instead": "Filtrar em vez disso",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Você ainda poderá aceitar, rejeitar ou denunciar",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Ignorar",
|
||||
"notifications.policy.drop_hint": "Enviar para o vácuo, para nunca mais ser visto",
|
||||
"notifications.policy.filter": "Filtrar",
|
||||
"notifications.policy.filter_bots_hint": "Contas marcadas como automáticas",
|
||||
"notifications.policy.filter_bots_title": "Robôs",
|
||||
"notifications.policy.filter_hint": "Enviar para a caixa de notificações filtradas",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Limitado pelos moderadores do servidor",
|
||||
"notifications.policy.filter_limited_accounts_title": "Contas moderadas",
|
||||
|
||||
@ -841,6 +841,7 @@
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Për shfaqje/fshehje mediash",
|
||||
"keyboard_shortcuts.toot": "Për të filluar një mesazh të ri",
|
||||
"keyboard_shortcuts.top": "Shpjere në krye të listës",
|
||||
"keyboard_shortcuts.translate": "Përkthe një postim",
|
||||
"keyboard_shortcuts.unfocus": "Për heqjen e fokusit nga fusha e hartimit të mesazheve apo kërkimeve",
|
||||
"keyboard_shortcuts.up": "Për ngjitje sipër nëpër listë",
|
||||
"learn_more_link.got_it": "E mora vesh",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "Xem bản cập nhật",
|
||||
"home.pending_critical_update.title": "Có bản cập nhật bảo mật quan trọng!",
|
||||
"home.show_announcements": "Xem thông báo máy chủ",
|
||||
"ignore_notifications_modal.bots_title": "Bỏ qua thông báo từ các tài khoản bot?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon sẽ không thông báo cho tài khoản rằng bạn đã bỏ qua thông báo của họ. Họ sẽ vẫn có thể tương tác với bạn.",
|
||||
"ignore_notifications_modal.filter_instead": "Lọc thay thế",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Bạn vẫn có thể chấp nhận, từ chối hoặc báo cáo tài khoản khác",
|
||||
@ -846,6 +847,7 @@
|
||||
"keyboard_shortcuts.toggle_sensitivity": "ẩn/hiện ảnh hoặc video",
|
||||
"keyboard_shortcuts.toot": "soạn tút mới",
|
||||
"keyboard_shortcuts.top": "di chuyển đến đầu danh sách",
|
||||
"keyboard_shortcuts.translate": "Dịch tút",
|
||||
"keyboard_shortcuts.unfocus": "đưa con trỏ ra khỏi ô soạn thảo hoặc ô tìm kiếm",
|
||||
"keyboard_shortcuts.up": "di chuyển lên trên danh sách",
|
||||
"learn_more_link.got_it": "Đã hiểu",
|
||||
@ -1041,6 +1043,8 @@
|
||||
"notifications.policy.drop": "Bỏ qua",
|
||||
"notifications.policy.drop_hint": "Bỏ qua vĩnh viễn",
|
||||
"notifications.policy.filter": "Lọc",
|
||||
"notifications.policy.filter_bots_hint": "Những tài khoản đánh dấu là tự động",
|
||||
"notifications.policy.filter_bots_title": "Bot",
|
||||
"notifications.policy.filter_hint": "Cho vào mục thông báo đã lọc",
|
||||
"notifications.policy.filter_limited_accounts_hint": "Chỉ dành cho kiểm duyệt viên",
|
||||
"notifications.policy.filter_limited_accounts_title": "Kiểm duyệt tài khoản",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "查看更新",
|
||||
"home.pending_critical_update.title": "有紧急安全更新!",
|
||||
"home.show_announcements": "显示公告",
|
||||
"ignore_notifications_modal.bots_title": "是否忽略来自机器人账号的通知?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon无法通知对方用户你忽略了他们的通知。忽略通知不会阻止消息本身的发送。",
|
||||
"ignore_notifications_modal.filter_instead": "改为过滤",
|
||||
"ignore_notifications_modal.filter_to_act_users": "你仍然可以接受、拒绝或举报用户",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "忽略",
|
||||
"notifications.policy.drop_hint": "送入虚空,再也不查看",
|
||||
"notifications.policy.filter": "过滤",
|
||||
"notifications.policy.filter_bots_hint": "被标记为自动化的账号",
|
||||
"notifications.policy.filter_bots_title": "机器人",
|
||||
"notifications.policy.filter_hint": "发送到被过滤通知列表",
|
||||
"notifications.policy.filter_limited_accounts_hint": "被服务器管理员限制的账号",
|
||||
"notifications.policy.filter_limited_accounts_title": "受限账号",
|
||||
|
||||
@ -780,6 +780,7 @@
|
||||
"home.pending_critical_update.link": "檢視更新內容",
|
||||
"home.pending_critical_update.title": "有可取得的重要安全性更新!",
|
||||
"home.show_announcements": "顯示公告",
|
||||
"ignore_notifications_modal.bots_title": "是否忽略來自機器人帳號之推播通知?",
|
||||
"ignore_notifications_modal.disclaimer": "Mastodon 無法通知您已忽略推播通知之使用者。忽略通知不會阻止訊息本身的發送。",
|
||||
"ignore_notifications_modal.filter_instead": "改為過濾",
|
||||
"ignore_notifications_modal.filter_to_act_users": "您仍能接受、拒絕、或檢舉使用者",
|
||||
@ -1042,6 +1043,8 @@
|
||||
"notifications.policy.drop": "忽略",
|
||||
"notifications.policy.drop_hint": "送至黑洞,永不相見",
|
||||
"notifications.policy.filter": "過濾器",
|
||||
"notifications.policy.filter_bots_hint": "自動化帳號",
|
||||
"notifications.policy.filter_bots_title": "機器人",
|
||||
"notifications.policy.filter_hint": "送至已過濾推播通知收件夾",
|
||||
"notifications.policy.filter_limited_accounts_hint": "已被伺服器管理員限制",
|
||||
"notifications.policy.filter_limited_accounts_title": "受管制帳號",
|
||||
|
||||
@ -155,6 +155,10 @@ function appendMedia(state, media, file) {
|
||||
|
||||
if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) {
|
||||
map.set('sensitive', true);
|
||||
|
||||
if (state.get('default_sensitive')) {
|
||||
map.set('spoiler', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -400,7 +404,7 @@ export const composeReducer = (state = initialState, action) => {
|
||||
map.set('spoiler', !state.get('spoiler'));
|
||||
map.set('idempotencyKey', uuid());
|
||||
|
||||
if (state.get('media_attachments').size >= 1 && !state.get('default_sensitive')) {
|
||||
if (state.get('media_attachments').size >= 1) {
|
||||
map.set('sensitive', !state.get('spoiler'));
|
||||
}
|
||||
});
|
||||
@ -545,7 +549,7 @@ export const composeReducer = (state = initialState, action) => {
|
||||
map.set('spoiler', true);
|
||||
map.set('spoiler_text', action.status.get('spoiler_text'));
|
||||
} else {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler', action.status.get('sensitive') && action.status.get('media_attachments').size > 0);
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
|
||||
@ -582,7 +586,7 @@ export const composeReducer = (state = initialState, action) => {
|
||||
map.set('spoiler', true);
|
||||
map.set('spoiler_text', action.spoiler_text);
|
||||
} else {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler', action.status.get('sensitive') && action.status.get('media_attachments').size > 0);
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
|
||||
|
||||
@ -17,8 +17,10 @@ const Modal = ImmutableRecord<Modal>({
|
||||
modalProps: ImmutableRecord({})(),
|
||||
});
|
||||
|
||||
export const IGNORE_FOCUS_ON_OPEN = 'on-open';
|
||||
|
||||
interface ModalState {
|
||||
ignoreFocus: boolean;
|
||||
ignoreFocus: boolean | typeof IGNORE_FOCUS_ON_OPEN;
|
||||
stack: Stack<ImmutableRecord<Modal>>;
|
||||
}
|
||||
|
||||
@ -53,9 +55,10 @@ const pushModal = (
|
||||
modalType: ModalType,
|
||||
modalProps: ModalProps,
|
||||
previousModalProps?: ModalProps,
|
||||
ignoreFocusOnOpen = false,
|
||||
): State => {
|
||||
return state.withMutations((record) => {
|
||||
record.set('ignoreFocus', false);
|
||||
record.set('ignoreFocus', ignoreFocusOnOpen ? IGNORE_FOCUS_ON_OPEN : false);
|
||||
record.update('stack', (stack) => {
|
||||
let tmp = stack;
|
||||
|
||||
@ -92,6 +95,7 @@ export const modalReducer: Reducer<State> = (state = initialState, action) => {
|
||||
action.payload.modalType,
|
||||
action.payload.modalProps,
|
||||
action.payload.previousModalProps,
|
||||
action.payload.ignoreFocus,
|
||||
);
|
||||
else if (closeModal.match(action)) return popModal(state, action.payload);
|
||||
// TODO: type those actions
|
||||
|
||||
@ -40,11 +40,13 @@ describe('createCache', () => {
|
||||
});
|
||||
|
||||
test('removes oldest item cached if it exceeds a set size', () => {
|
||||
const cache = createLimitedCache({ maxSize: 1 });
|
||||
const cache = createLimitedCache({ maxSize: 2 });
|
||||
cache.set('test1', 1);
|
||||
cache.set('test2', 2);
|
||||
cache.set('test3', 3);
|
||||
expect(cache.get('test1')).toBeUndefined();
|
||||
expect(cache.get('test2')).toBe(2);
|
||||
expect(cache.get('test3')).toBe(3);
|
||||
});
|
||||
|
||||
test('retrieving a value bumps up last access', () => {
|
||||
@ -63,13 +65,13 @@ describe('createCache', () => {
|
||||
const cache = createLimitedCache({ maxSize: 1, log });
|
||||
cache.set('test1', 1);
|
||||
expect(log).toHaveBeenLastCalledWith(
|
||||
'Added %s to cache, now size %d',
|
||||
'Added %o to cache, now size %d',
|
||||
'test1',
|
||||
1,
|
||||
);
|
||||
cache.set('test2', 1);
|
||||
expect(log).toHaveBeenLastCalledWith(
|
||||
'Added %s and deleted %s from cache, now size %d',
|
||||
'Added %o and deleted %o from cache, now size %d',
|
||||
'test2',
|
||||
'test1',
|
||||
1,
|
||||
|
||||
@ -43,13 +43,13 @@ export function createLimitedCache<CacheValue, CacheKey = string>({
|
||||
cacheMap.delete(lastKey);
|
||||
cacheKeys.delete(lastKey);
|
||||
log(
|
||||
'Added %s and deleted %s from cache, now size %d',
|
||||
'Added %o and deleted %o from cache, now size %d',
|
||||
key,
|
||||
lastKey,
|
||||
cacheMap.size,
|
||||
);
|
||||
} else {
|
||||
log('Added %s to cache, now size %d', key, cacheMap.size);
|
||||
log('Added %o to cache, now size %d', key, cacheMap.size);
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
|
||||
@ -3590,6 +3590,8 @@ a.account__display-name {
|
||||
text-align: center;
|
||||
padding-bottom: 32px;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
line-height: 1.5;
|
||||
@ -4641,6 +4643,7 @@ a.status-card {
|
||||
&__title-wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@ -4656,9 +4659,7 @@ a.status-card {
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
|
||||
&--with-back-button {
|
||||
padding-inline-start: 0;
|
||||
@ -4673,6 +4674,12 @@ a.status-card {
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.column-header__back-button {
|
||||
flex: 1;
|
||||
color: var(--color-text-brand);
|
||||
@ -9141,6 +9148,8 @@ noscript {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-size: 22px;
|
||||
line-height: 33px;
|
||||
@ -9171,6 +9180,8 @@ noscript {
|
||||
&__lead {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
@ -9246,6 +9257,8 @@ noscript {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@ -39,8 +39,8 @@ class ReportFilter
|
||||
end
|
||||
|
||||
def status_scope
|
||||
resolved = params.key?(:resolved)
|
||||
unresolved = params.key?(:unresolved)
|
||||
resolved = ActiveModel::Type::Boolean.new.cast(params[:resolved])
|
||||
unresolved = ActiveModel::Type::Boolean.new.cast(params[:unresolved])
|
||||
|
||||
return Report.all if resolved && unresolved
|
||||
return Report.resolved if resolved
|
||||
|
||||
@ -44,6 +44,16 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
end
|
||||
end
|
||||
|
||||
class ImageWithDescription < SimpleDelegator
|
||||
attr_reader :description
|
||||
|
||||
def initialize(object, description)
|
||||
super(object)
|
||||
|
||||
@description = description
|
||||
end
|
||||
end
|
||||
|
||||
has_one :endpoints, serializer: EndpointsSerializer
|
||||
|
||||
has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
|
||||
@ -120,11 +130,11 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
end
|
||||
|
||||
def icon
|
||||
object.avatar
|
||||
ImageWithDescription.new(object.avatar, object.avatar_description)
|
||||
end
|
||||
|
||||
def image
|
||||
object.header
|
||||
ImageWithDescription.new(object.header, object.header_description)
|
||||
end
|
||||
|
||||
def public_key
|
||||
|
||||
@ -7,6 +7,7 @@ class ActivityPub::ImageSerializer < ActivityPub::Serializer
|
||||
|
||||
attributes :type, :media_type, :url
|
||||
attribute :focal_point, if: :focal_point?
|
||||
attribute :summary, if: :summary?
|
||||
|
||||
def type
|
||||
'Image'
|
||||
@ -27,4 +28,12 @@ class ActivityPub::ImageSerializer < ActivityPub::Serializer
|
||||
def focal_point
|
||||
[object.meta['focus']['x'], object.meta['focus']['y']]
|
||||
end
|
||||
|
||||
def summary?
|
||||
object.respond_to?(:description) && object.description.present?
|
||||
end
|
||||
|
||||
def summary
|
||||
object.description
|
||||
end
|
||||
end
|
||||
|
||||
@ -206,7 +206,7 @@ class PostStatusService < BaseService
|
||||
return
|
||||
end
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > Status::MEDIA_ATTACHMENTS_LIMIT || @options[:poll].present?
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > Status::MEDIA_ATTACHMENTS_LIMIT
|
||||
|
||||
@media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(Status::MEDIA_ATTACHMENTS_LIMIT).map(&:to_i))
|
||||
|
||||
|
||||
@ -23,12 +23,15 @@ class ProcessLinksService < BaseService
|
||||
def scan_text!
|
||||
urls = @status.text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
||||
|
||||
domains = urls.map(&:domain).uniq
|
||||
valid_domains = Instance.searchable.where(domain: domains).pluck(:domain)
|
||||
|
||||
urls.each do |url|
|
||||
# We only support `FeaturedCollection` at this time
|
||||
|
||||
# TODO: We probably want to resolve unknown objects at authoring time
|
||||
object = ActivityPub::TagManager.instance.uri_to_resource(url.to_s, Collection)
|
||||
next if object.nil?
|
||||
object ||= ResolveURLService.new.call(url.to_s) if valid_domains.include?(url.domain)
|
||||
next unless object.is_a?(Collection)
|
||||
|
||||
tagged_object = @previous_objects.find { |x| x.object == object || x.uri == url }
|
||||
tagged_object ||= @current_objects.find { |x| x.object == object || x.uri == url }
|
||||
|
||||
20
app/views/collections/show.html.haml
Normal file
20
app/views/collections/show.html.haml
Normal file
@ -0,0 +1,20 @@
|
||||
- content_for :page_title, @collection.name
|
||||
|
||||
- content_for :header_tags do
|
||||
%meta{ name: 'robots', content: 'noindex, noarchive' }/
|
||||
|
||||
%link{ rel: 'alternate', type: 'application/activity+json', href: ap_account_collection_url(@collection.account_id, @collection) }/
|
||||
%link{ rel: 'alternate', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', href: ap_account_collection_url(@collection.account_id, @collection) }/
|
||||
|
||||
- unless @collection.sensitive?
|
||||
%meta{ name: 'description', content: @collection.description }/
|
||||
= opengraph 'og:description', @collection.description
|
||||
= opengraph 'og:site_name', site_title
|
||||
= opengraph 'og:type', 'website'
|
||||
= opengraph 'og:title', @collection.name
|
||||
= opengraph 'og:url', collection_url(@collection)
|
||||
- if @collection.language.present?
|
||||
= opengraph 'og:locale', @collection.language
|
||||
= opengraph 'twitter:card', 'summary'
|
||||
|
||||
= render 'shared/web_app'
|
||||
@ -525,6 +525,21 @@ be:
|
||||
no_lists_yet: Пакуль няма спісаў
|
||||
last_email: Апошняя электронная пошта
|
||||
lead: Уліковыя запісы, якія ўключылі гэту функцыю і маюць падпісчыкаў, будуць паказаныя знізу.
|
||||
show:
|
||||
confirm_disable_feature: Адключыць рассылку па электроннай пошце для %{name}? Абнаўленні больш не будуць прыходзіць на пошту гэтага ўліковага запісу. Карыстальнік усё яшчэ зможа ўключыць гэту функцыю нанова ў наладах уліковага запісу. Каб назаўсёды прыбраць доступ да гэтай функцыі, змяніце дазволы ўліковага запісу ў Ролях.
|
||||
confirm_remove_subscriber: "%{email} больш не будзе атрымліваць электронныя лісты ад %{name}. Гэтае дзеянне незваротнае."
|
||||
consent: Падпісчыкі пагадзіліся толькі атрымліваць допісы на электронную пошту. Не карыстайцеся гэтым спісам у іншых мэтах.
|
||||
date: Дата рэгістрацыі
|
||||
disable_feature: Адключыць функцыю
|
||||
disabled: Функцыя была адключаная і электронныя лісты больш не будуць дасылацца гэтаму спісу.
|
||||
email: Адрас электроннай пошты
|
||||
empty:
|
||||
hint: Ніхто яшчэ не аформіў падпіску на гэты ўліковы запіс.
|
||||
no_subscribers_yet: Пакуль няма падпісчыкаў
|
||||
enable_feature: Уключыць функцыю
|
||||
no_access_html: У гэтага ўліковага запісу больш няма дазволаў, патрэбных, каб уключыць гэту функцыю. Змяніце гэта ў <a href="%{roles_path}">Ролях</a>.
|
||||
title: Паштовая рассылка %{name}
|
||||
view_account: Прагл. уліковы запіс
|
||||
status: Стан
|
||||
subscribers: Падпісчыкі
|
||||
title: Спісы рассылкі
|
||||
@ -1541,6 +1556,7 @@ be:
|
||||
your_appeal_rejected: Ваша абскарджанне было адхілена
|
||||
edit_profile:
|
||||
other: Іншае
|
||||
privacy_redesign_body: Уключыць ці адключыць паказ Вашых падпісчыкаў і падпісак цяпер можна прама ў Вашым профілі.
|
||||
redesign_body: Рэдагаванне профілю цяпер даступнае наўпрост са старонкі профілю.
|
||||
redesign_button: Перайсці туды
|
||||
redesign_title: Адбыліся змены ў рэдагаванні профілю
|
||||
@ -1575,7 +1591,9 @@ be:
|
||||
success_html: Вы цяпер пачняце атрымліваць электронныя лісты, калі %{name} будзе рабіць новыя допісы. Дадайце %{sender} у свае кантакты, каб гэтыя допісы не траплялі ў папку са спамам.
|
||||
title: Вы падпісаліся праз эл. пошту
|
||||
unsubscribe: Адпісацца
|
||||
disabled: Адключана
|
||||
inactive: Неактыўная
|
||||
no_access: Няма доступу
|
||||
status: Стан
|
||||
subscribers: Падпісчыкі па эл.пошце
|
||||
emoji_styles:
|
||||
|
||||
@ -505,6 +505,21 @@ gl:
|
||||
no_lists_yet: Aínda non hai listas
|
||||
last_email: Último correo
|
||||
lead: Aquí móstranse as contas que activaron esta ferramenta e teñen subscritoras.
|
||||
show:
|
||||
confirm_disable_feature: Desactivar o boletín para %{name}? Non se van enviar máis novidades desta conta por correo. A usuaria poderá reactivar esta ferramenta nos axustes da súa conta. Para quitar de xeito permanente o acceso á ferramenta, edita os permisos da conta en Roles.
|
||||
confirm_remove_subscriber: "%{email} non vai seguir recibindo correos de %{name}. Esta acción non é reversible."
|
||||
consent: As subscritoras consentiron exclusivamente recibir no correo as publicacións. Non uses esta lista para outros propósitos.
|
||||
date: Data da subscrición
|
||||
disable_feature: Desactivar a ferramenta
|
||||
disabled: Esta ferramenta desactivouse e non se van seguir enviando correos a esta lista.
|
||||
email: Enderezo de correo
|
||||
empty:
|
||||
hint: Aínda non hai subscricións a esta conta.
|
||||
no_subscribers_yet: Sen subscricións
|
||||
enable_feature: Activar a ferramenta
|
||||
no_access_html: Esta conta xa non ten os permisos requeridos para activar a ferramenta. Cambiaos en <a href="%{roles_path}">Roles</a>.
|
||||
title: Boletín por correo de %{name}
|
||||
view_account: Ver conta
|
||||
status: Estado
|
||||
subscribers: Subscritoras
|
||||
title: Listas de correo
|
||||
@ -1532,7 +1547,9 @@ gl:
|
||||
success_html: Vas comezar a recibir correos cando %{name} publique novas publicacións. Engade %{sender} á túa libreta de enderezos para que os correos non vaian directamente ao cartafol de Spam.
|
||||
title: Subscribícheste
|
||||
unsubscribe: Anular subscrición
|
||||
disabled: Desactivado
|
||||
inactive: Inactiva
|
||||
no_access: Sen acceso
|
||||
status: Estado
|
||||
subscribers: Subscritoras
|
||||
emoji_styles:
|
||||
|
||||
@ -525,6 +525,21 @@ he:
|
||||
no_lists_yet: אין רשימות עדיין
|
||||
last_email: הודעת דואל אחרונה
|
||||
lead: חשבונות שבהם הופעלה האפשרות ויש להם מנויים יופיעו להלן.
|
||||
show:
|
||||
confirm_disable_feature: להשבית מנשרי דוא"ל של %{name}? עדכוני דוא"ל לא ישלחו עוד מטעם חשבון זה. המשתמשת תוכל לאפשר מחדש את התכונה מהעדפות החשבון שלה. כדי לכבות לצמיתות גישה לתכונה הזו, יש לערוך את הרשאות החשבון תחת תפקידים.
|
||||
confirm_remove_subscriber: '%{email} לא יקבלו יותר דוא"ל מאת %{name}. פעולה זו לא בלתי הפיכה.'
|
||||
consent: מנויים הסכימו רק לקבלת הודעות דרך דוא"ל. אין להשתש ברשימה זו לאף מטרה אחרת.
|
||||
date: תאריך ההצטרפות
|
||||
disable_feature: כיבוי תכונה
|
||||
disabled: תכונה זו הושבתה והודעות דוא"ל לא נשלחות יותר אל רשימה זו.
|
||||
email: כתובת דוא"ל
|
||||
empty:
|
||||
hint: אין לחשבון זה נרשמים עדיין.
|
||||
no_subscribers_yet: אין מנויים עדיין
|
||||
enable_feature: אפשר תכונה
|
||||
no_access_html: לחשבון זה אין יותר את ההרשאות הדרושות להפעלת התכונה. שנו זאת תחת <a href="%{roles_path}">תפקידים</a>.
|
||||
title: מנשרי דוא"ל של %{name}
|
||||
view_account: הצג חשבון
|
||||
status: מצב
|
||||
subscribers: מנויים
|
||||
title: רשימות תפוצה
|
||||
@ -1576,7 +1591,9 @@ he:
|
||||
success_html: מעתה תקבלנה דוא"ל כאשר %{name} יפרסמו הודעות חדשות. הוספנה את %{sender} לאנשי הקשר כדי שההודעות האלו לא יגיעו אל פח הספאם שלכן.
|
||||
title: נרשמת
|
||||
unsubscribe: ביטול ההרשמה
|
||||
disabled: כבוי
|
||||
inactive: לא פעילים
|
||||
no_access: אין גישה
|
||||
status: מצב
|
||||
subscribers: מנויים
|
||||
emoji_styles:
|
||||
|
||||
@ -506,13 +506,18 @@ is:
|
||||
last_email: Síðasti tölvupóstur
|
||||
lead: Aðgangar sem hafa virkjað eiginleikann og eru með áskrifendur munu birtast hér fyrir neðan.
|
||||
show:
|
||||
confirm_disable_feature: Á að gera óvirkar fréttir í tölvupósti frá %{name}? Færslur verða ekki lengur sendar í tölvupósti frá þessum notandaaðgangi. Notandinn mun samt geta virkjað aftur þennan eiginleika í stillingunum sínum. Til að fjarlægja endanlega aðgang að þessum eiginleika skaltu breyta heimildum aðgangsins í gegnum 'Hlutverk'.
|
||||
confirm_remove_subscriber: "%{email} mun ekki lengur fá tölvupósta frá %{name}. Ekki er hægt að afturkalla þessa aðgerð."
|
||||
consent: Áskrifendur hafa einungis samþykkt að fá sendar færslur í tölvupósti. Ekki nota þennan lista í neinum öðrum tilgangi.
|
||||
date: Dagsetning skráningar
|
||||
disable_feature: Gera eiginleika óvirkan
|
||||
disabled: Eiginleikinn var gerður óvirkur og eru tölvupóstar ekki lengur sendir á þenna lista.
|
||||
email: Tölvupóstfang
|
||||
empty:
|
||||
hint: Enginn hefur enn gerst áskrifandi að þessum notandaaðgangi.
|
||||
no_subscribers_yet: Engir áskrifendur ennþá
|
||||
enable_feature: Virkja eiginleika
|
||||
no_access_html: Þessi aðgangur hefur ekki lengur heimild til að virkja þennan eiginleika. Breyttu því í <a href="%{roles_path}">Hlutverk</a>.
|
||||
title: Tölvupóstfréttir frá %{name}
|
||||
view_account: Skoða notandaaðgang
|
||||
status: Staða
|
||||
|
||||
@ -505,6 +505,21 @@ sq:
|
||||
no_lists_yet: Ende pa lista
|
||||
last_email: Email-i i fundit
|
||||
lead: Llogaritë që kanë aktivizuar veçorinë dhe kanë pajtimtarë do të shfaqen më poshtë.
|
||||
show:
|
||||
confirm_disable_feature: Të çaktivizohen buletine me email për %{name}? Për këtë llogari s’do të dërgohen më përditësime me email. Përdoruesi do të jetë prapëseprapë në gjendje të riaktivizojë veçorinë që nga rregullimet e llogarisë të vet. Për të hequr përgjithnjë përdorimin e kësaj veçorie, përpunoni lejen e llogarisë te Role.
|
||||
confirm_remove_subscriber: "%{email} s’do të marrë më email-e nga %{name}. Ky veprim s’mund të zhbëhet."
|
||||
consent: Pajtimtarët kanë pranuar vetëm të marrin postime përmes email-i. Mos e përdorni këtë listë për qëllime të tjera.
|
||||
date: Datë e regjistrimit
|
||||
disable_feature: Çaktivizoje veçorinë
|
||||
disabled: Veçoria qe çaktivizuar dhe te kjo listë s’dërgohen më email-e.
|
||||
email: Adresë email
|
||||
empty:
|
||||
hint: Te kjo llogari s’është pajtuar ende dikush.
|
||||
no_subscribers_yet: Ende pa pajtimtarë
|
||||
enable_feature: Aktivizoje veçorinë
|
||||
no_access_html: Kjo llogari s’ka më lejet e domosdoshme për aktivizimin e veçorisë. Ndryshojeni këtë që nga <a href="%{roles_path}">Role</a>.
|
||||
title: Buletine me email nga %{name}
|
||||
view_account: Shiheni llogarinë
|
||||
status: Gjendje
|
||||
subscribers: Pajtimtarë
|
||||
title: Lista postimesh
|
||||
@ -1521,7 +1536,9 @@ sq:
|
||||
success_html: Tani do të filloni të merrni email-e, kur %{name} boton postime të reja. Shtojeni %{sender} te kontaktet tuaj, që këto postime të mos përfundojnë te dosja juaj e Të padëshiruarve.
|
||||
title: U regjistruat
|
||||
unsubscribe: Shpajtohuni
|
||||
disabled: E çaktivizuar
|
||||
inactive: Joaktiv
|
||||
no_access: S’lejohet përdorim
|
||||
status: Gjendje
|
||||
subscribers: Pajtimtarë
|
||||
errors:
|
||||
|
||||
@ -495,6 +495,21 @@ vi:
|
||||
no_lists_yet: Chưa có danh sách nào
|
||||
last_email: Email gần nhất
|
||||
lead: Các tài khoản đã kích hoạt tính năng này và có người đăng ký sẽ hiển thị bên dưới.
|
||||
show:
|
||||
confirm_disable_feature: Tắt bản tin email cho %{name}? Bản tin cập nhật qua email sẽ không còn được gửi cho tài khoản này nữa. Người dùng vẫn có thể bật lại tính năng này trong cài đặt tài khoản của họ. Để xóa vĩnh viễn quyền truy cập vào tính năng này, hãy chỉnh sửa quyền của tài khoản trong mục Vai trò.
|
||||
confirm_remove_subscriber: "%{email} sẽ không còn nhận được email từ %{name} nữa. Hành động này không thể hoàn tác."
|
||||
consent: Người đăng ký chỉ đồng ý nhận bài viết qua email. Vui lòng không sử dụng danh sách này cho mục đích khác.
|
||||
date: Ngày đăng ký
|
||||
disable_feature: Tắt tính năng
|
||||
disabled: Tính năng này đã bị vô hiệu hóa và email không còn được gửi đến danh sách này nữa.
|
||||
email: Địa chỉ email
|
||||
empty:
|
||||
hint: Hiện chưa có ai đăng ký theo dõi tài khoản này.
|
||||
no_subscribers_yet: Chưa có đăng ký theo dõi
|
||||
enable_feature: Bật tính năng
|
||||
no_access_html: Tài khoản này không còn quyền hạn cần thiết để kích hoạt tính năng này nữa. Thay đổi trong <a href="%{roles_path}">Vai trò</a>.
|
||||
title: Bản tin email của %{name}
|
||||
view_account: Xem tài khoản
|
||||
status: Trạng thái
|
||||
subscribers: Người đăng ký đọc
|
||||
title: Danh sách gửi thư
|
||||
@ -1510,7 +1525,9 @@ vi:
|
||||
success_html: Bạn sẽ bắt đầu nhận được email khi %{name} đăng tút mới. Thêm %{sender} vào danh bạ của bạn để những tút này không bị chuyển vào thư mục Spam.
|
||||
title: Bạn đã đăng ký đọc
|
||||
unsubscribe: Hủy đăng ký đọc
|
||||
disabled: Đã tắt
|
||||
inactive: Không hoạt động
|
||||
no_access: Không có quyền
|
||||
status: Trạng thái
|
||||
subscribers: Người đăng ký đọc
|
||||
emoji_styles:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user