Merge commit '0af2c4829ff86cf84ed9e7804d023f31e805542b' into glitch-soc/merge-upstream

Conflicts:
- `vite.config.mts`:
  Conflict because of differently-named theme plugins.
  Updated to go with upstream's changes.
This commit is contained in:
Claire
2025-07-23 12:08:59 +02:00
26 changed files with 873 additions and 265 deletions

View File

@@ -23,7 +23,6 @@
matchManagers: ['npm'],
matchPackageNames: [
'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes
// react-router: Requires manual upgrade
'history',

View File

@@ -438,7 +438,7 @@ GEM
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0514)
mime-types-data (3.2025.0715)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.5)
@@ -610,7 +610,7 @@ GEM
pg (1.5.9)
pghero (3.7.0)
activerecord (>= 7.1)
playwright-ruby-client (1.52.0)
playwright-ruby-client (1.54.0)
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
pp (0.6.2)
@@ -722,7 +722,7 @@ GEM
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.10.0)
reline (0.6.1)
reline (0.6.2)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)

View File

@@ -0,0 +1,171 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect } from 'storybook/test';
import type { HandlerMap } from '.';
import { Hotkeys } from '.';
const meta = {
title: 'Components/Hotkeys',
component: Hotkeys,
args: {
global: undefined,
focusable: undefined,
handlers: {},
},
tags: ['test'],
} satisfies Meta<typeof Hotkeys>;
export default meta;
type Story = StoryObj<typeof meta>;
const hotkeyTest: Story['play'] = async ({ canvas, userEvent }) => {
async function confirmHotkey(name: string, shouldFind = true) {
// 'status' is the role of the 'output' element
const output = await canvas.findByRole('status');
if (shouldFind) {
await expect(output).toHaveTextContent(name);
} else {
await expect(output).not.toHaveTextContent(name);
}
}
const button = await canvas.findByRole('button');
await userEvent.click(button);
await userEvent.keyboard('n');
await confirmHotkey('new');
await userEvent.keyboard('/');
await confirmHotkey('search');
await userEvent.keyboard('o');
await confirmHotkey('open');
await userEvent.keyboard('{Alt>}N{/Alt}');
await confirmHotkey('forceNew');
await userEvent.keyboard('gh');
await confirmHotkey('goToHome');
await userEvent.keyboard('gn');
await confirmHotkey('goToNotifications');
await userEvent.keyboard('gf');
await confirmHotkey('goToFavourites');
/**
* Ensure that hotkeys are not triggered when certain
* interactive elements are focused:
*/
await userEvent.keyboard('{enter}');
await confirmHotkey('open', false);
const input = await canvas.findByRole('textbox');
await userEvent.click(input);
await userEvent.keyboard('n');
await confirmHotkey('new', false);
await userEvent.keyboard('{backspace}');
await confirmHotkey('None', false);
/**
* Reset playground:
*/
await userEvent.click(button);
await userEvent.keyboard('{backspace}');
};
export const Default = {
render: function Render() {
const [matchedHotkey, setMatchedHotkey] = useState<keyof HandlerMap | null>(
null,
);
const handlers = {
back: () => {
setMatchedHotkey(null);
},
new: () => {
setMatchedHotkey('new');
},
forceNew: () => {
setMatchedHotkey('forceNew');
},
search: () => {
setMatchedHotkey('search');
},
open: () => {
setMatchedHotkey('open');
},
goToHome: () => {
setMatchedHotkey('goToHome');
},
goToNotifications: () => {
setMatchedHotkey('goToNotifications');
},
goToFavourites: () => {
setMatchedHotkey('goToFavourites');
},
};
return (
<Hotkeys handlers={handlers}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
padding: '1em',
border: '1px dashed #ccc',
fontSize: 14,
color: '#222',
}}
>
<h1
style={{
fontSize: 22,
marginBottom: '0.3em',
}}
>
Hotkey playground
</h1>
<p>
Last pressed hotkey: <output>{matchedHotkey ?? 'None'}</output>
</p>
<p>
Click within the dashed border and press the &quot;<kbd>n</kbd>
&quot; or &quot;<kbd>/</kbd>&quot; key. Press &quot;
<kbd>Backspace</kbd>&quot; to clear the displayed hotkey.
</p>
<p>
Try typing a sequence, like &quot;<kbd>g</kbd>&quot; shortly
followed by &quot;<kbd>h</kbd>&quot;, &quot;<kbd>n</kbd>&quot;, or
&quot;<kbd>f</kbd>&quot;
</p>
<p>
Note that this playground doesn&apos;t support all hotkeys we use in
the app.
</p>
<p>
When a <button>Button</button> is focused, &quot;
<kbd>Enter</kbd>
&quot; should not trigger &quot;open&quot;, but &quot;<kbd>o</kbd>
&quot; should.
</p>
<p>
When an input element is focused, hotkeys should not interfere with
regular typing:
</p>
<input type='text' />
</div>
</Hotkeys>
);
},
play: hotkeyTest,
};

View File

@@ -0,0 +1,282 @@
import { useEffect, useRef } from 'react';
import { normalizeKey, isKeyboardEvent } from './utils';
/**
* In case of multiple hotkeys matching the pressed key(s),
* the hotkey with a higher priority is selected. All others
* are ignored.
*/
const hotkeyPriority = {
singleKey: 0,
combo: 1,
sequence: 2,
} as const;
/**
* This type of function receives a keyboard event and an array of
* previously pressed keys (within the last second), and returns
* `isMatch` (whether the pressed keys match a hotkey) and `priority`
* (a weighting used to resolve conflicts when two hotkeys match the
* pressed keys)
*/
type KeyMatcher = (
event: KeyboardEvent,
bufferedKeys?: string[],
) => {
/**
* Whether the event.key matches the hotkey
*/
isMatch: boolean;
/**
* If there are multiple matching hotkeys, the
* first one with the highest priority will be handled
*/
priority: (typeof hotkeyPriority)[keyof typeof hotkeyPriority];
};
/**
* Matches a single key
*/
function just(keyName: string): KeyMatcher {
return (event) => ({
isMatch: normalizeKey(event.key) === keyName,
priority: hotkeyPriority.singleKey,
});
}
/**
* Matches any single key out of those provided
*/
function any(...keys: string[]): KeyMatcher {
return (event) => ({
isMatch: keys.some((keyName) => just(keyName)(event).isMatch),
priority: hotkeyPriority.singleKey,
});
}
/**
* Matches a single key combined with the option/alt modifier
*/
function optionPlus(key: string): KeyMatcher {
return (event) => ({
// Matching against event.code here as alt combos are often
// mapped to other characters
isMatch: event.altKey && event.code === `Key${key.toUpperCase()}`,
priority: hotkeyPriority.combo,
});
}
/**
* Matches when all provided keys are pressed in sequence.
*/
function sequence(...sequence: string[]): KeyMatcher {
return (event, bufferedKeys) => {
const lastKeyInSequence = sequence.at(-1);
const startOfSequence = sequence.slice(0, -1);
const relevantBufferedKeys = bufferedKeys?.slice(-startOfSequence.length);
const bufferMatchesStartOfSequence =
!!relevantBufferedKeys &&
startOfSequence.join('') === relevantBufferedKeys.join('');
return {
isMatch:
bufferMatchesStartOfSequence &&
normalizeKey(event.key) === lastKeyInSequence,
priority: hotkeyPriority.sequence,
};
};
}
/**
* This is a map of all global hotkeys we support.
* To trigger a hotkey, a handler with a matching name must be
* provided to the `useHotkeys` hook or `Hotkeys` component.
*/
const hotkeyMatcherMap = {
help: just('?'),
search: any('s', '/'),
back: just('backspace'),
new: just('n'),
forceNew: optionPlus('n'),
focusColumn: any('1', '2', '3', '4', '5', '6', '7', '8', '9'),
reply: just('r'),
favourite: just('f'),
boost: just('b'),
mention: just('m'),
open: any('enter', 'o'),
openProfile: just('p'),
moveDown: any('down', 'j'),
moveUp: any('up', 'k'),
toggleHidden: just('x'),
toggleSensitive: just('h'),
toggleComposeSpoilers: optionPlus('x'),
openMedia: just('e'),
onTranslate: just('t'),
goToHome: sequence('g', 'h'),
goToNotifications: sequence('g', 'n'),
goToLocal: sequence('g', 'l'),
goToFederated: sequence('g', 't'),
goToDirect: sequence('g', 'd'),
goToStart: sequence('g', 's'),
goToFavourites: sequence('g', 'f'),
goToPinned: sequence('g', 'p'),
goToProfile: sequence('g', 'u'),
goToBlocked: sequence('g', 'b'),
goToMuted: sequence('g', 'm'),
goToRequests: sequence('g', 'r'),
cheat: sequence(
'up',
'up',
'down',
'down',
'left',
'right',
'left',
'right',
'b',
'a',
'enter',
),
} as const;
type HotkeyName = keyof typeof hotkeyMatcherMap;
export type HandlerMap = Partial<
Record<HotkeyName, (event: KeyboardEvent) => void>
>;
export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
const ref = useRef<T>(null);
const bufferedKeys = useRef<string[]>([]);
const sequenceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
/**
* Store the latest handlers object in a ref so we don't need to
* add it as a dependency to the main event listener effect
*/
const handlersRef = useRef(handlers);
useEffect(() => {
handlersRef.current = handlers;
}, [handlers]);
useEffect(() => {
const element = ref.current ?? document;
function listener(event: Event) {
// Ignore key presses from input, textarea, or select elements
const tagName = (event.target as HTMLElement).tagName.toLowerCase();
const shouldHandleEvent =
isKeyboardEvent(event) &&
!event.defaultPrevented &&
!['input', 'textarea', 'select'].includes(tagName) &&
!(
['a', 'button'].includes(tagName) &&
normalizeKey(event.key) === 'enter'
);
if (shouldHandleEvent) {
const matchCandidates: {
handler: (event: KeyboardEvent) => void;
priority: number;
}[] = [];
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
(handlerName) => {
const handler = handlersRef.current[handlerName];
if (handler) {
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
const { isMatch, priority } = hotkeyMatcher(
event,
bufferedKeys.current,
);
if (isMatch) {
matchCandidates.push({ handler, priority });
}
}
},
);
// Sort all matches by priority
matchCandidates.sort((a, b) => b.priority - a.priority);
const bestMatchingHandler = matchCandidates.at(0)?.handler;
if (bestMatchingHandler) {
bestMatchingHandler(event);
event.stopPropagation();
event.preventDefault();
}
// Add last keypress to buffer
bufferedKeys.current.push(normalizeKey(event.key));
// Reset the timeout
if (sequenceTimer.current) {
clearTimeout(sequenceTimer.current);
}
sequenceTimer.current = setTimeout(() => {
bufferedKeys.current = [];
}, 1000);
}
}
element.addEventListener('keydown', listener);
return () => {
element.removeEventListener('keydown', listener);
if (sequenceTimer.current) {
clearTimeout(sequenceTimer.current);
}
};
}, []);
return ref;
}
/**
* The Hotkeys component allows us to globally register keyboard combinations
* under a name and assign actions to them, either globally or scoped to a portion
* of the app.
*
* ### How to use
*
* To add a new hotkey, add its key combination to the `hotkeyMatcherMap` object
* and give it a name.
*
* Use the `<Hotkeys>` component or the `useHotkeys` hook in the part of of the app
* where you want to handle the action, and pass in a handlers object.
*
* ```tsx
* <Hotkeys handlers={{open: openStatus}} />
* ```
*
* Now this function will be called when the 'open' hotkey is pressed by the user.
*/
export const Hotkeys: React.FC<{
/**
* An object containing functions to be run when a hotkey is pressed.
* The key must be the name of a registered hotkey, e.g. "help" or "search"
*/
handlers: HandlerMap;
/**
* When enabled, hotkeys will be matched against the document root
* rather than only inside of this component's DOM node.
*/
global?: boolean;
/**
* Allow the rendered `div` to be focused
*/
focusable?: boolean;
children: React.ReactNode;
}> = ({ handlers, global, focusable = true, children }) => {
const ref = useHotkeys<HTMLDivElement>(handlers);
return (
<div ref={global ? undefined : ref} tabIndex={focusable ? -1 : undefined}>
{children}
</div>
);
};

View File

@@ -0,0 +1,29 @@
export function isKeyboardEvent(event: Event): event is KeyboardEvent {
return 'key' in event;
}
export function normalizeKey(key: string): string {
const lowerKey = key.toLowerCase();
switch (lowerKey) {
case ' ':
case 'spacebar': // for older browsers
return 'space';
case 'arrowup':
return 'up';
case 'arrowdown':
return 'down';
case 'arrowleft':
return 'left';
case 'arrowright':
return 'right';
case 'esc':
case 'escape':
return 'escape';
default:
return lowerKey;
}
}

View File

@@ -8,10 +8,9 @@ import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { Hotkeys } from 'mastodon/components/hotkeys';
import { ContentWarning } from 'mastodon/components/content_warning';
import { FilterWarning } from 'mastodon/components/filter_warning';
import { Icon } from 'mastodon/components/icon';
@@ -35,7 +34,6 @@ import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
import { StatusThreadLabel } from './status_thread_label';
import { VisibilityIcon } from './visibility_icon';
const domParser = new DOMParser();
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
@@ -325,11 +323,11 @@ class Status extends ImmutablePureComponent {
};
handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
this.props.onMoveUp?.(this.props.status.get('id'), this.node.getAttribute('data-featured'));
};
handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
this.props.onMoveDown?.(this.props.status.get('id'), this.node.getAttribute('data-featured'));
};
handleHotkeyToggleHidden = () => {
@@ -437,13 +435,13 @@ class Status extends ImmutablePureComponent {
if (hidden) {
return (
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<Hotkeys handlers={handlers} focusable={!unfocusable}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={unfocusable ? null : 0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
{status.get('spoiler_text').length > 0 && (<span>{status.get('spoiler_text')}</span>)}
{expanded && <span>{status.get('content')}</span>}
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -543,7 +541,7 @@ class Status extends ImmutablePureComponent {
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
return (
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
<Hotkeys handlers={handlers} focusable={!unfocusable}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
{!skipPrepend && prepend}
@@ -604,7 +602,7 @@ class Status extends ImmutablePureComponent {
}
</div>
</div>
</HotKeys>
</Hotkeys>
);
}

View File

@@ -69,6 +69,7 @@ export default class StatusList extends ImmutablePureComponent {
_selectChild (index, align_top) {
const container = this.node.node;
// TODO: This breaks at the inline-follow-suggestions container
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) {

View File

@@ -92,10 +92,29 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onChange(e.target.value);
};
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
blurOnEscape = (e) => {
if (['esc', 'escape'].includes(e.key.toLowerCase())) {
e.target.blur();
}
}
handleKeyDownPost = (e) => {
if (e.key.toLowerCase() === 'enter' && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
this.blurOnEscape(e);
};
handleKeyDownSpoiler = (e) => {
if (e.key.toLowerCase() === 'enter') {
if (e.ctrlKey || e.metaKey) {
this.handleSubmit();
} else {
e.preventDefault();
this.textareaRef.current?.focus();
}
}
this.blurOnEscape(e);
};
getFulltextForCharacterCounting = () => {
@@ -248,7 +267,7 @@ class ComposeForm extends ImmutablePureComponent {
value={this.props.spoilerText}
disabled={isSubmitting}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
onKeyDown={this.handleKeyDownSpoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
@@ -273,7 +292,7 @@ class ComposeForm extends ImmutablePureComponent {
onChange={this.handleChange}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onKeyDown={this.handleKeyDownPost}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}

View File

@@ -10,15 +10,13 @@ import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { useDispatch, useSelector } from 'react-redux';
import { HotKeys } from 'react-hotkeys';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import { replyCompose } from 'mastodon/actions/compose';
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
import { openModal } from 'mastodon/actions/modal';
import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { Hotkeys } from 'mastodon/components/hotkeys';
import AttachmentList from 'mastodon/components/attachment_list';
import AvatarComposite from 'mastodon/components/avatar_composite';
import { IconButton } from 'mastodon/components/icon_button';
@@ -169,7 +167,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
};
return (
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div className={classNames('conversation focusable muted', { unread })} tabIndex={0}>
<div className='conversation__avatar' onClick={handleClick} role='presentation'>
<AvatarComposite accounts={accounts} size={48} />
@@ -219,7 +217,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
</div>
</div>
</div>
</HotKeys>
</Hotkeys>
);
};

View File

@@ -8,7 +8,6 @@ import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
@@ -20,6 +19,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { Hotkeys } from 'mastodon/components/hotkeys';
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
import { me } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@@ -137,7 +137,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-follow focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='user-plus' icon={PersonAddIcon} />
@@ -149,7 +149,7 @@ class Notification extends ImmutablePureComponent {
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -157,7 +157,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='user' icon={PersonIcon} />
@@ -169,7 +169,7 @@ class Notification extends ImmutablePureComponent {
<FollowRequestContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -195,7 +195,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-favourite focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='star' icon={StarIcon} className='star-icon' />
@@ -217,7 +217,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -225,7 +225,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='retweet' icon={RepeatIcon} />
@@ -247,7 +247,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -259,7 +259,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-status focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='home' icon={HomeIcon} />
@@ -282,7 +282,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -294,7 +294,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-update focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.update, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='pencil' icon={EditIcon} />
@@ -317,7 +317,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -331,7 +331,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='tasks' icon={InsertChartIcon} />
@@ -358,7 +358,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -371,7 +371,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.relationshipsSevered, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}>
<RelationshipsSeveranceEvent
type={event.get('type')}
@@ -381,7 +381,7 @@ class Notification extends ImmutablePureComponent {
hidden={hidden}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -394,7 +394,7 @@ class Notification extends ImmutablePureComponent {
}
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-moderation-warning focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.moderationWarning), notification.get('created_at'))}>
<ModerationWarning
action={warning.get('action')}
@@ -402,7 +402,7 @@ class Notification extends ImmutablePureComponent {
hidden={hidden}
/>
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -410,7 +410,7 @@ class Notification extends ImmutablePureComponent {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminSignUp, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='user-plus' icon={PersonAddIcon} />
@@ -422,7 +422,7 @@ class Notification extends ImmutablePureComponent {
<Account id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}
@@ -438,7 +438,7 @@ class Notification extends ImmutablePureComponent {
const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} data-hover-card-account={targetAccount.get('id')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
return (
<HotKeys handlers={this.getHandlers()}>
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: account.get('acct'), target: notification.getIn(['report', 'target_account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='flag' icon={FlagIcon} />
@@ -450,7 +450,7 @@ class Notification extends ImmutablePureComponent {
<Report account={account} report={notification.get('report')} hidden={this.props.hidden} />
</div>
</HotKeys>
</Hotkeys>
);
}

View File

@@ -1,9 +1,8 @@
import { useMemo } from 'react';
import { HotKeys } from 'react-hotkeys';
import { navigateToProfile } from 'mastodon/actions/accounts';
import { mentionComposeById } from 'mastodon/actions/compose';
import { Hotkeys } from 'mastodon/components/hotkeys';
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
@@ -156,5 +155,5 @@ export const NotificationGroup: React.FC<{
return null;
}
return <HotKeys handlers={handlers}>{content}</HotKeys>;
return <Hotkeys handlers={handlers}>{content}</Hotkeys>;
};

View File

@@ -3,12 +3,11 @@ import type { JSX } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { navigateToStatus } from 'mastodon/actions/statuses';
import { Avatar } from 'mastodon/components/avatar';
import { AvatarGroup } from 'mastodon/components/avatar_group';
import { Hotkeys } from 'mastodon/components/hotkeys';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
@@ -91,7 +90,7 @@ export const NotificationGroupWithStatus: React.FC<{
);
return (
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div
role='button'
className={classNames(
@@ -149,6 +148,6 @@ export const NotificationGroupWithStatus: React.FC<{
)}
</div>
</div>
</HotKeys>
</Hotkeys>
);
};

View File

@@ -2,14 +2,13 @@ import { useMemo } from 'react';
import classNames from 'classnames';
import { HotKeys } from 'react-hotkeys';
import { replyComposeById } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import {
navigateToStatus,
toggleStatusSpoilers,
} from 'mastodon/actions/statuses';
import { Hotkeys } from 'mastodon/components/hotkeys';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
@@ -83,7 +82,7 @@ export const NotificationWithStatus: React.FC<{
if (!statusId || isFiltered) return null;
return (
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div
role='button'
className={classNames(
@@ -111,6 +110,6 @@ export const NotificationWithStatus: React.FC<{
unfocusable
/>
</div>
</HotKeys>
</Hotkeys>
);
};

View File

@@ -10,10 +10,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { HotKeys } from 'react-hotkeys';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { Hotkeys } from 'mastodon/components/hotkeys';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { TimelineHint } from 'mastodon/components/timeline_hint';
@@ -616,7 +615,7 @@ class Status extends ImmutablePureComponent {
<div className={classNames('scrollable', { fullscreen })} ref={this.setContainerRef}>
{ancestors}
<HotKeys handlers={handlers}>
<Hotkeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false)} ref={this.setStatusRef}>
<DetailedStatus
key={`details-${status.get('id')}`}
@@ -654,7 +653,7 @@ class Status extends ImmutablePureComponent {
onEmbed={this.handleEmbed}
/>
</div>
</HotKeys>
</Hotkeys>
{descendants}
{remoteHint}

View File

@@ -9,13 +9,13 @@ import { Redirect, Route, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { fetchNotifications } from 'mastodon/actions/notification_groups';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import { AlertsController } from 'mastodon/components/alerts_controller';
import { Hotkeys } from 'mastodon/components/hotkeys';
import { HoverCardController } from 'mastodon/components/hover_card_controller';
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
@@ -98,40 +98,6 @@ const mapStateToProps = state => ({
username: state.getIn(['accounts', me, 'username']),
});
const keyMap = {
help: '?',
new: 'n',
search: ['s', '/'],
forceNew: 'option+n',
toggleComposeSpoilers: 'option+x',
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
reply: 'r',
favourite: 'f',
boost: 'b',
mention: 'm',
open: ['enter', 'o'],
openProfile: 'p',
moveDown: ['down', 'j'],
moveUp: ['up', 'k'],
back: 'backspace',
goToHome: 'g h',
goToNotifications: 'g n',
goToLocal: 'g l',
goToFederated: 'g t',
goToDirect: 'g d',
goToStart: 'g s',
goToFavourites: 'g f',
goToPinned: 'g p',
goToProfile: 'g u',
goToBlocked: 'g b',
goToMuted: 'g m',
goToRequests: 'g r',
toggleHidden: 'x',
toggleSensitive: 'h',
openMedia: 'e',
onTranslate: 't',
};
class SwitchingColumnsArea extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
@@ -400,6 +366,10 @@ class UI extends PureComponent {
}
};
handleDonate = () => {
location.href = 'https://joinmastodon.org/sponsors#donate'
}
componentDidMount () {
const { signedIn } = this.props.identity;
@@ -426,10 +396,6 @@ class UI extends PureComponent {
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
}
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
};
}
componentWillUnmount () {
@@ -509,10 +475,6 @@ class UI extends PureComponent {
}
};
setHotkeysRef = c => {
this.hotkeys = c;
};
handleHotkeyToggleHelp = () => {
if (this.props.location.pathname === '/keyboard-shortcuts') {
this.props.history.goBack();
@@ -593,10 +555,11 @@ class UI extends PureComponent {
goToBlocked: this.handleHotkeyGoToBlocked,
goToMuted: this.handleHotkeyGoToMuted,
goToRequests: this.handleHotkeyGoToRequests,
cheat: this.handleDonate,
};
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<Hotkeys global handlers={handlers}>
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
<SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'} forceOnboarding={firstLaunch && newAccount}>
{children}
@@ -611,7 +574,7 @@ class UI extends PureComponent {
<ModalContainer />
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
</div>
</HotKeys>
</Hotkeys>
);
}

View File

@@ -1,8 +1,12 @@
{
"about.contact": "ਸੰਪਰਕ:",
"about.default_locale": "ਮੂਲ",
"about.disclaimer": "ਮਸਟੋਡੋਨ ਇੱਕ ਆਜ਼ਾਦ, ਖੁੱਲ੍ਹੇ ਸਰੋਤ ਵਾਲਾ ਸਾਫਟਵੇਅਰ ਹੈ ਅਤੇ Mastodon gGmbH ਦਾ ਮਾਰਕਾ ਹੈ।",
"about.domain_blocks.no_reason_available": "ਕਾਰਨ ਮੌਜੂਦ ਨਹੀਂ ਹੈ",
"about.domain_blocks.silenced.title": "ਸੀਮਿਤ",
"about.domain_blocks.suspended.title": "ਮੁਅੱਤਲ ਕੀਤ",
"about.domain_blocks.suspended.title": "ਸਸਪੈਂਡ ਕੀਤ",
"about.language_label": "ਭਾਸ਼ਾ",
"about.not_available": "ਇਹ ਜਾਣਕਾਰੀ ਨੂੰ ਇਸ ਸਰਵਰ ਉੱਤੇ ਉਪਲੱਬਧ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਹੈ।",
"about.rules": "ਸਰਵਰ ਨਿਯਮ",
"account.account_note_header": "ਨਿੱਜੀ ਨੋਟ",
"account.add_or_remove_from_list": "ਸੂਚੀ ਵਿੱਚ ਜੋੜੋ ਜਾਂ ਹਟਾਓ",
@@ -12,21 +16,33 @@
"account.block_domain": "{domain} ਡੋਮੇਨ ਉੱਤੇ ਪਾਬੰਦੀ ਲਾਓ",
"account.block_short": "ਪਾਬੰਦੀ",
"account.blocked": "ਪਾਬੰਦੀਸ਼ੁਦਾ",
"account.blocking": "ਪਾਬੰਦੀ ਲਾਉਣੀ",
"account.cancel_follow_request": "ਫ਼ਾਲੋ ਕਰਨ ਨੂੰ ਰੱਦ ਕਰੋ",
"account.copy": "ਪਰੋਫਾਇਲ ਲਈ ਲਿੰਕ ਕਾਪੀ ਕਰੋ",
"account.direct": "ਨਿੱਜੀ ਜ਼ਿਕਰ @{name}",
"account.disable_notifications": "ਜਦੋਂ {name} ਕੋਈ ਪੋਸਟ ਕਰੇ ਤਾਂ ਮੈਨੂੰ ਸੂਚਨਾ ਨਾ ਦਿਓ",
"account.domain_blocking": "ਡੋਮੇਨ ਉੱਤੇ ਪਾਬੰਦੀ",
"account.edit_profile": "ਪਰੋਫਾਈਲ ਨੂੰ ਸੋਧੋ",
"account.enable_notifications": "ਜਦੋਂ {name} ਪੋਸਟ ਕਰੇ ਤਾਂ ਮੈਨੂੰ ਸੂਚਨਾ ਦਿਓ",
"account.endorse": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਫ਼ੀਚਰ",
"account.familiar_followers_one": "{name1} ਵਲੋਂ ਫ਼ਾਲੋ ਕੀਤਾ",
"account.familiar_followers_two": "{name1} ਅਤੇ {name2} ਵਲੋਂ ਫ਼ਾਲੋ ਕੀਤਾ",
"account.featured": "ਫ਼ੀਚਰ",
"account.featured.accounts": "ਪਰੋਫਾਈਲ",
"account.featured.hashtags": "ਹੈਸ਼ਟੈਗ",
"account.featured_tags.last_status_at": "{date} ਨੂੰ ਆਖਰੀ ਪੋਸਟ",
"account.featured_tags.last_status_never": "ਕੋਈ ਪੋਸਟ ਨਹੀਂ",
"account.follow": "ਫ਼ਾਲੋ",
"account.follow_back": "ਵਾਪਸ ਫਾਲ਼ੋ ਕਰੋ",
"account.followers": "ਫ਼ਾਲੋਅਰ",
"account.followers.empty": "ਇਸ ਵਰਤੋਂਕਾਰ ਨੂੰ ਹਾਲੇ ਕੋਈ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।",
"account.followers_counter": "{count, plural, one {{counter} ਫ਼ਾਲੋਅਰ} other {{counter} ਫ਼ਾਲੋਅਰ}}",
"account.followers_you_know_counter": "{counter} ਤੁਸੀਂ ਜਾਣਦੇ ਹੋ",
"account.following": "ਫ਼ਾਲੋ ਕੀਤਾ",
"account.follows.empty": "ਇਹ ਵਰਤੋਂਕਾਰ ਹਾਲੇ ਕਿਸੇ ਨੂੰ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।",
"account.follows_you": "ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹਨ",
"account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ",
"account.hide_reblogs": "{name} ਵਲੋਂ ਬੂਸਟ ਨੂੰ ਲੁਕਾਓ",
"account.joined_short": "ਜੁਆਇਨ ਕੀਤਾ",
"account.media": "ਮੀਡੀਆ",
"account.mention": "@{name} ਦਾ ਜ਼ਿਕਰ",
@@ -34,6 +50,7 @@
"account.mute_notifications_short": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਮੌਨ ਕਰੋ",
"account.mute_short": "ਮੌਨ ਕਰੋ",
"account.muted": "ਮੌਨ ਕੀਤੀਆਂ",
"account.mutual": "ਤੁਸੀਂ ਇੱਕ ਦੂਜੇ ਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹੋ",
"account.no_bio": "ਕੋਈ ਵਰਣਨ ਨਹੀਂ ਦਿੱਤਾ।",
"account.open_original_page": "ਅਸਲ ਸਫ਼ੇ ਨੂੰ ਖੋਲ੍ਹੋ",
"account.posts": "ਪੋਸਟਾਂ",
@@ -42,8 +59,10 @@
"account.requested": "ਮਨਜ਼ੂਰੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ। ਫ਼ਾਲੋ ਬੇਨਤੀਆਂ ਨੂੰ ਰੱਦ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ",
"account.requested_follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ",
"account.share": "{name} ਦਾ ਪਰੋਫ਼ਾਇਲ ਸਾਂਝਾ ਕਰੋ",
"account.statuses_counter": "{count, plural, one {{counter} ਪੋਸਟ} other {{counter} ਪੋਸਟਾਂ}}",
"account.unblock": "@{name} ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ",
"account.unblock_domain": "{domain} ਡੋਮੇਨ ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ",
"account.unblock_domain_short": "ਪਾਬੰਦੀ ਹਟਾਓ",
"account.unblock_short": "ਪਾਬੰਦੀ ਹਟਾਓ",
"account.unendorse": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਫ਼ੀਚਰ ਨਾ ਕਰੋ",
"account.unfollow": "ਅਣ-ਫ਼ਾਲੋ",
@@ -148,6 +167,8 @@
"confirmations.missing_alt_text.secondary": "ਕਿਵੇਂ ਵੀ ਪੋਸਟ ਕਰੋ",
"confirmations.mute.confirm": "ਮੌਨ ਕਰੋ",
"confirmations.redraft.confirm": "ਹਟਾਓ ਤੇ ਮੁੜ-ਡਰਾਫਟ",
"confirmations.remove_from_followers.confirm": "ਫ਼ਾਲੋਅਰ ਨੂੰ ਹਟਾਓ",
"confirmations.remove_from_followers.title": "ਫ਼ਾਲੋਅਰ ਨੂੰ ਹਟਾਉਣਾ ਹੈ?",
"confirmations.unfollow.confirm": "ਅਣ-ਫ਼ਾਲੋ",
"confirmations.unfollow.message": "ਕੀ ਤੁਸੀਂ {name} ਨੂੰ ਅਣ-ਫ਼ਾਲੋ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
"confirmations.unfollow.title": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਅਣ-ਫ਼ਾਲੋ ਕਰਨਾ ਹੈ?",
@@ -182,7 +203,9 @@
"emoji_button.custom": "ਕਸਟਮ",
"emoji_button.flags": "ਝੰਡੀਆਂ",
"emoji_button.food": "ਖਾਣਾ-ਪੀਣਾ",
"emoji_button.label": "ਇਮੋਜੀ ਪਾਓ",
"emoji_button.nature": "ਕੁਦਰਤ",
"emoji_button.not_found": "ਕੋਈ ਮਿਲਦਾ ਇਮੋਜ਼ੀ ਨਹੀਂ ਲੱਭਿਆ",
"emoji_button.objects": "ਇਕਾਈ",
"emoji_button.people": "ਲੋਕ",
"emoji_button.recent": "ਅਕਸਰ ਵਰਤੇ",
@@ -199,9 +222,15 @@
"empty_column.list": "ਇਸ ਸੂਚੀ ਵਿੱਚ ਹਾਲੇ ਕੁਝ ਵੀ ਨਹੀਂ ਹੈ। ਜਦੋਂ ਇਸ ਸੂਚੀ ਦੇ ਮੈਂਬਰ ਨਵੀਆਂ ਪੋਸਟਾਂ ਪਾਉਂਦੇ ਹਨ ਤਾਂ ਉਹ ਇੱਥੇ ਦਿਖਾਈ ਦੇਣਗੀਆਂ।",
"errors.unexpected_crash.report_issue": "ਮੁੱਦੇ ਦੀ ਰਿਪੋਰਟ ਕਰੋ",
"explore.suggested_follows": "ਲੋਕ",
"explore.title": "ਰੁਝਾਨ",
"explore.trending_links": "ਖ਼ਬਰਾਂ",
"explore.trending_statuses": "ਪੋਸਟਾਂ",
"explore.trending_tags": "ਹੈਸ਼ਟੈਗ",
"featured_carousel.header": "{count, plural, one {ਟੰਗੀ ਹੋਈ ਪੋਸਟ} other {ਟੰਗੀਆਂ ਹੋਈਆਂ ਪੋਸਟਾਂ}}",
"featured_carousel.next": "ਅੱਗੇ",
"featured_carousel.post": "ਪੋਸਟ",
"featured_carousel.previous": "ਪਿੱਛੇ",
"featured_carousel.slide": "{total} ਵਿੱਚੋਂ {index}",
"filter_modal.added.expired_title": "ਫਿਲਟਰ ਦੀ ਮਿਆਦ ਪੁੱਗੀ!",
"filter_modal.added.review_and_configure_title": "ਫਿਲਟਰ ਸੈਟਿੰਗਾਂ",
"filter_modal.added.settings_link": "ਸੈਟਿੰਗਾਂ ਸਫ਼ਾ",
@@ -252,6 +281,8 @@
"home.column_settings.show_replies": "ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ",
"home.hide_announcements": "ਐਲਾਨਾਂ ਨੂੰ ਓਹਲੇ ਕਰੋ",
"home.pending_critical_update.link": "ਅੱਪਡੇਟ ਵੇਖੋ",
"home.pending_critical_update.title": "ਗੰਭੀਰ ਸੁਰੱਖਿਆ ਅੱਪਡੇਟ ਮੌਜੂਦ ਹੈ!",
"home.show_announcements": "ਐਲਾਨਾਂ ਨੂੰ ਵੇਖਾਓ",
"ignore_notifications_modal.ignore": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਅਣਡਿੱਠਾ ਕਰੋ",
"info_button.label": "ਮਦਦ",
"interaction_modal.go": "ਜਾਓ",
@@ -332,9 +363,12 @@
"media_gallery.hide": "ਲੁਕਾਓ",
"mute_modal.hide_from_notifications": "ਨੋਟੀਫਿਕੇਸ਼ਨਾਂ ਵਿੱਚੋਂ ਲੁਕਾਓ",
"mute_modal.show_options": "ਚੋਣਾਂ ਨੂੰ ਵੇਖਾਓ",
"mute_modal.title": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਮੌਨ ਕਰਨਾ ਹੈ?",
"navigation_bar.about": "ਇਸ ਬਾਰੇ",
"navigation_bar.account_settings": "ਪਾਸਵਰਡ ਅਤੇ ਸੁਰੱਖਿਆ",
"navigation_bar.administration": "ਪਰਸ਼ਾਸ਼ਨ",
"navigation_bar.advanced_interface": "ਤਕਨੀਕੀ ਵੈੱਬ ਇੰਟਰਫੇਸ ਵਿੱਚ ਖੋਲ੍ਹੋ",
"navigation_bar.automated_deletion": "ਆਪਣੇ-ਆਪ ਹਟਾਈ ਪੋਸਟ",
"navigation_bar.blocks": "ਪਾਬੰਦੀ ਲਾਏ ਵਰਤੋਂਕਾਰ",
"navigation_bar.bookmarks": "ਬੁੱਕਮਾਰਕ",
"navigation_bar.direct": "ਨਿੱਜੀ ਜ਼ਿਕਰ",
@@ -346,11 +380,16 @@
"navigation_bar.follows_and_followers": "ਫ਼ਾਲੋ ਅਤੇ ਫ਼ਾਲੋ ਕਰਨ ਵਾਲੇ",
"navigation_bar.lists": "ਸੂਚੀਆਂ",
"navigation_bar.logout": "ਲਾਗ ਆਉਟ",
"navigation_bar.more": "ਹੋਰ",
"navigation_bar.mutes": "ਮੌਨ ਕੀਤੇ ਵਰਤੋਂਕਾਰ",
"navigation_bar.preferences": "ਪਸੰਦਾਂ",
"navigation_bar.privacy_and_reach": "ਪਰਦੇਦਾਰੀ ਅਤੇ ਪਹੁੰਚ",
"navigation_bar.search": "ਖੋਜੋ",
"navigation_bar.search_trends": "ਖੋਜ / ਰੁਝਾਨ",
"not_signed_in_indicator.not_signed_in": "ਇਹ ਸਰੋਤ ਵਰਤਣ ਲਈ ਤੁਹਾਨੂੰ ਲਾਗਇਨ ਕਰਨ ਦੀ ਲੋੜ ਹੈ।",
"notification.admin.sign_up": "{name} ਨੇ ਸਾਈਨ ਅੱਪ ਕੀਤਾ",
"notification.favourite": "{name} ਨੇ ਤੁਹਾਡੀ ਪੋਸਟ ਨੂੰ ਪਸੰਦ ਕੀਤਾ",
"notification.favourite_pm": "{name} ਨੇ ਤੁਹਾਡੇ ਨਿੱਜੀ ਜ਼ਿਕਰ ਨੂੰ ਪਸੰਦ ਕੀਤਾ",
"notification.follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕੀਤਾ",
"notification.follow.name_and_others": "{name} ਅਤੇ <a>{count, plural, one {# ਹੋਰ} other {# ਹੋਰਾਂ}}</a> ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕੀਤਾ",
"notification.follow_request": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ",
@@ -365,6 +404,7 @@
"notification.moderation_warning.action_silence": "ਤੁਹਾਡੇ ਖਾਤੇ ਨੂੰ ਸੀਮਿਤ ਕੀਤਾ ਗਿਆ ਹੈ।",
"notification.moderation_warning.action_suspend": "ਤੁਹਾਡੇ ਖਾਤੇ ਨੂੰ ਮੁਅੱਤਲ ਕੀਤਾ ਗਿਆ ਹੈ।",
"notification.reblog": "{name} boosted your status",
"notification.relationships_severance_event": "{name} ਨਾਲ ਕਨੈਕਸ਼ਨ ਗੁਆਚੇ",
"notification.relationships_severance_event.learn_more": "ਹੋਰ ਜਾਣੋ",
"notification.status": "{name} ਨੇ ਹੁਣੇ ਪੋਸਟ ਕੀਤਾ",
"notification.update": "{name} ਨੋ ਪੋਸਟ ਨੂੰ ਸੋਧਿਆ",
@@ -540,7 +580,10 @@
"status.unpin": "ਪਰੋਫਾਈਲ ਤੋਂ ਲਾਹੋ",
"subscribed_languages.save": "ਤਬਦੀਲੀਆਂ ਸੰਭਾਲੋ",
"tabs_bar.home": "ਘਰ",
"tabs_bar.menu": "ਮੇਨੂ",
"tabs_bar.notifications": "ਸੂਚਨਾਵਾਂ",
"tabs_bar.publish": "ਨਵੀਂ ਪੋਸਟ",
"tabs_bar.search": "ਖੋਜੋ",
"terms_of_service.title": "ਸੇਵਾ ਦੀਆਂ ਸ਼ਰਤਾਂ",
"time_remaining.days": "{number, plural, one {# ਦਿਨ} other {# ਦਿਨ}} ਬਾਕੀ",
"time_remaining.hours": "{number, plural, one {# ਘੰਟਾ} other {# ਘੰਟੇ}} ਬਾਕੀ",
@@ -561,6 +604,9 @@
"video.expand": "ਵੀਡੀਓ ਨੂੰ ਫੈਲਾਓ",
"video.fullscreen": "ਪੂਰੀ ਸਕਰੀਨ",
"video.hide": "ਵੀਡੀਓ ਨੂੰ ਲੁਕਾਓ",
"video.mute": "ਮੌਨ",
"video.pause": "ਠਹਿਰੋ",
"video.play": "ਚਲਾਓ"
"video.play": "ਚਲਾਓ",
"video.volume_down": "ਅਵਾਜ਼ ਘਟਾਓ",
"video.volume_up": "ਅਵਾਜ਼ ਵਧਾਓ"
}

View File

@@ -537,8 +537,10 @@
"mute_modal.you_wont_see_mentions": "你看不到提及对方的嘟文。",
"mute_modal.you_wont_see_posts": "对方可以看到你的嘟文,但是你看不到对方的。",
"navigation_bar.about": "关于",
"navigation_bar.account_settings": "密码与安全",
"navigation_bar.administration": "管理",
"navigation_bar.advanced_interface": "在高级网页界面中打开",
"navigation_bar.automated_deletion": "自动删除嘟文",
"navigation_bar.blocks": "已屏蔽的用户",
"navigation_bar.bookmarks": "书签",
"navigation_bar.direct": "私下提及",

View File

@@ -318,6 +318,8 @@ et:
new:
create: Loo teadaanne
title: Uus teadaanne
preview:
title: Info teavituse üle vaatamine
publish: Postita
published_msg: Teadaande avaldamine õnnestus!
scheduled_for: Kavandatud ajaks %{time}
@@ -485,6 +487,7 @@ et:
request_body: Päringu sisu
providers:
active: Aktiivne
base_url: Baas-URL
callback: Pöördliiklus
delete: Kustuta
edit: Muuda teenusepakkujat
@@ -499,6 +502,7 @@ et:
reject: Keeldu
title: Kinnita FASP-i registreerimine
save: Salvesta
select_capabilities: Vali oskused
sign_in: Logi sisse
status: Olek
title: Täiendavad teenusepakkujad Födiversumis (FASP - Fediverse Auxiliary Service Providers)

View File

@@ -1220,9 +1220,9 @@ ga:
confirmation_dialogs: Dialóga deimhnithe
discovery: Fionnachtain
localization:
body: Aistríonn oibrithe deonacha Mastodon.
body: Oibrithe deonacha a dhéanann aistriúchán Mastodon.
guide_link: https://crowdin.com/project/mastodon
guide_link_text: Is féidir le gach duine rannchuidiú.
guide_link_text: Is féidir le gach duine cur leis.
sensitive_content: Ábhar íogair
application_mailer:
notification_preferences: Athraigh roghanna ríomhphoist

View File

@@ -454,6 +454,7 @@ nan:
title: 封鎖新ê電子phue網域
no_email_domain_block_selected: 因為無揀任何電子phue域名封鎖所以lóng無改變
not_permitted: 無允准
resolved_dns_records_hint_html: 域名解析做下kha ê MX域名tsiah ê域名上後負責收電子phue。封鎖MX域名ē封任何有siâng款MX域名ê電子郵件ê註冊就算通看見ê域名無kângmā án-ne。<strong>Tio̍h細膩m̄通封鎖主要ê電子phue提供者。</strong>
resolved_through_html: 通過 %{domain} 解析
title: 封鎖ê電子phue網域
export_domain_allows:
@@ -468,9 +469,78 @@ nan:
private_comment_template: 佇 %{date} tuì %{source} 輸入
title: 輸入域名封鎖
invalid_domain_block: 因為下kha ê錯誤làng過tsi̍t ê以上ê域名封鎖:%{error}
new:
title: 輸入域名封鎖
no_file: Iáu bē揀檔案
fasp:
debug:
callbacks:
created_at: 建立佇
delete: Thâi掉
ip: IP地址
request_body: 請求主文
title: 除蟲callback
providers:
active: 有效
base_url: 基本URL
callback: Callback
delete: Thâi掉
edit: 編輯提供者
finish_registration: 完成註冊
name:
providers: 提供者
public_key_fingerprint: 公開鎖匙ê指頭仔螺(public key fingerprint)
registration_requested: 註冊請求ah
registrations:
confirm: 確認
description: Lí收著FASP ê註冊ah。nā準lí bô啟動請拒絕。若有啟動請佇確認註冊以前細膩比較名kap鎖匙ê指頭仔螺。
reject: 拒絕
title: 確認FASP註冊
save: 儲存
select_capabilities: 揀功能
sign_in: 登入
status: 狀態
title: 聯邦宇宙輔助服務提供者 (FASP)
title: FASP
follow_recommendations:
description_html: "<strong>跟tuè建議幫tsān新用者緊tshuē著心適ê內容</strong>。Nā使用者無hām別lâng有夠額ê互動來形成個人化ê跟tuè建議就ē推薦tsiah ê口座。In是佇指定語言內底由最近上tsia̍p參與êkap上tsē lâng跟tuè ê口座用ta̍k kang做基礎相濫koh計算出來ê。"
language: 揀語言
status: 狀態
suppress: Khàm掉跟tuè建議
suppressed: Khàm掉ê
title: 跟tuè建議
unsuppress: 恢復跟tuè建議
instances:
audit_log:
title: 最近ê審核日誌
view_all: 看完整ê審核日誌
availability:
description_html:
other: Nā佇 <strong>%{count} kang</strong>內寄送kàu hit ê域名lóng失敗除非收著hit ê域名<em>來ê</em>寄送a̍h無buē koh試寄送。
failure_threshold_reached: 佇 %{date} kàu失敗ê底限。
failures_recorded:
other: 連suà %{count} kang lóng寄失敗。
no_failures_recorded: 報告內底無失敗。
title: 可用性
warning: 頂kái試連接tsit臺服侍器是無成功
back_to_all: 全部
back_to_limited: 受限制
back_to_warning: 警告
by_domain: 域名
confirm_purge: Lí kám確定beh永永thâi掉tsit ê域名來ê資料?
content_policies:
comment: 內部ê筆記
description_html: Lí ē當定義用tī所有tuì tsit ê域名kap伊ê子域名來ê口座ê內容政策。
dashboard:
instance_languages_dimension: Tsia̍p用ê語言
invites:
filter:
available: 通用ê
expired: 過期ê
title: 過濾器
title: 邀請
ip_blocks:
add_new: 建立規則
statuses:
language: 語言
trends:

View File

@@ -7,7 +7,13 @@ pa:
hosted_on: "%{domain} ਉੱਤੇ ਹੋਸਟ ਕੀਤਾ ਮਸਟਾਡੋਨ"
title: ਇਸ ਬਾਰੇ
accounts:
followers:
one: ਫ਼ਾਲੋਅਰ
other: ਫ਼ਾਲੋਅਰ
following: ਫ਼ਾਲੋ ਕੀਤੇ ਜਾ ਰਹੇ
posts:
one: ਪੋਸਟ
other: ਪੋਸਟਾਂ
posts_tab_heading: ਪੋਸਟਾਂ
admin:
account_moderation_notes:
@@ -126,6 +132,9 @@ pa:
thread: ਗੱਲਾਂਬਾਤਾਂ
index:
delete: ਹਟਾਓ
statuses:
one: "%{count} ਪੋਸਟ"
other: "%{count} ਪੋਸਟ"
generic:
all: ਸਭ
copy: ਕਾਪੀ ਕਰੋ

View File

@@ -61,7 +61,7 @@ es-MX:
setting_display_media_default: Ocultar contenido multimedia marcado como sensible
setting_display_media_hide_all: Siempre ocultar todo el contenido multimedia
setting_display_media_show_all: Mostrar siempre contenido multimedia marcado como sensible
setting_emoji_style: Cómo se mostran los emojis. "Auto" intentará usar emojis nativos, cambiando a Twemoji en navegadores antiguos.
setting_emoji_style: Cómo se muestran los emojis. «Automático» intentará usar emojis nativos, pero vuelve a Twemoji para los navegadores antiguos.
setting_system_scrollbars_ui: Solo se aplica a los navegadores de escritorio basados en Safari y Chrome
setting_use_blurhash: Los degradados se basan en los colores de los elementos visuales ocultos, pero ocultan cualquier detalle
setting_use_pending_items: Ocultar las publicaciones de la línea de tiempo tras un clic en lugar de desplazar automáticamente el feed
@@ -151,8 +151,8 @@ es-MX:
user:
chosen_languages: Cuando se marca, solo se mostrarán las publicaciones en los idiomas seleccionados en las líneas de tiempo públicas
date_of_birth:
one: Tenemos que asegurarnos de que tienes al menos %{count} para usar %{domain}. No guardaremos esta información.
other: Tenemos que asegurarnos de que tienes al menos %{count} para usar %{domain}. No guardaremos esta información.
one: Tenemos que asegurarnos de que tienes al menos %{count} para usar %{domain}. No almacenaremos esta información.
other: Tenemos que asegurarnos de que tienes al menos %{count} para usar %{domain}. No almacenaremos esta información.
role: El rol controla qué permisos tiene el usuario.
user_role:
color: Color que se usará para el rol en toda la interfaz de usuario, como RGB en formato hexadecimal

View File

@@ -0,0 +1,84 @@
// Heavily inspired by https://github.com/ElMassimo/vite_ruby
import { createHash } from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import glob from 'fast-glob';
import type { Plugin } from 'vite';
interface AssetManifestChunk {
file: string;
integrity: string;
}
const ALGORITHM = 'sha384';
export function MastodonAssetsManifest(): Plugin {
let manifest: string | boolean = true;
let jsRoot = '';
return {
name: 'mastodon-assets-manifest',
applyToEnvironment(environment) {
return !!environment.config.build.manifest;
},
configResolved(resolvedConfig) {
manifest = resolvedConfig.build.manifest;
jsRoot = resolvedConfig.root;
},
async generateBundle() {
// Glob all assets and return an array of absolute paths.
const assetPaths = await glob('{fonts,icons,images}/**/*', {
cwd: jsRoot,
absolute: true,
});
const assetManifest: Record<string, AssetManifestChunk> = {};
const excludeExts = ['', '.md'];
for (const file of assetPaths) {
// Exclude files like markdown or README files with no extension.
const ext = path.extname(file);
if (excludeExts.includes(ext)) {
continue;
}
// Read the file and emit it as an asset.
const contents = await fs.readFile(file);
const ref = this.emitFile({
name: path.basename(file),
type: 'asset',
source: contents,
});
const hashedFilename = this.getFileName(ref);
// With the emitted file information, hash the contents and store in manifest.
const name = path.relative(jsRoot, file);
const hash = createHash(ALGORITHM)
.update(contents)
.digest()
.toString('base64');
assetManifest[name] = {
file: hashedFilename,
integrity: `${ALGORITHM}-${hash}`,
};
}
if (Object.keys(assetManifest).length === 0) {
console.warn('Asset manifest is empty');
return;
}
// Get manifest location and emit the manifest.
const manifestDir =
typeof manifest === 'string' ? path.dirname(manifest) : '.vite';
const fileName = `${manifestDir}/manifest-assets.json`;
this.emitFile({
fileName,
type: 'asset',
source: JSON.stringify(assetManifest, null, 2),
});
},
};
}

View File

@@ -90,7 +90,6 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-hotkeys": "^1.1.4",
"react-immutable-proptypes": "^2.2.0",
"react-immutable-pure-component": "^2.2.2",
"react-intl": "^7.1.10",
@@ -165,12 +164,12 @@
"@vitest/browser": "^3.2.1",
"@vitest/coverage-v8": "^3.2.0",
"@vitest/ui": "^3.2.1",
"chromatic": "^12.1.0",
"chromatic": "^13.0.0",
"eslint": "^9.23.0",
"eslint-import-resolver-typescript": "^4.2.5",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-import": "~2.31.0",
"eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-jsdoc": "^51.0.0",
"eslint-plugin-jsx-a11y": "~6.10.2",
"eslint-plugin-promise": "~7.2.1",
"eslint-plugin-react": "^7.37.4",
@@ -181,7 +180,7 @@
"lint-staged": "^16.0.0",
"msw": "^2.10.2",
"msw-storybook-addon": "^2.0.5",
"playwright": "^1.52.0",
"playwright": "^1.54.1",
"prettier": "^3.3.3",
"react-test-renderer": "^18.2.0",
"storybook": "^9.0.4",

View File

@@ -4,7 +4,6 @@ import { readdir } from 'node:fs/promises';
import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
import legacy from '@vitejs/plugin-legacy';
import react from '@vitejs/plugin-react';
import glob from 'fast-glob';
import postcssPresetEnv from 'postcss-preset-env';
import Compress from 'rollup-plugin-gzip';
import { visualizer } from 'rollup-plugin-visualizer';
@@ -24,6 +23,7 @@ import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales';
import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed';
import { GlitchThemes } from './config/vite/plugin-glitch-themes';
import { MastodonNameLookup } from './config/vite/plugin-name-lookup';
import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest';
const jsRoot = path.resolve(__dirname, 'app/javascript');
@@ -120,6 +120,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
},
}),
GlitchThemes(),
MastodonAssetsManifest(),
viteStaticCopy({
targets: [
{
@@ -144,7 +145,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
isProdBuild && (Compress() as PluginOption),
command === 'build' &&
manifestSRI({
manifestPaths: ['.vite/manifest.json', '.vite/manifest-assets.json'],
manifestPaths: ['.vite/manifest.json'],
}),
VitePWA({
srcDir: path.resolve(jsRoot, 'mastodon/service_worker'),
@@ -211,21 +212,6 @@ async function findEntrypoints() {
}
}
// Lastly other assets
const assetEntrypoints = await glob('{fonts,icons,images}/**/*', {
cwd: jsRoot,
absolute: true,
});
const excludeExts = ['', '.md'];
for (const file of assetEntrypoints) {
const ext = path.extname(file);
if (excludeExts.includes(ext)) {
continue;
}
const name = path.basename(file);
entrypoints[name] = path.resolve(jsRoot, file);
}
return entrypoints;
}

212
yarn.lock
View File

@@ -1932,14 +1932,16 @@ __metadata:
languageName: node
linkType: hard
"@es-joy/jsdoccomment@npm:~0.49.0":
version: 0.49.0
resolution: "@es-joy/jsdoccomment@npm:0.49.0"
"@es-joy/jsdoccomment@npm:~0.52.0":
version: 0.52.0
resolution: "@es-joy/jsdoccomment@npm:0.52.0"
dependencies:
"@types/estree": "npm:^1.0.8"
"@typescript-eslint/types": "npm:^8.34.1"
comment-parser: "npm:1.4.1"
esquery: "npm:^1.6.0"
jsdoc-type-pratt-parser: "npm:~4.1.0"
checksum: 10c0/16717507d557d37e7b59456fedeefbe0a3bc93aa2d9c043d5db91e24e076509b6fcb10ee6fd1dafcb0c5bbe50ae329b45de5b83541cb5994a98c9e862a45641e
checksum: 10c0/4def78060ef58859f31757b9d30c4939fc33e7d9ee85637a7f568c1d209c33aa0abd2cf5a3a4f3662ec5b12b85ecff2f2035d809dc93b9382a31a6dfb200d83c
languageName: node
linkType: hard
@@ -2660,7 +2662,7 @@ __metadata:
babel-plugin-formatjs: "npm:^10.5.37"
babel-plugin-transform-react-remove-prop-types: "npm:^0.4.24"
blurhash: "npm:^2.0.5"
chromatic: "npm:^12.1.0"
chromatic: "npm:^13.0.0"
classnames: "npm:^2.3.2"
cocoon-js-vanilla: "npm:^1.5.1"
color-blend: "npm:^4.0.0"
@@ -2675,7 +2677,7 @@ __metadata:
eslint-import-resolver-typescript: "npm:^4.2.5"
eslint-plugin-formatjs: "npm:^5.3.1"
eslint-plugin-import: "npm:~2.31.0"
eslint-plugin-jsdoc: "npm:^50.6.9"
eslint-plugin-jsdoc: "npm:^51.0.0"
eslint-plugin-jsx-a11y: "npm:~6.10.2"
eslint-plugin-promise: "npm:~7.2.1"
eslint-plugin-react: "npm:^7.37.4"
@@ -2700,7 +2702,7 @@ __metadata:
msw: "npm:^2.10.2"
msw-storybook-addon: "npm:^2.0.5"
path-complete-extname: "npm:^1.0.0"
playwright: "npm:^1.52.0"
playwright: "npm:^1.54.1"
postcss-preset-env: "npm:^10.1.5"
prettier: "npm:^3.3.3"
prop-types: "npm:^15.8.1"
@@ -2708,7 +2710,6 @@ __metadata:
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-helmet: "npm:^6.1.0"
react-hotkeys: "npm:^1.1.4"
react-immutable-proptypes: "npm:^2.2.0"
react-immutable-pure-component: "npm:^2.2.2"
react-intl: "npm:^7.1.10"
@@ -3091,13 +3092,6 @@ __metadata:
languageName: node
linkType: hard
"@pkgr/core@npm:^0.1.0":
version: 0.1.1
resolution: "@pkgr/core@npm:0.1.1"
checksum: 10c0/3f7536bc7f57320ab2cf96f8973664bef624710c403357429fbf680a5c3b4843c1dbd389bb43daa6b1f6f1f007bb082f5abcb76bb2b5dc9f421647743b71d3d8
languageName: node
linkType: hard
"@polka/url@npm:^1.0.0-next.24":
version: 1.0.0-next.29
resolution: "@polka/url@npm:1.0.0-next.29"
@@ -3988,10 +3982,10 @@ __metadata:
languageName: node
linkType: hard
"@types/estree@npm:*, @types/estree@npm:1.0.7, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6":
version: 1.0.7
resolution: "@types/estree@npm:1.0.7"
checksum: 10c0/be815254316882f7c40847336cd484c3bc1c3e34f710d197160d455dc9d6d050ffbf4c3bc76585dba86f737f020ab20bdb137ebe0e9116b0c86c7c0342221b8c
"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8":
version: 1.0.8
resolution: "@types/estree@npm:1.0.8"
checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5
languageName: node
linkType: hard
@@ -4002,6 +3996,13 @@ __metadata:
languageName: node
linkType: hard
"@types/estree@npm:1.0.7":
version: 1.0.7
resolution: "@types/estree@npm:1.0.7"
checksum: 10c0/be815254316882f7c40847336cd484c3bc1c3e34f710d197160d455dc9d6d050ffbf4c3bc76585dba86f737f020ab20bdb137ebe0e9116b0c86c7c0342221b8c
languageName: node
linkType: hard
"@types/express-serve-static-core@npm:^4.17.33":
version: 4.17.41
resolution: "@types/express-serve-static-core@npm:4.17.41"
@@ -4504,13 +4505,20 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.33.0, @typescript-eslint/types@npm:^8.33.0":
"@typescript-eslint/types@npm:8.33.0":
version: 8.33.0
resolution: "@typescript-eslint/types@npm:8.33.0"
checksum: 10c0/348b64eb408719d7711a433fc9716e0c2aab8b3f3676f5a1cc2e00269044132282cf655deb6d0dd9817544116909513de3b709005352d186949d1014fad1a3cb
languageName: node
linkType: hard
"@typescript-eslint/types@npm:^8.33.0, @typescript-eslint/types@npm:^8.34.1":
version: 8.36.0
resolution: "@typescript-eslint/types@npm:8.36.0"
checksum: 10c0/cacb941a0caad6ab556c416051b97ec33b364b7c8e0703e2729ae43f12daf02b42eef12011705329107752e3f1685ca82cfffe181d637f85907293cb634bee31
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.29.1":
version: 8.29.1
resolution: "@typescript-eslint/typescript-estree@npm:8.29.1"
@@ -4999,12 +5007,12 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.14.0, acorn@npm:^8.8.2":
version: 8.14.1
resolution: "acorn@npm:8.14.1"
"acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.8.2":
version: 8.15.0
resolution: "acorn@npm:8.15.0"
bin:
acorn: bin/acorn
checksum: 10c0/dbd36c1ed1d2fa3550140000371fcf721578095b18777b85a79df231ca093b08edc6858d75d6e48c73e431c174dcf9214edbd7e6fa5911b93bd8abfa54e47123
checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec
languageName: node
linkType: hard
@@ -5389,13 +5397,13 @@ __metadata:
linkType: hard
"axios@npm:^1.4.0":
version: 1.9.0
resolution: "axios@npm:1.9.0"
version: 1.10.0
resolution: "axios@npm:1.10.0"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/9371a56886c2e43e4ff5647b5c2c3c046ed0a3d13482ef1d0135b994a628c41fbad459796f101c655e62f0c161d03883454474d2e435b2e021b1924d9f24994c
checksum: 10c0/2239cb269cc789eac22f5d1aabd58e1a83f8f364c92c2caa97b6f5cbb4ab2903d2e557d9dc670b5813e9bcdebfb149e783fb8ab3e45098635cd2f559b06bd5d8
languageName: node
linkType: hard
@@ -5825,9 +5833,9 @@ __metadata:
languageName: node
linkType: hard
"chromatic@npm:^12.1.0":
version: 12.1.0
resolution: "chromatic@npm:12.1.0"
"chromatic@npm:^13.0.0":
version: 13.0.0
resolution: "chromatic@npm:13.0.0"
peerDependencies:
"@chromatic-com/cypress": ^0.*.* || ^1.0.0
"@chromatic-com/playwright": ^0.*.* || ^1.0.0
@@ -5840,7 +5848,7 @@ __metadata:
chroma: dist/bin.js
chromatic: dist/bin.js
chromatic-cli: dist/bin.js
checksum: 10c0/4acb70a4a84605f1963a823beed4f3062ec91e373104500f4295af2298b8d0b49f864d06ca81bc9389e44cae3a284332aac07c6cbfc123aa6457f3b52a4c4b78
checksum: 10c0/30c697eb84d5b3b8cdab989df0e4fed0bf51f4bfefb616873f68fc00337978b9b38b84e52af22861769176181bd98525d467baeb22daa712a0f7a58bd61bf336
languageName: node
linkType: hard
@@ -6790,7 +6798,7 @@ __metadata:
languageName: node
linkType: hard
"es-module-lexer@npm:^1.5.3, es-module-lexer@npm:^1.7.0":
"es-module-lexer@npm:^1.7.0":
version: 1.7.0
resolution: "es-module-lexer@npm:1.7.0"
checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b
@@ -7050,24 +7058,23 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-jsdoc@npm:^50.6.9":
version: 50.6.9
resolution: "eslint-plugin-jsdoc@npm:50.6.9"
"eslint-plugin-jsdoc@npm:^51.0.0":
version: 51.3.4
resolution: "eslint-plugin-jsdoc@npm:51.3.4"
dependencies:
"@es-joy/jsdoccomment": "npm:~0.49.0"
"@es-joy/jsdoccomment": "npm:~0.52.0"
are-docs-informative: "npm:^0.0.2"
comment-parser: "npm:1.4.1"
debug: "npm:^4.3.6"
debug: "npm:^4.4.1"
escape-string-regexp: "npm:^4.0.0"
espree: "npm:^10.1.0"
espree: "npm:^10.4.0"
esquery: "npm:^1.6.0"
parse-imports: "npm:^2.1.1"
semver: "npm:^7.6.3"
parse-imports-exports: "npm:^0.2.4"
semver: "npm:^7.7.2"
spdx-expression-parse: "npm:^4.0.0"
synckit: "npm:^0.9.1"
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
checksum: 10c0/cad199d262c2e889a3af4e402f6adc624e4273b3d5ca1940e7227b37d87af8090ca3444f7fff57f58dab9a827faed8722fc2f5d4daf31ec085eb00e9f5a338a7
checksum: 10c0/59e5aa972bdd1bd4e2ca2796ed4455dff1069044abc028621e107aa4b0cbb62ce09554c8e7c2ff3a44a1cbd551e54b6970adc420ba3a89adc6236b94310a81ff
languageName: node
linkType: hard
@@ -7173,10 +7180,10 @@ __metadata:
languageName: node
linkType: hard
"eslint-visitor-keys@npm:^4.2.0":
version: 4.2.0
resolution: "eslint-visitor-keys@npm:4.2.0"
checksum: 10c0/2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269
"eslint-visitor-keys@npm:^4.2.0, eslint-visitor-keys@npm:^4.2.1":
version: 4.2.1
resolution: "eslint-visitor-keys@npm:4.2.1"
checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43
languageName: node
linkType: hard
@@ -7230,14 +7237,14 @@ __metadata:
languageName: node
linkType: hard
"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.3.0":
version: 10.3.0
resolution: "espree@npm:10.3.0"
"espree@npm:^10.0.1, espree@npm:^10.3.0, espree@npm:^10.4.0":
version: 10.4.0
resolution: "espree@npm:10.4.0"
dependencies:
acorn: "npm:^8.14.0"
acorn: "npm:^8.15.0"
acorn-jsx: "npm:^5.3.2"
eslint-visitor-keys: "npm:^4.2.0"
checksum: 10c0/272beeaca70d0a1a047d61baff64db04664a33d7cfb5d144f84bc8a5c6194c6c8ebe9cc594093ca53add88baa23e59b01e69e8a0160ab32eac570482e165c462
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b
languageName: node
linkType: hard
@@ -9188,27 +9195,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.isboolean@npm:^3.0.3":
version: 3.0.3
resolution: "lodash.isboolean@npm:3.0.3"
checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7
languageName: node
linkType: hard
"lodash.isequal@npm:^4.5.0":
version: 4.5.0
resolution: "lodash.isequal@npm:4.5.0"
checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f
languageName: node
linkType: hard
"lodash.isobject@npm:^3.0.2":
version: 3.0.2
resolution: "lodash.isobject@npm:3.0.2"
checksum: 10c0/da4c8480d98b16835b59380b2fbd43c54081acd9466febb788ba77c434384349e0bec162d1c4e89f613f21687b2b6d8384d8a112b80da00c78d28d9915a5cdde
languageName: node
linkType: hard
"lodash.merge@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
@@ -9619,13 +9605,6 @@ __metadata:
languageName: node
linkType: hard
"mousetrap@npm:^1.5.2":
version: 1.6.5
resolution: "mousetrap@npm:1.6.5"
checksum: 10c0/5c361bdbbff3966fd58d70f39b9fe1f8e32c78f3ce65989d83af7aad32a3a95313ce835a8dd8a55cb5de9eeb7c1f0c2b9048631a3073b5606241589e8fc0ba53
languageName: node
linkType: hard
"mrmime@npm:^2.0.0":
version: 2.0.1
resolution: "mrmime@npm:2.0.1"
@@ -10048,13 +10027,12 @@ __metadata:
languageName: node
linkType: hard
"parse-imports@npm:^2.1.1":
version: 2.1.1
resolution: "parse-imports@npm:2.1.1"
"parse-imports-exports@npm:^0.2.4":
version: 0.2.4
resolution: "parse-imports-exports@npm:0.2.4"
dependencies:
es-module-lexer: "npm:^1.5.3"
slashes: "npm:^3.0.12"
checksum: 10c0/c9bb0b4e1823f84f034d2d7bd2b37415b1715a5c963fda14968c706186b48b02c10e97d04bce042b9dcd679b42f29c391ea120799ddf581c7f54786edd99e3a9
parse-statements: "npm:1.0.11"
checksum: 10c0/51b729037208abdf65c4a1f8e9ed06f4e7ccd907c17c668a64db54b37d95bb9e92081f8b16e4133e14102af3cb4e89870975b6ad661b4d654e9ec8f4fb5c77d6
languageName: node
linkType: hard
@@ -10070,6 +10048,13 @@ __metadata:
languageName: node
linkType: hard
"parse-statements@npm:1.0.11":
version: 1.0.11
resolution: "parse-statements@npm:1.0.11"
checksum: 10c0/48960e085019068a5f5242e875fd9d21ec87df2e291acf5ad4e4887b40eab6929a8c8d59542acb85a6497e870c5c6a24f5ab7f980ef5f907c14cc5f7984a93f3
languageName: node
linkType: hard
"parse5@npm:^7.2.1":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
@@ -10358,27 +10343,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.52.0":
version: 1.52.0
resolution: "playwright-core@npm:1.52.0"
"playwright-core@npm:1.54.1":
version: 1.54.1
resolution: "playwright-core@npm:1.54.1"
bin:
playwright-core: cli.js
checksum: 10c0/640945507e6ca2144e9f596b2a6ecac042c2fd3683ff99e6271e9a7b38f3602d415f282609d569456f66680aab8b3c5bb1b257d8fb63a7fc0ed648261110421f
checksum: 10c0/b821262b024d7753b1bfa71eb2bc99f2dda12a869d175b2e1bc6ac2764bd661baf36d9d42f45caf622854ad7e4a6077b9b57014c74bb5a78fe339c9edf1c9019
languageName: node
linkType: hard
"playwright@npm:^1.52.0":
version: 1.52.0
resolution: "playwright@npm:1.52.0"
"playwright@npm:^1.54.1":
version: 1.54.1
resolution: "playwright@npm:1.54.1"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.52.0"
playwright-core: "npm:1.54.1"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10c0/2c6edf1e15e59bbaf77f3fa0fe0ac975793c17cff835d9c8b8bc6395a3b6f1c01898b3058ab37891b2e4d424bcc8f1b4844fe70d943e0143d239d7451408c579
checksum: 10c0/c5fedae31a03a1f4c4846569aef3ffb98da23000a4d255abfc8c2ede15b43cc7cd87b80f6fa078666c030373de8103787cf77ef7653ae9458aabbbd4320c2599
languageName: node
linkType: hard
@@ -11131,22 +11116,6 @@ __metadata:
languageName: node
linkType: hard
"react-hotkeys@npm:^1.1.4":
version: 1.1.4
resolution: "react-hotkeys@npm:1.1.4"
dependencies:
lodash.isboolean: "npm:^3.0.3"
lodash.isequal: "npm:^4.5.0"
lodash.isobject: "npm:^3.0.2"
mousetrap: "npm:^1.5.2"
prop-types: "npm:^15.6.0"
peerDependencies:
react: ">= 0.14.0"
react-dom: ">= 0.14.0"
checksum: 10c0/6bd566ea97e00058749d43d768ee843e5132f988571536e090b564d5dbaa71093695255514fc5b9fcf9fbd03fcb0603f6e135dcab6dcaaffe43dedbfe742a163
languageName: node
linkType: hard
"react-immutable-proptypes@npm:^2.2.0":
version: 2.2.0
resolution: "react-immutable-proptypes@npm:2.2.0"
@@ -12092,7 +12061,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1":
"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.7.1, semver@npm:^7.7.2":
version: 7.7.2
resolution: "semver@npm:7.7.2"
bin:
@@ -12297,13 +12266,6 @@ __metadata:
languageName: node
linkType: hard
"slashes@npm:^3.0.12":
version: 3.0.12
resolution: "slashes@npm:3.0.12"
checksum: 10c0/71ca2a1fcd1ab6814b0fdb8cf9c33a3d54321deec2aa8d173510f0086880201446021a9b9e6a18561f7c472b69a2145977c6a8fb9c53a8ff7be31778f203d175
languageName: node
linkType: hard
"slice-ansi@npm:^4.0.0":
version: 4.0.0
resolution: "slice-ansi@npm:4.0.0"
@@ -12991,16 +12953,6 @@ __metadata:
languageName: node
linkType: hard
"synckit@npm:^0.9.1":
version: 0.9.1
resolution: "synckit@npm:0.9.1"
dependencies:
"@pkgr/core": "npm:^0.1.0"
tslib: "npm:^2.6.2"
checksum: 10c0/d8b89e1bf30ba3ffb469d8418c836ad9c0c062bf47028406b4d06548bc66af97155ea2303b96c93bf5c7c0f0d66153a6fbd6924c76521b434e6a9898982abc2e
languageName: node
linkType: hard
"systemjs@npm:^6.15.1":
version: 6.15.1
resolution: "systemjs@npm:6.15.1"
@@ -13341,7 +13293,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.0":
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.8.0":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62