Merge commit '9e0a3aaf08e7d4fc700f7f19d61f5f8fee346b6a' into glitch-soc/merge-upstream

This commit is contained in:
Claire committed 2026-06-10 18:52:58 +02:00
commit 89c2ac444f
111 files changed
+891 -238

No files matched your search

@@ -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}
+37 -31
View File
@@ -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}