From 4de5cbd6f56fe0b8fe1a4e71c4d50839da6c4c26 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Mon, 21 Jul 2025 16:43:38 +0200 Subject: [PATCH 01/12] refactor: Replace react-hotkeys with custom hook (#35425) --- .github/renovate.json5 | 1 - .../components/hotkeys/hotkeys.stories.tsx | 171 +++++++++++ .../mastodon/components/hotkeys/index.tsx | 282 ++++++++++++++++++ .../mastodon/components/hotkeys/utils.ts | 29 ++ app/javascript/mastodon/components/status.jsx | 16 +- .../mastodon/components/status_list.jsx | 3 +- .../compose/components/compose_form.jsx | 5 +- .../components/conversation.jsx | 8 +- .../notifications/components/notification.jsx | 46 +-- .../components/notification_group.tsx | 5 +- .../notification_group_with_status.tsx | 7 +- .../components/notification_with_status.tsx | 7 +- .../mastodon/features/status/index.jsx | 7 +- app/javascript/mastodon/features/ui/index.jsx | 53 +--- package.json | 1 - yarn.lock | 45 --- 16 files changed, 540 insertions(+), 146 deletions(-) create mode 100644 app/javascript/mastodon/components/hotkeys/hotkeys.stories.tsx create mode 100644 app/javascript/mastodon/components/hotkeys/index.tsx create mode 100644 app/javascript/mastodon/components/hotkeys/utils.ts diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 1850a45bbc..07400a07a4 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -23,7 +23,6 @@ matchManagers: ['npm'], matchPackageNames: [ 'tesseract.js', // Requires code changes - 'react-hotkeys', // Requires code changes // react-router: Requires manual upgrade 'history', diff --git a/app/javascript/mastodon/components/hotkeys/hotkeys.stories.tsx b/app/javascript/mastodon/components/hotkeys/hotkeys.stories.tsx new file mode 100644 index 0000000000..b95c9410e1 --- /dev/null +++ b/app/javascript/mastodon/components/hotkeys/hotkeys.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +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( + 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 ( + +
+

+ Hotkey playground +

+

+ Last pressed hotkey: {matchedHotkey ?? 'None'} +

+

+ Click within the dashed border and press the "n + " or "/" key. Press " + Backspace" to clear the displayed hotkey. +

+

+ Try typing a sequence, like "g" shortly + followed by "h", "n", or + "f" +

+

+ Note that this playground doesn't support all hotkeys we use in + the app. +

+

+ When a is focused, " + Enter + " should not trigger "open", but "o + " should. +

+

+ When an input element is focused, hotkeys should not interfere with + regular typing: +

+ +
+
+ ); + }, + play: hotkeyTest, +}; diff --git a/app/javascript/mastodon/components/hotkeys/index.tsx b/app/javascript/mastodon/components/hotkeys/index.tsx new file mode 100644 index 0000000000..b5e0de4c59 --- /dev/null +++ b/app/javascript/mastodon/components/hotkeys/index.tsx @@ -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 void> +>; + +export function useHotkeys(handlers: HandlerMap) { + const ref = useRef(null); + const bufferedKeys = useRef([]); + const sequenceTimer = useRef | 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 `` 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 + * + * ``` + * + * 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(handlers); + + return ( +
+ {children} +
+ ); +}; diff --git a/app/javascript/mastodon/components/hotkeys/utils.ts b/app/javascript/mastodon/components/hotkeys/utils.ts new file mode 100644 index 0000000000..1430e1685b --- /dev/null +++ b/app/javascript/mastodon/components/hotkeys/utils.ts @@ -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; + } +} diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 29fd4234dd..171ae780ff 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -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 ( - +
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.get('spoiler_text').length > 0 && ({status.get('spoiler_text')})} {expanded && {status.get('content')}}
-
+
); } @@ -543,7 +541,7 @@ class Status extends ImmutablePureComponent { const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); return ( - +
{!skipPrepend && prepend} @@ -604,7 +602,7 @@ class Status extends ImmutablePureComponent { }
-
+
); } diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index 390659e9b6..cca449b0ca 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -56,7 +56,7 @@ export default class StatusList extends ImmutablePureComponent { const elementIndex = this.getCurrentStatusIndex(id, featured) - 1; this._selectChild(elementIndex, true); }; - + handleMoveDown = (id, featured) => { const elementIndex = this.getCurrentStatusIndex(id, featured) + 1; this._selectChild(elementIndex, false); @@ -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) { diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index 6dd3dbd054..b563a6b45d 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -93,9 +93,12 @@ class ComposeForm extends ImmutablePureComponent { }; handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + if (e.key.toLowerCase() === 'enter' && (e.ctrlKey || e.metaKey)) { this.handleSubmit(); } + if (['esc', 'escape'].includes(e.key.toLowerCase())) { + this.textareaRef.current?.blur(); + } }; getFulltextForCharacterCounting = () => { diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index c27cd3727f..ec3621f0c0 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -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 ( - +
@@ -219,7 +217,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
-
+
); }; diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index 86431f62fd..b38e5da159 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -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 ( - +
@@ -149,7 +149,7 @@ class Notification extends ImmutablePureComponent {
- + ); } @@ -157,7 +157,7 @@ class Notification extends ImmutablePureComponent { const { intl, unread } = this.props; return ( - +
@@ -169,7 +169,7 @@ class Notification extends ImmutablePureComponent {
- + ); } @@ -195,7 +195,7 @@ class Notification extends ImmutablePureComponent { const { intl, unread } = this.props; return ( - +
@@ -217,7 +217,7 @@ class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} />
- + ); } @@ -225,7 +225,7 @@ class Notification extends ImmutablePureComponent { const { intl, unread } = this.props; return ( - +
@@ -247,7 +247,7 @@ class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} />
- + ); } @@ -259,7 +259,7 @@ class Notification extends ImmutablePureComponent { } return ( - +
@@ -282,7 +282,7 @@ class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} />
- + ); } @@ -294,7 +294,7 @@ class Notification extends ImmutablePureComponent { } return ( - +
@@ -317,7 +317,7 @@ class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} />
- + ); } @@ -331,7 +331,7 @@ class Notification extends ImmutablePureComponent { } return ( - +
@@ -358,7 +358,7 @@ class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} />
- + ); } @@ -371,7 +371,7 @@ class Notification extends ImmutablePureComponent { } return ( - +
-
+
); } @@ -394,7 +394,7 @@ class Notification extends ImmutablePureComponent { } return ( - +
-
+
); } @@ -410,7 +410,7 @@ class Notification extends ImmutablePureComponent { const { intl, unread } = this.props; return ( - +
@@ -422,7 +422,7 @@ class Notification extends ImmutablePureComponent {
- + ); } @@ -438,7 +438,7 @@ class Notification extends ImmutablePureComponent { const targetLink = ; return ( - +
@@ -450,7 +450,7 @@ class Notification extends ImmutablePureComponent {
- + ); } diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx index d5eb851985..f0f2139ad2 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx @@ -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 {content}; + return {content}; }; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx index e7ed8792f6..4be1eefcdd 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_group_with_status.tsx @@ -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 ( - +
-
+
); }; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx index de484322fb..96a4a4d65d 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx @@ -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 ( - +
-
+
); }; diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 0f02e7b50f..64cd0c4f82 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -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 {
{ancestors} - +
-
+
{descendants} {remoteHint} diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index c9834eb0a4..e8eef704ef 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -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 ( - +
{children} @@ -611,7 +574,7 @@ class UI extends PureComponent {
-
+
); } diff --git a/package.json b/package.json index 2ddb85b763..f19a5564f8 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,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", diff --git a/yarn.lock b/yarn.lock index 3575fbedde..3ede60941a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2706,7 +2706,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" @@ -9172,27 +9171,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" @@ -9603,13 +9581,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" @@ -11115,22 +11086,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" From ee21f7221143d94aac00a6cbadea352ebfe44905 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Mon, 21 Jul 2025 17:57:20 +0200 Subject: [PATCH 02/12] fix: Don't submit post when pressing Enter in CW field (#35445) --- .../compose/components/compose_form.jsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index b563a6b45d..5bc77c4bcd 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -92,13 +92,29 @@ class ComposeForm extends ImmutablePureComponent { this.props.onChange(e.target.value); }; - handleKeyDown = (e) => { - if (e.key.toLowerCase() === 'enter' && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } + blurOnEscape = (e) => { if (['esc', 'escape'].includes(e.key.toLowerCase())) { - this.textareaRef.current?.blur(); + 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 = () => { @@ -251,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} @@ -276,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} From 20b3c43ddee386e603e4c8c519b1df9464803d11 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 21 Jul 2025 12:24:55 -0400 Subject: [PATCH 03/12] Update playwright-ruby-client to version 1.54.0 (#35448) --- Gemfile.lock | 6 +++--- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1c187ac9df..094c9b1d6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/package.json b/package.json index f19a5564f8..70ae4cd331 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,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", diff --git a/yarn.lock b/yarn.lock index 3ede60941a..f44bce6efc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2698,7 +2698,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" @@ -10313,27 +10313,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 From bf17895d19dcd77ca4b16f54fadc1620d79557f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:28:01 +0200 Subject: [PATCH 04/12] chore(deps): update dependency chromatic to v13 (#35064) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 70ae4cd331..a6ee9cef00 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "@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", diff --git a/yarn.lock b/yarn.lock index f44bce6efc..94ed02e674 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2659,7 +2659,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" @@ -5815,9 +5815,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 @@ -5830,7 +5830,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 From 1ed58aaaf2f5296102197e1b5adda4f769312bf4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:28:49 +0200 Subject: [PATCH 05/12] fix(deps): update dependency axios to v1.10.0 (#35050) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 94ed02e674..c4d3179b76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5379,13 +5379,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 From ae13063460a4b7ccc744a31858a11a8aac1a571d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:31:10 +0000 Subject: [PATCH 06/12] chore(deps): update dependency eslint-plugin-jsdoc to v51 (#35026) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 131 +++++++++++++++++++++++++-------------------------- 2 files changed, 65 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index a6ee9cef00..e1085909b7 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,7 @@ "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", diff --git a/yarn.lock b/yarn.lock index c4d3179b76..598b15acb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 @@ -2674,7 +2676,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" @@ -3088,13 +3090,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" @@ -3985,10 +3980,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 @@ -3999,6 +3994,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" @@ -4501,13 +4503,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" @@ -4996,12 +5005,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 @@ -6780,7 +6789,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 @@ -7040,24 +7049,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 @@ -7163,10 +7171,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 @@ -7220,14 +7228,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 @@ -10003,13 +10011,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 @@ -10025,6 +10032,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" @@ -12031,7 +12045,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: @@ -12236,13 +12250,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" @@ -12930,16 +12937,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" @@ -13280,7 +13277,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 From be3dc5b50857021fe164733ac4c998d35d72b4fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:10:37 +0200 Subject: [PATCH 07/12] New Crowdin Translations (automated) (#35453) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/pa.json | 50 +++++++++++++++- app/javascript/mastodon/locales/zh-CN.json | 2 + config/locales/et.yml | 4 ++ config/locales/ga.yml | 4 +- config/locales/nan.yml | 70 ++++++++++++++++++++++ config/locales/pa.yml | 9 +++ config/locales/simple_form.es-MX.yml | 6 +- 7 files changed, 138 insertions(+), 7 deletions(-) diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 8ab4eb2252..5236d246c0 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -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} เจ…เจคเฉ‡ {count, plural, one {# เจนเฉ‹เจฐ} other {# เจนเฉ‹เจฐเจพเจ‚}} เจจเฉ‡ เจคเฉเจนเจพเจจเฉ‚เฉฐ เจซเจผเจพเจฒเฉ‹ เจ•เฉ€เจคเจพ", "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": "เจ…เจตเจพเฉ› เจตเจงเจพเจ“" } diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 57958076a7..8928f253b1 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -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": "็งไธ‹ๆๅŠ", diff --git a/config/locales/et.yml b/config/locales/et.yml index dba31f4ffd..962f6be9db 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -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) diff --git a/config/locales/ga.yml b/config/locales/ga.yml index c9e943c91c..111ae9c56f 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -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 diff --git a/config/locales/nan.yml b/config/locales/nan.yml index 28c98a58d6..354ef8b0f8 100644 --- a/config/locales/nan.yml +++ b/config/locales/nan.yml @@ -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รขng๏ผŒmฤ รกn-neใ€‚Tioฬh็ดฐ่†ฉ๏ผŒmฬ„้€šๅฐ้Ž–ไธป่ฆรช้›ปๅญphueๆไพ›่€…ใ€‚ 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: "่ทŸtuรจๅปบ่ญฐๅนซtsฤnๆ–ฐ็”จ่€…็ทŠtshuฤ“่‘—ๅฟƒ้ฉรชๅ…งๅฎนใ€‚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ฤไฝ‡ %{count} kangๅ…ง๏ผŒๅฏ„้€kร u hit รชๅŸŸๅlรณngๅคฑๆ•—๏ผŒ้™ค้žๆ”ถ่‘—hit รชๅŸŸๅไพ†รชๅฏ„้€๏ผŒ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: diff --git a/config/locales/pa.yml b/config/locales/pa.yml index 1899d71008..f9508f9b9a 100644 --- a/config/locales/pa.yml +++ b/config/locales/pa.yml @@ -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: เจ•เจพเจชเฉ€ เจ•เจฐเฉ‹ diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml index 02ecd4ebe8..e8080b2a76 100644 --- a/config/locales/simple_form.es-MX.yml +++ b/config/locales/simple_form.es-MX.yml @@ -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 mostrarรกn 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 From 0af2c4829ff86cf84ed9e7804d023f31e805542b Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 22 Jul 2025 15:58:04 +0200 Subject: [PATCH 08/12] Creates Vite plugin to generate assets file (#35454) --- config/vite/plugin-assets-manifest.ts | 84 +++++++++++++++++++++++++++ vite.config.mts | 20 +------ 2 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 config/vite/plugin-assets-manifest.ts diff --git a/config/vite/plugin-assets-manifest.ts b/config/vite/plugin-assets-manifest.ts new file mode 100644 index 0000000000..3d465549ce --- /dev/null +++ b/config/vite/plugin-assets-manifest.ts @@ -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 = {}; + 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), + }); + }, + }; +} diff --git a/vite.config.mts b/vite.config.mts index b47bea382c..7f93157b7e 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -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 { MastodonThemes } from './config/vite/plugin-mastodon-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 }) => { }, }), MastodonThemes(), + 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; } From 105315a2e331f7e901e3e0685494d6cf678c0ec2 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 23 Jul 2025 12:10:29 +0200 Subject: [PATCH 09/12] Rename GlitchThemes plugin on import to reduce changes with upstream --- vite.config.mts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.mts b/vite.config.mts index 0c2414ec7c..460c851dcd 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -21,7 +21,7 @@ import tsconfigPaths from 'vite-tsconfig-paths'; 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 { GlitchThemes as MastodonThemes } from './config/vite/plugin-glitch-themes'; import { MastodonNameLookup } from './config/vite/plugin-name-lookup'; import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest'; @@ -119,7 +119,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { plugins: ['formatjs', 'transform-react-remove-prop-types'], }, }), - GlitchThemes(), + MastodonThemes(), MastodonAssetsManifest(), viteStaticCopy({ targets: [ From 0ae7c7e40627763b503529a2945b10d5b8325f25 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Mon, 21 Jul 2025 16:43:38 +0200 Subject: [PATCH 10/12] [Glitch] refactor: Replace react-hotkeys with custom hook Port 4de5cbd6f56fe0b8fe1a4e71c4d50839da6c4c26 to glitch-soc Signed-off-by: Claire --- .../glitch/components/hotkeys/index.tsx | 282 ++++++++++++++++++ .../glitch/components/hotkeys/utils.ts | 29 ++ .../flavours/glitch/components/status.jsx | 16 +- .../glitch/components/status_list.jsx | 3 +- .../compose/components/compose_form.jsx | 8 +- .../components/conversation.jsx | 8 +- .../notifications/components/notification.jsx | 26 +- .../components/notification_group.tsx | 5 +- .../notification_group_with_status.tsx | 7 +- .../components/notification_with_status.tsx | 7 +- .../flavours/glitch/features/status/index.jsx | 7 +- .../flavours/glitch/features/ui/index.jsx | 54 +--- 12 files changed, 361 insertions(+), 91 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/hotkeys/index.tsx create mode 100644 app/javascript/flavours/glitch/components/hotkeys/utils.ts diff --git a/app/javascript/flavours/glitch/components/hotkeys/index.tsx b/app/javascript/flavours/glitch/components/hotkeys/index.tsx new file mode 100644 index 0000000000..b5e0de4c59 --- /dev/null +++ b/app/javascript/flavours/glitch/components/hotkeys/index.tsx @@ -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 void> +>; + +export function useHotkeys(handlers: HandlerMap) { + const ref = useRef(null); + const bufferedKeys = useRef([]); + const sequenceTimer = useRef | 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 `` 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 + * + * ``` + * + * 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(handlers); + + return ( +
+ {children} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/hotkeys/utils.ts b/app/javascript/flavours/glitch/components/hotkeys/utils.ts new file mode 100644 index 0000000000..1430e1685b --- /dev/null +++ b/app/javascript/flavours/glitch/components/hotkeys/utils.ts @@ -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; + } +} diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 4070ea2913..609c7694d9 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -7,8 +7,7 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; - +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import { ContentWarning } from 'flavours/glitch/components/content_warning'; import { PictureInPicturePlaceholder } from 'flavours/glitch/components/picture_in_picture_placeholder'; import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; @@ -33,7 +32,6 @@ import StatusActionBar from './status_action_bar'; import StatusContent from './status_content'; import StatusIcons from './status_icons'; import StatusPrepend from './status_prepend'; - const domParser = new DOMParser(); export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => { @@ -499,13 +497,13 @@ class Status extends ImmutablePureComponent { if (hidden) { return ( - +
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.get('spoiler_text').length > 0 && ({status.get('spoiler_text')})} {expanded && {status.get('content')}}
-
+
); } @@ -516,7 +514,7 @@ class Status extends ImmutablePureComponent { }; return ( - +
: {matchedFilters.join(', ')}. {' '} @@ -524,7 +522,7 @@ class Status extends ImmutablePureComponent {
-
+
); } @@ -678,7 +676,7 @@ class Status extends ImmutablePureComponent { const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); return ( - +
-
+
); } diff --git a/app/javascript/flavours/glitch/components/status_list.jsx b/app/javascript/flavours/glitch/components/status_list.jsx index 95ff8f45e5..3323c6faae 100644 --- a/app/javascript/flavours/glitch/components/status_list.jsx +++ b/app/javascript/flavours/glitch/components/status_list.jsx @@ -57,7 +57,7 @@ export default class StatusList extends ImmutablePureComponent { const elementIndex = this.getCurrentStatusIndex(id, featured) - 1; this._selectChild(elementIndex, true); }; - + handleMoveDown = (id, featured) => { const elementIndex = this.getCurrentStatusIndex(id, featured) + 1; this._selectChild(elementIndex, false); @@ -70,6 +70,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) { diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 96e252ad2f..bb366f6066 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -101,13 +101,17 @@ class ComposeForm extends ImmutablePureComponent { }; handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + if (e.key.toLowerCase() === 'enter' && (e.ctrlKey || e.metaKey)) { this.handleSubmit(e); } - if (e.keyCode === 13 && e.altKey) { + if (e.key.toLowerCase() === 'enter' && e.altKey) { this.handleSecondarySubmit(e); } + + if (['esc', 'escape'].includes(e.key.toLowerCase())) { + this.textareaRef.current?.blur(); + } }; getFulltextForCharacterCounting = () => { diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx index 48aec3f12f..85066ecf09 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx @@ -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 'flavours/glitch/actions/compose'; import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations'; import { openModal } from 'flavours/glitch/actions/modal'; import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'flavours/glitch/actions/statuses'; +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import AttachmentList from 'flavours/glitch/components/attachment_list'; import AvatarComposite from 'flavours/glitch/components/avatar_composite'; import { IconButton } from 'flavours/glitch/components/icon_button'; @@ -178,7 +176,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) }; return ( - +
@@ -228,7 +226,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
- + ); }; diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx index a2ed584135..cebb589691 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx @@ -8,13 +8,13 @@ import { withRouter } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; import { Account } from 'flavours/glitch/components/account'; import { Icon } from 'flavours/glitch/components/icon'; +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import { Permalink } from 'flavours/glitch/components/permalink'; import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; @@ -121,7 +121,7 @@ class Notification extends ImmutablePureComponent { const { intl, unread } = this.props; return ( - +
@@ -133,7 +133,7 @@ class Notification extends ImmutablePureComponent {
- + ); } @@ -141,7 +141,7 @@ class Notification extends ImmutablePureComponent { const { intl, unread } = this.props; return ( - +
@@ -153,7 +153,7 @@ class Notification extends ImmutablePureComponent {
- + ); } @@ -313,7 +313,7 @@ class Notification extends ImmutablePureComponent { } return ( - +
-
+
); } @@ -336,7 +336,7 @@ class Notification extends ImmutablePureComponent { } return ( - +
-
+
); } @@ -352,7 +352,7 @@ class Notification extends ImmutablePureComponent { const { intl, unread } = this.props; return ( - +
@@ -364,7 +364,7 @@ class Notification extends ImmutablePureComponent {
- + ); } @@ -391,7 +391,7 @@ class Notification extends ImmutablePureComponent { ); return ( - +
@@ -403,7 +403,7 @@ class Notification extends ImmutablePureComponent {
- + ); } diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx index 9bd6c27a86..f4a9013a70 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx @@ -1,9 +1,8 @@ import { useMemo } from 'react'; -import { HotKeys } from 'react-hotkeys'; - import { navigateToProfile } from 'flavours/glitch/actions/accounts'; import { mentionComposeById } from 'flavours/glitch/actions/compose'; +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import type { NotificationGroup as NotificationGroupModel } from 'flavours/glitch/models/notification_group'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; @@ -156,5 +155,5 @@ export const NotificationGroup: React.FC<{ return null; } - return {content}; + return {content}; }; diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx index db12019f8e..774de6e54d 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group_with_status.tsx @@ -3,12 +3,11 @@ import type { JSX } from 'react'; import classNames from 'classnames'; -import { HotKeys } from 'react-hotkeys'; - import { replyComposeById } from 'flavours/glitch/actions/compose'; import { navigateToStatus } from 'flavours/glitch/actions/statuses'; import { Avatar } from 'flavours/glitch/components/avatar'; import { AvatarGroup } from 'flavours/glitch/components/avatar_group'; +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import type { IconProp } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; @@ -91,7 +90,7 @@ export const NotificationGroupWithStatus: React.FC<{ ); return ( - +
-
+
); }; diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx index 9f0ead6888..371b141f55 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx @@ -2,8 +2,6 @@ import { useMemo } from 'react'; import classNames from 'classnames'; -import { HotKeys } from 'react-hotkeys'; - import { replyComposeById } from 'flavours/glitch/actions/compose'; import { toggleReblog, @@ -13,6 +11,7 @@ import { navigateToStatus, toggleStatusSpoilers, } from 'flavours/glitch/actions/statuses'; +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import type { IconProp } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon'; import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted'; @@ -87,7 +86,7 @@ export const NotificationWithStatus: React.FC<{ if (!statusId || isFiltered) return null; return ( - +
-
+
); }; diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index 37cf4c9be5..da04de0cc7 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -10,11 +10,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import { HotKeys } from 'react-hotkeys'; - import ChatIcon from '@/material-icons/400-24px/chat.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react'; +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import { Icon } from 'flavours/glitch/components/icon'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; @@ -647,7 +646,7 @@ class Status extends ImmutablePureComponent {
{ancestors} - +
-
+
{descendants} {remoteHint} diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index 0069fb603f..617d2a6d8e 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -10,13 +10,13 @@ import { connect } from 'react-redux'; import Favico from 'favico.js'; import { debounce } from 'lodash'; -import { HotKeys } from 'react-hotkeys'; import { focusApp, unfocusApp, changeLayout } from 'flavours/glitch/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { fetchNotifications } from 'flavours/glitch/actions/notification_groups'; import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding'; import { AlertsController } from 'flavours/glitch/components/alerts_controller'; +import { Hotkeys } from 'flavours/glitch/components/hotkeys'; import { HoverCardController } from 'flavours/glitch/components/hover_card_controller'; import { Permalink } from 'flavours/glitch/components/permalink'; import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture'; @@ -106,41 +106,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', - bookmark: 'd', - toggleSensitive: 'h', - openMedia: 'e', - onTranslate: 't', -}; - class SwitchingColumnsArea extends PureComponent { static propTypes = { identity: identityContextPropShape, @@ -416,6 +381,10 @@ class UI extends PureComponent { } }; + handleDonate = () => { + location.href = 'https://joinmastodon.org/sponsors#donate' + } + componentDidMount () { const { signedIn } = this.props.identity; @@ -443,10 +412,6 @@ class UI extends PureComponent { setTimeout(() => this.props.dispatch(fetchServer()), 3000); } - this.hotkeys.__mousetrap__.stopCallback = (e, element) => { - return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); - }; - if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support this.visibilityHiddenProp = 'hidden'; this.visibilityChange = 'visibilitychange'; @@ -556,10 +521,6 @@ class UI extends PureComponent { } }; - setHotkeysRef = c => { - this.hotkeys = c; - }; - handleHotkeyToggleHelp = () => { if (this.props.location.pathname === '/keyboard-shortcuts') { this.props.history.goBack(); @@ -647,10 +608,11 @@ class UI extends PureComponent { goToBlocked: this.handleHotkeyGoToBlocked, goToMuted: this.handleHotkeyGoToMuted, goToRequests: this.handleHotkeyGoToRequests, + cheat: this.handleDonate, }; return ( - +
{moved && (
- + ); } From 26ba2db53f68eb6ab0ef5ae37a8ea4a8506282bb Mon Sep 17 00:00:00 2001 From: diondiondion Date: Mon, 21 Jul 2025 17:57:20 +0200 Subject: [PATCH 11/12] [Glitch] fix: Don't submit post when pressing Enter in CW field Port ee21f7221143d94aac00a6cbadea352ebfe44905 to glitch-soc Signed-off-by: Claire --- .../compose/components/compose_form.jsx | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index bb366f6066..e29f6b218d 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -100,7 +100,13 @@ class ComposeForm extends ImmutablePureComponent { this.props.onChange(e.target.value); }; - handleKeyDown = (e) => { + 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(e); } @@ -109,11 +115,23 @@ class ComposeForm extends ImmutablePureComponent { this.handleSecondarySubmit(e); } - if (['esc', 'escape'].includes(e.key.toLowerCase())) { - this.textareaRef.current?.blur(); - } + this.blurOnEscape(e); }; + handleKeyDownSpoiler = (e) => { + if (e.key.toLowerCase() === 'enter') { + if (e.ctrlKey || e.metaKey) { + this.handleSubmit(); + } else if (e.altKey) { + this.handleSecondarySubmit(e); + } else { + e.preventDefault(); + this.textareaRef.current?.focus(); + } + } + this.blurOnEscape(e); + } + getFulltextForCharacterCounting = () => { return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join(''); }; @@ -269,7 +287,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} @@ -294,7 +312,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} From d69c5f1a6ecc7f3e2a1a0feb6f0ce6ced4bb8641 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 23 Jul 2025 12:48:45 +0200 Subject: [PATCH 12/12] Fix glitch assets not being found --- config/vite/plugin-assets-manifest.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/vite/plugin-assets-manifest.ts b/config/vite/plugin-assets-manifest.ts index 3d465549ce..b74b505398 100644 --- a/config/vite/plugin-assets-manifest.ts +++ b/config/vite/plugin-assets-manifest.ts @@ -29,10 +29,13 @@ export function MastodonAssetsManifest(): Plugin { }, async generateBundle() { // Glob all assets and return an array of absolute paths. - const assetPaths = await glob('{fonts,icons,images}/**/*', { - cwd: jsRoot, - absolute: true, - }); + const assetPaths = await glob( + ['flavours/*/{fonts,icons,images}/**/*', '{fonts,icons,images}/**/*'], + { + cwd: jsRoot, + absolute: true, + }, + ); const assetManifest: Record = {}; const excludeExts = ['', '.md'];