Merge commit '0af2c4829ff86cf84ed9e7804d023f31e805542b' into glitch-soc/merge-upstream
Conflicts: - `vite.config.mts`: Conflict because of differently-named theme plugins. Updated to go with upstream's changes.
This commit is contained in:
commit
45d79427c4
26 files changed
+873
-265
No files matched your search
@@ -0,0 +1,171 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { expect } from 'storybook/test';
|
||||
|
||||
import type { HandlerMap } from '.';
|
||||
import { Hotkeys } from '.';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Hotkeys',
|
||||
component: Hotkeys,
|
||||
args: {
|
||||
global: undefined,
|
||||
focusable: undefined,
|
||||
handlers: {},
|
||||
},
|
||||
tags: ['test'],
|
||||
} satisfies Meta<typeof Hotkeys>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const hotkeyTest: Story['play'] = async ({ canvas, userEvent }) => {
|
||||
async function confirmHotkey(name: string, shouldFind = true) {
|
||||
// 'status' is the role of the 'output' element
|
||||
const output = await canvas.findByRole('status');
|
||||
if (shouldFind) {
|
||||
await expect(output).toHaveTextContent(name);
|
||||
} else {
|
||||
await expect(output).not.toHaveTextContent(name);
|
||||
}
|
||||
}
|
||||
|
||||
const button = await canvas.findByRole('button');
|
||||
await userEvent.click(button);
|
||||
|
||||
await userEvent.keyboard('n');
|
||||
await confirmHotkey('new');
|
||||
|
||||
await userEvent.keyboard('/');
|
||||
await confirmHotkey('search');
|
||||
|
||||
await userEvent.keyboard('o');
|
||||
await confirmHotkey('open');
|
||||
|
||||
await userEvent.keyboard('{Alt>}N{/Alt}');
|
||||
await confirmHotkey('forceNew');
|
||||
|
||||
await userEvent.keyboard('gh');
|
||||
await confirmHotkey('goToHome');
|
||||
|
||||
await userEvent.keyboard('gn');
|
||||
await confirmHotkey('goToNotifications');
|
||||
|
||||
await userEvent.keyboard('gf');
|
||||
await confirmHotkey('goToFavourites');
|
||||
|
||||
/**
|
||||
* Ensure that hotkeys are not triggered when certain
|
||||
* interactive elements are focused:
|
||||
*/
|
||||
|
||||
await userEvent.keyboard('{enter}');
|
||||
await confirmHotkey('open', false);
|
||||
|
||||
const input = await canvas.findByRole('textbox');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.keyboard('n');
|
||||
await confirmHotkey('new', false);
|
||||
|
||||
await userEvent.keyboard('{backspace}');
|
||||
await confirmHotkey('None', false);
|
||||
|
||||
/**
|
||||
* Reset playground:
|
||||
*/
|
||||
|
||||
await userEvent.click(button);
|
||||
await userEvent.keyboard('{backspace}');
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
render: function Render() {
|
||||
const [matchedHotkey, setMatchedHotkey] = useState<keyof HandlerMap | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handlers = {
|
||||
back: () => {
|
||||
setMatchedHotkey(null);
|
||||
},
|
||||
new: () => {
|
||||
setMatchedHotkey('new');
|
||||
},
|
||||
forceNew: () => {
|
||||
setMatchedHotkey('forceNew');
|
||||
},
|
||||
search: () => {
|
||||
setMatchedHotkey('search');
|
||||
},
|
||||
open: () => {
|
||||
setMatchedHotkey('open');
|
||||
},
|
||||
goToHome: () => {
|
||||
setMatchedHotkey('goToHome');
|
||||
},
|
||||
goToNotifications: () => {
|
||||
setMatchedHotkey('goToNotifications');
|
||||
},
|
||||
goToFavourites: () => {
|
||||
setMatchedHotkey('goToFavourites');
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
padding: '1em',
|
||||
border: '1px dashed #ccc',
|
||||
fontSize: 14,
|
||||
color: '#222',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: 22,
|
||||
marginBottom: '0.3em',
|
||||
}}
|
||||
>
|
||||
Hotkey playground
|
||||
</h1>
|
||||
<p>
|
||||
Last pressed hotkey: <output>{matchedHotkey ?? 'None'}</output>
|
||||
</p>
|
||||
<p>
|
||||
Click within the dashed border and press the "<kbd>n</kbd>
|
||||
" or "<kbd>/</kbd>" key. Press "
|
||||
<kbd>Backspace</kbd>" to clear the displayed hotkey.
|
||||
</p>
|
||||
<p>
|
||||
Try typing a sequence, like "<kbd>g</kbd>" shortly
|
||||
followed by "<kbd>h</kbd>", "<kbd>n</kbd>", or
|
||||
"<kbd>f</kbd>"
|
||||
</p>
|
||||
<p>
|
||||
Note that this playground doesn't support all hotkeys we use in
|
||||
the app.
|
||||
</p>
|
||||
<p>
|
||||
When a <button>Button</button> is focused, "
|
||||
<kbd>Enter</kbd>
|
||||
" should not trigger "open", but "<kbd>o</kbd>
|
||||
" should.
|
||||
</p>
|
||||
<p>
|
||||
When an input element is focused, hotkeys should not interfere with
|
||||
regular typing:
|
||||
</p>
|
||||
<input type='text' />
|
||||
</div>
|
||||
</Hotkeys>
|
||||
);
|
||||
},
|
||||
play: hotkeyTest,
|
||||
};
|
||||
@@ -0,0 +1,282 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { normalizeKey, isKeyboardEvent } from './utils';
|
||||
|
||||
/**
|
||||
* In case of multiple hotkeys matching the pressed key(s),
|
||||
* the hotkey with a higher priority is selected. All others
|
||||
* are ignored.
|
||||
*/
|
||||
const hotkeyPriority = {
|
||||
singleKey: 0,
|
||||
combo: 1,
|
||||
sequence: 2,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* This type of function receives a keyboard event and an array of
|
||||
* previously pressed keys (within the last second), and returns
|
||||
* `isMatch` (whether the pressed keys match a hotkey) and `priority`
|
||||
* (a weighting used to resolve conflicts when two hotkeys match the
|
||||
* pressed keys)
|
||||
*/
|
||||
type KeyMatcher = (
|
||||
event: KeyboardEvent,
|
||||
bufferedKeys?: string[],
|
||||
) => {
|
||||
/**
|
||||
* Whether the event.key matches the hotkey
|
||||
*/
|
||||
isMatch: boolean;
|
||||
/**
|
||||
* If there are multiple matching hotkeys, the
|
||||
* first one with the highest priority will be handled
|
||||
*/
|
||||
priority: (typeof hotkeyPriority)[keyof typeof hotkeyPriority];
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches a single key
|
||||
*/
|
||||
function just(keyName: string): KeyMatcher {
|
||||
return (event) => ({
|
||||
isMatch: normalizeKey(event.key) === keyName,
|
||||
priority: hotkeyPriority.singleKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches any single key out of those provided
|
||||
*/
|
||||
function any(...keys: string[]): KeyMatcher {
|
||||
return (event) => ({
|
||||
isMatch: keys.some((keyName) => just(keyName)(event).isMatch),
|
||||
priority: hotkeyPriority.singleKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a single key combined with the option/alt modifier
|
||||
*/
|
||||
function optionPlus(key: string): KeyMatcher {
|
||||
return (event) => ({
|
||||
// Matching against event.code here as alt combos are often
|
||||
// mapped to other characters
|
||||
isMatch: event.altKey && event.code === `Key${key.toUpperCase()}`,
|
||||
priority: hotkeyPriority.combo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches when all provided keys are pressed in sequence.
|
||||
*/
|
||||
function sequence(...sequence: string[]): KeyMatcher {
|
||||
return (event, bufferedKeys) => {
|
||||
const lastKeyInSequence = sequence.at(-1);
|
||||
const startOfSequence = sequence.slice(0, -1);
|
||||
const relevantBufferedKeys = bufferedKeys?.slice(-startOfSequence.length);
|
||||
|
||||
const bufferMatchesStartOfSequence =
|
||||
!!relevantBufferedKeys &&
|
||||
startOfSequence.join('') === relevantBufferedKeys.join('');
|
||||
|
||||
return {
|
||||
isMatch:
|
||||
bufferMatchesStartOfSequence &&
|
||||
normalizeKey(event.key) === lastKeyInSequence,
|
||||
priority: hotkeyPriority.sequence,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a map of all global hotkeys we support.
|
||||
* To trigger a hotkey, a handler with a matching name must be
|
||||
* provided to the `useHotkeys` hook or `Hotkeys` component.
|
||||
*/
|
||||
const hotkeyMatcherMap = {
|
||||
help: just('?'),
|
||||
search: any('s', '/'),
|
||||
back: just('backspace'),
|
||||
new: just('n'),
|
||||
forceNew: optionPlus('n'),
|
||||
focusColumn: any('1', '2', '3', '4', '5', '6', '7', '8', '9'),
|
||||
reply: just('r'),
|
||||
favourite: just('f'),
|
||||
boost: just('b'),
|
||||
mention: just('m'),
|
||||
open: any('enter', 'o'),
|
||||
openProfile: just('p'),
|
||||
moveDown: any('down', 'j'),
|
||||
moveUp: any('up', 'k'),
|
||||
toggleHidden: just('x'),
|
||||
toggleSensitive: just('h'),
|
||||
toggleComposeSpoilers: optionPlus('x'),
|
||||
openMedia: just('e'),
|
||||
onTranslate: just('t'),
|
||||
goToHome: sequence('g', 'h'),
|
||||
goToNotifications: sequence('g', 'n'),
|
||||
goToLocal: sequence('g', 'l'),
|
||||
goToFederated: sequence('g', 't'),
|
||||
goToDirect: sequence('g', 'd'),
|
||||
goToStart: sequence('g', 's'),
|
||||
goToFavourites: sequence('g', 'f'),
|
||||
goToPinned: sequence('g', 'p'),
|
||||
goToProfile: sequence('g', 'u'),
|
||||
goToBlocked: sequence('g', 'b'),
|
||||
goToMuted: sequence('g', 'm'),
|
||||
goToRequests: sequence('g', 'r'),
|
||||
cheat: sequence(
|
||||
'up',
|
||||
'up',
|
||||
'down',
|
||||
'down',
|
||||
'left',
|
||||
'right',
|
||||
'left',
|
||||
'right',
|
||||
'b',
|
||||
'a',
|
||||
'enter',
|
||||
),
|
||||
} as const;
|
||||
|
||||
type HotkeyName = keyof typeof hotkeyMatcherMap;
|
||||
|
||||
export type HandlerMap = Partial<
|
||||
Record<HotkeyName, (event: KeyboardEvent) => void>
|
||||
>;
|
||||
|
||||
export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
|
||||
const ref = useRef<T>(null);
|
||||
const bufferedKeys = useRef<string[]>([]);
|
||||
const sequenceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
/**
|
||||
* Store the latest handlers object in a ref so we don't need to
|
||||
* add it as a dependency to the main event listener effect
|
||||
*/
|
||||
const handlersRef = useRef(handlers);
|
||||
useEffect(() => {
|
||||
handlersRef.current = handlers;
|
||||
}, [handlers]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current ?? document;
|
||||
|
||||
function listener(event: Event) {
|
||||
// Ignore key presses from input, textarea, or select elements
|
||||
const tagName = (event.target as HTMLElement).tagName.toLowerCase();
|
||||
const shouldHandleEvent =
|
||||
isKeyboardEvent(event) &&
|
||||
!event.defaultPrevented &&
|
||||
!['input', 'textarea', 'select'].includes(tagName) &&
|
||||
!(
|
||||
['a', 'button'].includes(tagName) &&
|
||||
normalizeKey(event.key) === 'enter'
|
||||
);
|
||||
|
||||
if (shouldHandleEvent) {
|
||||
const matchCandidates: {
|
||||
handler: (event: KeyboardEvent) => void;
|
||||
priority: number;
|
||||
}[] = [];
|
||||
|
||||
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
|
||||
(handlerName) => {
|
||||
const handler = handlersRef.current[handlerName];
|
||||
|
||||
if (handler) {
|
||||
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
|
||||
|
||||
const { isMatch, priority } = hotkeyMatcher(
|
||||
event,
|
||||
bufferedKeys.current,
|
||||
);
|
||||
|
||||
if (isMatch) {
|
||||
matchCandidates.push({ handler, priority });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Sort all matches by priority
|
||||
matchCandidates.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
const bestMatchingHandler = matchCandidates.at(0)?.handler;
|
||||
if (bestMatchingHandler) {
|
||||
bestMatchingHandler(event);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Add last keypress to buffer
|
||||
bufferedKeys.current.push(normalizeKey(event.key));
|
||||
|
||||
// Reset the timeout
|
||||
if (sequenceTimer.current) {
|
||||
clearTimeout(sequenceTimer.current);
|
||||
}
|
||||
sequenceTimer.current = setTimeout(() => {
|
||||
bufferedKeys.current = [];
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
element.addEventListener('keydown', listener);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('keydown', listener);
|
||||
if (sequenceTimer.current) {
|
||||
clearTimeout(sequenceTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Hotkeys component allows us to globally register keyboard combinations
|
||||
* under a name and assign actions to them, either globally or scoped to a portion
|
||||
* of the app.
|
||||
*
|
||||
* ### How to use
|
||||
*
|
||||
* To add a new hotkey, add its key combination to the `hotkeyMatcherMap` object
|
||||
* and give it a name.
|
||||
*
|
||||
* Use the `<Hotkeys>` component or the `useHotkeys` hook in the part of of the app
|
||||
* where you want to handle the action, and pass in a handlers object.
|
||||
*
|
||||
* ```tsx
|
||||
* <Hotkeys handlers={{open: openStatus}} />
|
||||
* ```
|
||||
*
|
||||
* Now this function will be called when the 'open' hotkey is pressed by the user.
|
||||
*/
|
||||
export const Hotkeys: React.FC<{
|
||||
/**
|
||||
* An object containing functions to be run when a hotkey is pressed.
|
||||
* The key must be the name of a registered hotkey, e.g. "help" or "search"
|
||||
*/
|
||||
handlers: HandlerMap;
|
||||
/**
|
||||
* When enabled, hotkeys will be matched against the document root
|
||||
* rather than only inside of this component's DOM node.
|
||||
*/
|
||||
global?: boolean;
|
||||
/**
|
||||
* Allow the rendered `div` to be focused
|
||||
*/
|
||||
focusable?: boolean;
|
||||
children: React.ReactNode;
|
||||
}> = ({ handlers, global, focusable = true, children }) => {
|
||||
const ref = useHotkeys<HTMLDivElement>(handlers);
|
||||
|
||||
return (
|
||||
<div ref={global ? undefined : ref} tabIndex={focusable ? -1 : undefined}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,9 @@ import { Link } from 'react-router-dom';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
@@ -35,7 +34,6 @@ import StatusActionBar from './status_action_bar';
|
||||
import StatusContent from './status_content';
|
||||
import { StatusThreadLabel } from './status_thread_label';
|
||||
import { VisibilityIcon } from './visibility_icon';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||
@@ -325,11 +323,11 @@ class Status extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
handleHotkeyMoveUp = e => {
|
||||
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||
this.props.onMoveUp?.(this.props.status.get('id'), this.node.getAttribute('data-featured'));
|
||||
};
|
||||
|
||||
handleHotkeyMoveDown = e => {
|
||||
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||
this.props.onMoveDown?.(this.props.status.get('id'), this.node.getAttribute('data-featured'));
|
||||
};
|
||||
|
||||
handleHotkeyToggleHidden = () => {
|
||||
@@ -437,13 +435,13 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<Hotkeys handlers={handlers} focusable={!unfocusable}>
|
||||
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={unfocusable ? null : 0}>
|
||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||
{status.get('spoiler_text').length > 0 && (<span>{status.get('spoiler_text')}</span>)}
|
||||
{expanded && <span>{status.get('content')}</span>}
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -543,7 +541,7 @@ class Status extends ImmutablePureComponent {
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<Hotkeys handlers={handlers} focusable={!unfocusable}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||
{!skipPrepend && prepend}
|
||||
|
||||
@@ -604,7 +602,7 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -92,10 +92,29 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleSubmit();
|
||||
blurOnEscape = (e) => {
|
||||
if (['esc', 'escape'].includes(e.key.toLowerCase())) {
|
||||
e.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDownPost = (e) => {
|
||||
if (e.key.toLowerCase() === 'enter' && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleSubmit();
|
||||
}
|
||||
this.blurOnEscape(e);
|
||||
};
|
||||
|
||||
handleKeyDownSpoiler = (e) => {
|
||||
if (e.key.toLowerCase() === 'enter') {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
this.handleSubmit();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
this.textareaRef.current?.focus();
|
||||
}
|
||||
}
|
||||
this.blurOnEscape(e);
|
||||
};
|
||||
|
||||
getFulltextForCharacterCounting = () => {
|
||||
@@ -248,7 +267,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
value={this.props.spoilerText}
|
||||
disabled={isSubmitting}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyDown={this.handleKeyDownSpoiler}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
@@ -273,7 +292,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyDown={this.handleKeyDownPost}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
|
||||
@@ -10,15 +10,13 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import { replyCompose } from 'mastodon/actions/compose';
|
||||
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
import AvatarComposite from 'mastodon/components/avatar_composite';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
@@ -169,7 +167,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div className={classNames('conversation focusable muted', { unread })} tabIndex={0}>
|
||||
<div className='conversation__avatar' onClick={handleClick} role='presentation'>
|
||||
<AvatarComposite accounts={accounts} size={48} />
|
||||
@@ -219,7 +217,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Link, withRouter } from 'react-router-dom';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
|
||||
@@ -20,6 +19,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
@@ -137,7 +137,7 @@ class Notification extends ImmutablePureComponent {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-follow focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='user-plus' icon={PersonAddIcon} />
|
||||
@@ -149,7 +149,7 @@ class Notification extends ImmutablePureComponent {
|
||||
|
||||
<Account id={account.get('id')} hidden={this.props.hidden} />
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class Notification extends ImmutablePureComponent {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='user' icon={PersonIcon} />
|
||||
@@ -169,7 +169,7 @@ class Notification extends ImmutablePureComponent {
|
||||
|
||||
<FollowRequestContainer id={account.get('id')} hidden={this.props.hidden} />
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class Notification extends ImmutablePureComponent {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-favourite focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='star' icon={StarIcon} className='star-icon' />
|
||||
@@ -217,7 +217,7 @@ class Notification extends ImmutablePureComponent {
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ class Notification extends ImmutablePureComponent {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='retweet' icon={RepeatIcon} />
|
||||
@@ -247,7 +247,7 @@ class Notification extends ImmutablePureComponent {
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-status focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='home' icon={HomeIcon} />
|
||||
@@ -282,7 +282,7 @@ class Notification extends ImmutablePureComponent {
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-update focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.update, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='pencil' icon={EditIcon} />
|
||||
@@ -317,7 +317,7 @@ class Notification extends ImmutablePureComponent {
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='tasks' icon={InsertChartIcon} />
|
||||
@@ -358,7 +358,7 @@ class Notification extends ImmutablePureComponent {
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.relationshipsSevered, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}>
|
||||
<RelationshipsSeveranceEvent
|
||||
type={event.get('type')}
|
||||
@@ -381,7 +381,7 @@ class Notification extends ImmutablePureComponent {
|
||||
hidden={hidden}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-moderation-warning focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.moderationWarning), notification.get('created_at'))}>
|
||||
<ModerationWarning
|
||||
action={warning.get('action')}
|
||||
@@ -402,7 +402,7 @@ class Notification extends ImmutablePureComponent {
|
||||
hidden={hidden}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ class Notification extends ImmutablePureComponent {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminSignUp, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='user-plus' icon={PersonAddIcon} />
|
||||
@@ -422,7 +422,7 @@ class Notification extends ImmutablePureComponent {
|
||||
|
||||
<Account id={account.get('id')} hidden={this.props.hidden} />
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ class Notification extends ImmutablePureComponent {
|
||||
const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} data-hover-card-account={targetAccount.get('id')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<Hotkeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: account.get('acct'), target: notification.getIn(['report', 'target_account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<Icon id='flag' icon={FlagIcon} />
|
||||
@@ -450,7 +450,7 @@ class Notification extends ImmutablePureComponent {
|
||||
|
||||
<Report account={account} report={notification.get('report')} hidden={this.props.hidden} />
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { navigateToProfile } from 'mastodon/actions/accounts';
|
||||
import { mentionComposeById } from 'mastodon/actions/compose';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
@@ -156,5 +155,5 @@ export const NotificationGroup: React.FC<{
|
||||
return null;
|
||||
}
|
||||
|
||||
return <HotKeys handlers={handlers}>{content}</HotKeys>;
|
||||
return <Hotkeys handlers={handlers}>{content}</Hotkeys>;
|
||||
};
|
||||
+3
-4
@@ -3,12 +3,11 @@ import type { JSX } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { replyComposeById } from 'mastodon/actions/compose';
|
||||
import { navigateToStatus } from 'mastodon/actions/statuses';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { AvatarGroup } from 'mastodon/components/avatar_group';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import type { IconProp } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
@@ -91,7 +90,7 @@ export const NotificationGroupWithStatus: React.FC<{
|
||||
);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div
|
||||
role='button'
|
||||
className={classNames(
|
||||
@@ -149,6 +148,6 @@ export const NotificationGroupWithStatus: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
};
|
||||
+3
-4
@@ -2,14 +2,13 @@ import { useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { replyComposeById } from 'mastodon/actions/compose';
|
||||
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
|
||||
import {
|
||||
navigateToStatus,
|
||||
toggleStatusSpoilers,
|
||||
} from 'mastodon/actions/statuses';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import type { IconProp } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
|
||||
@@ -83,7 +82,7 @@ export const NotificationWithStatus: React.FC<{
|
||||
if (!statusId || isFiltered) return null;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div
|
||||
role='button'
|
||||
className={classNames(
|
||||
@@ -111,6 +110,6 @@ export const NotificationWithStatus: React.FC<{
|
||||
unfocusable
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
};
|
||||
@@ -10,10 +10,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
|
||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||
@@ -616,7 +615,7 @@ class Status extends ImmutablePureComponent {
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setContainerRef}>
|
||||
{ancestors}
|
||||
|
||||
<HotKeys handlers={handlers}>
|
||||
<Hotkeys handlers={handlers}>
|
||||
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false)} ref={this.setStatusRef}>
|
||||
<DetailedStatus
|
||||
key={`details-${status.get('id')}`}
|
||||
@@ -654,7 +653,7 @@ class Status extends ImmutablePureComponent {
|
||||
onEmbed={this.handleEmbed}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
|
||||
{descendants}
|
||||
{remoteHint}
|
||||
|
||||
@@ -9,13 +9,13 @@ import { Redirect, Route, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||
import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
||||
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
||||
import { AlertsController } from 'mastodon/components/alerts_controller';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { HoverCardController } from 'mastodon/components/hover_card_controller';
|
||||
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
|
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||
@@ -98,40 +98,6 @@ const mapStateToProps = state => ({
|
||||
username: state.getIn(['accounts', me, 'username']),
|
||||
});
|
||||
|
||||
const keyMap = {
|
||||
help: '?',
|
||||
new: 'n',
|
||||
search: ['s', '/'],
|
||||
forceNew: 'option+n',
|
||||
toggleComposeSpoilers: 'option+x',
|
||||
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
reply: 'r',
|
||||
favourite: 'f',
|
||||
boost: 'b',
|
||||
mention: 'm',
|
||||
open: ['enter', 'o'],
|
||||
openProfile: 'p',
|
||||
moveDown: ['down', 'j'],
|
||||
moveUp: ['up', 'k'],
|
||||
back: 'backspace',
|
||||
goToHome: 'g h',
|
||||
goToNotifications: 'g n',
|
||||
goToLocal: 'g l',
|
||||
goToFederated: 'g t',
|
||||
goToDirect: 'g d',
|
||||
goToStart: 'g s',
|
||||
goToFavourites: 'g f',
|
||||
goToPinned: 'g p',
|
||||
goToProfile: 'g u',
|
||||
goToBlocked: 'g b',
|
||||
goToMuted: 'g m',
|
||||
goToRequests: 'g r',
|
||||
toggleHidden: 'x',
|
||||
toggleSensitive: 'h',
|
||||
openMedia: 'e',
|
||||
onTranslate: 't',
|
||||
};
|
||||
|
||||
class SwitchingColumnsArea extends PureComponent {
|
||||
static propTypes = {
|
||||
identity: identityContextPropShape,
|
||||
@@ -400,6 +366,10 @@ class UI extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleDonate = () => {
|
||||
location.href = 'https://joinmastodon.org/sponsors#donate'
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { signedIn } = this.props.identity;
|
||||
|
||||
@@ -426,10 +396,6 @@ class UI extends PureComponent {
|
||||
|
||||
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||
}
|
||||
|
||||
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
|
||||
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@@ -509,10 +475,6 @@ class UI extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
setHotkeysRef = c => {
|
||||
this.hotkeys = c;
|
||||
};
|
||||
|
||||
handleHotkeyToggleHelp = () => {
|
||||
if (this.props.location.pathname === '/keyboard-shortcuts') {
|
||||
this.props.history.goBack();
|
||||
@@ -593,10 +555,11 @@ class UI extends PureComponent {
|
||||
goToBlocked: this.handleHotkeyGoToBlocked,
|
||||
goToMuted: this.handleHotkeyGoToMuted,
|
||||
goToRequests: this.handleHotkeyGoToRequests,
|
||||
cheat: this.handleDonate,
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
||||
<Hotkeys global handlers={handlers}>
|
||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
||||
<SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'} forceOnboarding={firstLaunch && newAccount}>
|
||||
{children}
|
||||
@@ -611,7 +574,7 @@ class UI extends PureComponent {
|
||||
<ModalContainer />
|
||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||
</div>
|
||||
</HotKeys>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"about.contact": "ਸੰਪਰਕ:",
|
||||
"about.default_locale": "ਮੂਲ",
|
||||
"about.disclaimer": "ਮਸਟੋਡੋਨ ਇੱਕ ਆਜ਼ਾਦ, ਖੁੱਲ੍ਹੇ ਸਰੋਤ ਵਾਲਾ ਸਾਫਟਵੇਅਰ ਹੈ ਅਤੇ Mastodon gGmbH ਦਾ ਮਾਰਕਾ ਹੈ।",
|
||||
"about.domain_blocks.no_reason_available": "ਕਾਰਨ ਮੌਜੂਦ ਨਹੀਂ ਹੈ",
|
||||
"about.domain_blocks.silenced.title": "ਸੀਮਿਤ",
|
||||
"about.domain_blocks.suspended.title": "ਮੁਅੱਤਲ ਕੀਤੀ",
|
||||
"about.domain_blocks.suspended.title": "ਸਸਪੈਂਡ ਕੀਤਾ",
|
||||
"about.language_label": "ਭਾਸ਼ਾ",
|
||||
"about.not_available": "ਇਹ ਜਾਣਕਾਰੀ ਨੂੰ ਇਸ ਸਰਵਰ ਉੱਤੇ ਉਪਲੱਬਧ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਹੈ।",
|
||||
"about.rules": "ਸਰਵਰ ਨਿਯਮ",
|
||||
"account.account_note_header": "ਨਿੱਜੀ ਨੋਟ",
|
||||
"account.add_or_remove_from_list": "ਸੂਚੀ ਵਿੱਚ ਜੋੜੋ ਜਾਂ ਹਟਾਓ",
|
||||
@@ -12,21 +16,33 @@
|
||||
"account.block_domain": "{domain} ਡੋਮੇਨ ਉੱਤੇ ਪਾਬੰਦੀ ਲਾਓ",
|
||||
"account.block_short": "ਪਾਬੰਦੀ",
|
||||
"account.blocked": "ਪਾਬੰਦੀਸ਼ੁਦਾ",
|
||||
"account.blocking": "ਪਾਬੰਦੀ ਲਾਉਣੀ",
|
||||
"account.cancel_follow_request": "ਫ਼ਾਲੋ ਕਰਨ ਨੂੰ ਰੱਦ ਕਰੋ",
|
||||
"account.copy": "ਪਰੋਫਾਇਲ ਲਈ ਲਿੰਕ ਕਾਪੀ ਕਰੋ",
|
||||
"account.direct": "ਨਿੱਜੀ ਜ਼ਿਕਰ @{name}",
|
||||
"account.disable_notifications": "ਜਦੋਂ {name} ਕੋਈ ਪੋਸਟ ਕਰੇ ਤਾਂ ਮੈਨੂੰ ਸੂਚਨਾ ਨਾ ਦਿਓ",
|
||||
"account.domain_blocking": "ਡੋਮੇਨ ਉੱਤੇ ਪਾਬੰਦੀ",
|
||||
"account.edit_profile": "ਪਰੋਫਾਈਲ ਨੂੰ ਸੋਧੋ",
|
||||
"account.enable_notifications": "ਜਦੋਂ {name} ਪੋਸਟ ਕਰੇ ਤਾਂ ਮੈਨੂੰ ਸੂਚਨਾ ਦਿਓ",
|
||||
"account.endorse": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਫ਼ੀਚਰ",
|
||||
"account.familiar_followers_one": "{name1} ਵਲੋਂ ਫ਼ਾਲੋ ਕੀਤਾ",
|
||||
"account.familiar_followers_two": "{name1} ਅਤੇ {name2} ਵਲੋਂ ਫ਼ਾਲੋ ਕੀਤਾ",
|
||||
"account.featured": "ਫ਼ੀਚਰ",
|
||||
"account.featured.accounts": "ਪਰੋਫਾਈਲ",
|
||||
"account.featured.hashtags": "ਹੈਸ਼ਟੈਗ",
|
||||
"account.featured_tags.last_status_at": "{date} ਨੂੰ ਆਖਰੀ ਪੋਸਟ",
|
||||
"account.featured_tags.last_status_never": "ਕੋਈ ਪੋਸਟ ਨਹੀਂ",
|
||||
"account.follow": "ਫ਼ਾਲੋ",
|
||||
"account.follow_back": "ਵਾਪਸ ਫਾਲ਼ੋ ਕਰੋ",
|
||||
"account.followers": "ਫ਼ਾਲੋਅਰ",
|
||||
"account.followers.empty": "ਇਸ ਵਰਤੋਂਕਾਰ ਨੂੰ ਹਾਲੇ ਕੋਈ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।",
|
||||
"account.followers_counter": "{count, plural, one {{counter} ਫ਼ਾਲੋਅਰ} other {{counter} ਫ਼ਾਲੋਅਰ}}",
|
||||
"account.followers_you_know_counter": "{counter} ਤੁਸੀਂ ਜਾਣਦੇ ਹੋ",
|
||||
"account.following": "ਫ਼ਾਲੋ ਕੀਤਾ",
|
||||
"account.follows.empty": "ਇਹ ਵਰਤੋਂਕਾਰ ਹਾਲੇ ਕਿਸੇ ਨੂੰ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।",
|
||||
"account.follows_you": "ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹਨ",
|
||||
"account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ",
|
||||
"account.hide_reblogs": "{name} ਵਲੋਂ ਬੂਸਟ ਨੂੰ ਲੁਕਾਓ",
|
||||
"account.joined_short": "ਜੁਆਇਨ ਕੀਤਾ",
|
||||
"account.media": "ਮੀਡੀਆ",
|
||||
"account.mention": "@{name} ਦਾ ਜ਼ਿਕਰ",
|
||||
@@ -34,6 +50,7 @@
|
||||
"account.mute_notifications_short": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਮੌਨ ਕਰੋ",
|
||||
"account.mute_short": "ਮੌਨ ਕਰੋ",
|
||||
"account.muted": "ਮੌਨ ਕੀਤੀਆਂ",
|
||||
"account.mutual": "ਤੁਸੀਂ ਇੱਕ ਦੂਜੇ ਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹੋ",
|
||||
"account.no_bio": "ਕੋਈ ਵਰਣਨ ਨਹੀਂ ਦਿੱਤਾ।",
|
||||
"account.open_original_page": "ਅਸਲ ਸਫ਼ੇ ਨੂੰ ਖੋਲ੍ਹੋ",
|
||||
"account.posts": "ਪੋਸਟਾਂ",
|
||||
@@ -42,8 +59,10 @@
|
||||
"account.requested": "ਮਨਜ਼ੂਰੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ। ਫ਼ਾਲੋ ਬੇਨਤੀਆਂ ਨੂੰ ਰੱਦ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ",
|
||||
"account.requested_follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ",
|
||||
"account.share": "{name} ਦਾ ਪਰੋਫ਼ਾਇਲ ਸਾਂਝਾ ਕਰੋ",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} ਪੋਸਟ} other {{counter} ਪੋਸਟਾਂ}}",
|
||||
"account.unblock": "@{name} ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ",
|
||||
"account.unblock_domain": "{domain} ਡੋਮੇਨ ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ",
|
||||
"account.unblock_domain_short": "ਪਾਬੰਦੀ ਹਟਾਓ",
|
||||
"account.unblock_short": "ਪਾਬੰਦੀ ਹਟਾਓ",
|
||||
"account.unendorse": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਫ਼ੀਚਰ ਨਾ ਕਰੋ",
|
||||
"account.unfollow": "ਅਣ-ਫ਼ਾਲੋ",
|
||||
@@ -148,6 +167,8 @@
|
||||
"confirmations.missing_alt_text.secondary": "ਕਿਵੇਂ ਵੀ ਪੋਸਟ ਕਰੋ",
|
||||
"confirmations.mute.confirm": "ਮੌਨ ਕਰੋ",
|
||||
"confirmations.redraft.confirm": "ਹਟਾਓ ਤੇ ਮੁੜ-ਡਰਾਫਟ",
|
||||
"confirmations.remove_from_followers.confirm": "ਫ਼ਾਲੋਅਰ ਨੂੰ ਹਟਾਓ",
|
||||
"confirmations.remove_from_followers.title": "ਫ਼ਾਲੋਅਰ ਨੂੰ ਹਟਾਉਣਾ ਹੈ?",
|
||||
"confirmations.unfollow.confirm": "ਅਣ-ਫ਼ਾਲੋ",
|
||||
"confirmations.unfollow.message": "ਕੀ ਤੁਸੀਂ {name} ਨੂੰ ਅਣ-ਫ਼ਾਲੋ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"confirmations.unfollow.title": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਅਣ-ਫ਼ਾਲੋ ਕਰਨਾ ਹੈ?",
|
||||
@@ -182,7 +203,9 @@
|
||||
"emoji_button.custom": "ਕਸਟਮ",
|
||||
"emoji_button.flags": "ਝੰਡੀਆਂ",
|
||||
"emoji_button.food": "ਖਾਣਾ-ਪੀਣਾ",
|
||||
"emoji_button.label": "ਇਮੋਜੀ ਪਾਓ",
|
||||
"emoji_button.nature": "ਕੁਦਰਤ",
|
||||
"emoji_button.not_found": "ਕੋਈ ਮਿਲਦਾ ਇਮੋਜ਼ੀ ਨਹੀਂ ਲੱਭਿਆ",
|
||||
"emoji_button.objects": "ਇਕਾਈ",
|
||||
"emoji_button.people": "ਲੋਕ",
|
||||
"emoji_button.recent": "ਅਕਸਰ ਵਰਤੇ",
|
||||
@@ -199,9 +222,15 @@
|
||||
"empty_column.list": "ਇਸ ਸੂਚੀ ਵਿੱਚ ਹਾਲੇ ਕੁਝ ਵੀ ਨਹੀਂ ਹੈ। ਜਦੋਂ ਇਸ ਸੂਚੀ ਦੇ ਮੈਂਬਰ ਨਵੀਆਂ ਪੋਸਟਾਂ ਪਾਉਂਦੇ ਹਨ ਤਾਂ ਉਹ ਇੱਥੇ ਦਿਖਾਈ ਦੇਣਗੀਆਂ।",
|
||||
"errors.unexpected_crash.report_issue": "ਮੁੱਦੇ ਦੀ ਰਿਪੋਰਟ ਕਰੋ",
|
||||
"explore.suggested_follows": "ਲੋਕ",
|
||||
"explore.title": "ਰੁਝਾਨ",
|
||||
"explore.trending_links": "ਖ਼ਬਰਾਂ",
|
||||
"explore.trending_statuses": "ਪੋਸਟਾਂ",
|
||||
"explore.trending_tags": "ਹੈਸ਼ਟੈਗ",
|
||||
"featured_carousel.header": "{count, plural, one {ਟੰਗੀ ਹੋਈ ਪੋਸਟ} other {ਟੰਗੀਆਂ ਹੋਈਆਂ ਪੋਸਟਾਂ}}",
|
||||
"featured_carousel.next": "ਅੱਗੇ",
|
||||
"featured_carousel.post": "ਪੋਸਟ",
|
||||
"featured_carousel.previous": "ਪਿੱਛੇ",
|
||||
"featured_carousel.slide": "{total} ਵਿੱਚੋਂ {index}",
|
||||
"filter_modal.added.expired_title": "ਫਿਲਟਰ ਦੀ ਮਿਆਦ ਪੁੱਗੀ!",
|
||||
"filter_modal.added.review_and_configure_title": "ਫਿਲਟਰ ਸੈਟਿੰਗਾਂ",
|
||||
"filter_modal.added.settings_link": "ਸੈਟਿੰਗਾਂ ਸਫ਼ਾ",
|
||||
@@ -252,6 +281,8 @@
|
||||
"home.column_settings.show_replies": "ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ",
|
||||
"home.hide_announcements": "ਐਲਾਨਾਂ ਨੂੰ ਓਹਲੇ ਕਰੋ",
|
||||
"home.pending_critical_update.link": "ਅੱਪਡੇਟ ਵੇਖੋ",
|
||||
"home.pending_critical_update.title": "ਗੰਭੀਰ ਸੁਰੱਖਿਆ ਅੱਪਡੇਟ ਮੌਜੂਦ ਹੈ!",
|
||||
"home.show_announcements": "ਐਲਾਨਾਂ ਨੂੰ ਵੇਖਾਓ",
|
||||
"ignore_notifications_modal.ignore": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਅਣਡਿੱਠਾ ਕਰੋ",
|
||||
"info_button.label": "ਮਦਦ",
|
||||
"interaction_modal.go": "ਜਾਓ",
|
||||
@@ -332,9 +363,12 @@
|
||||
"media_gallery.hide": "ਲੁਕਾਓ",
|
||||
"mute_modal.hide_from_notifications": "ਨੋਟੀਫਿਕੇਸ਼ਨਾਂ ਵਿੱਚੋਂ ਲੁਕਾਓ",
|
||||
"mute_modal.show_options": "ਚੋਣਾਂ ਨੂੰ ਵੇਖਾਓ",
|
||||
"mute_modal.title": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਮੌਨ ਕਰਨਾ ਹੈ?",
|
||||
"navigation_bar.about": "ਇਸ ਬਾਰੇ",
|
||||
"navigation_bar.account_settings": "ਪਾਸਵਰਡ ਅਤੇ ਸੁਰੱਖਿਆ",
|
||||
"navigation_bar.administration": "ਪਰਸ਼ਾਸ਼ਨ",
|
||||
"navigation_bar.advanced_interface": "ਤਕਨੀਕੀ ਵੈੱਬ ਇੰਟਰਫੇਸ ਵਿੱਚ ਖੋਲ੍ਹੋ",
|
||||
"navigation_bar.automated_deletion": "ਆਪਣੇ-ਆਪ ਹਟਾਈ ਪੋਸਟ",
|
||||
"navigation_bar.blocks": "ਪਾਬੰਦੀ ਲਾਏ ਵਰਤੋਂਕਾਰ",
|
||||
"navigation_bar.bookmarks": "ਬੁੱਕਮਾਰਕ",
|
||||
"navigation_bar.direct": "ਨਿੱਜੀ ਜ਼ਿਕਰ",
|
||||
@@ -346,11 +380,16 @@
|
||||
"navigation_bar.follows_and_followers": "ਫ਼ਾਲੋ ਅਤੇ ਫ਼ਾਲੋ ਕਰਨ ਵਾਲੇ",
|
||||
"navigation_bar.lists": "ਸੂਚੀਆਂ",
|
||||
"navigation_bar.logout": "ਲਾਗ ਆਉਟ",
|
||||
"navigation_bar.more": "ਹੋਰ",
|
||||
"navigation_bar.mutes": "ਮੌਨ ਕੀਤੇ ਵਰਤੋਂਕਾਰ",
|
||||
"navigation_bar.preferences": "ਪਸੰਦਾਂ",
|
||||
"navigation_bar.privacy_and_reach": "ਪਰਦੇਦਾਰੀ ਅਤੇ ਪਹੁੰਚ",
|
||||
"navigation_bar.search": "ਖੋਜੋ",
|
||||
"navigation_bar.search_trends": "ਖੋਜ / ਰੁਝਾਨ",
|
||||
"not_signed_in_indicator.not_signed_in": "ਇਹ ਸਰੋਤ ਵਰਤਣ ਲਈ ਤੁਹਾਨੂੰ ਲਾਗਇਨ ਕਰਨ ਦੀ ਲੋੜ ਹੈ।",
|
||||
"notification.admin.sign_up": "{name} ਨੇ ਸਾਈਨ ਅੱਪ ਕੀਤਾ",
|
||||
"notification.favourite": "{name} ਨੇ ਤੁਹਾਡੀ ਪੋਸਟ ਨੂੰ ਪਸੰਦ ਕੀਤਾ",
|
||||
"notification.favourite_pm": "{name} ਨੇ ਤੁਹਾਡੇ ਨਿੱਜੀ ਜ਼ਿਕਰ ਨੂੰ ਪਸੰਦ ਕੀਤਾ",
|
||||
"notification.follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕੀਤਾ",
|
||||
"notification.follow.name_and_others": "{name} ਅਤੇ <a>{count, plural, one {# ਹੋਰ} other {# ਹੋਰਾਂ}}</a> ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕੀਤਾ",
|
||||
"notification.follow_request": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ",
|
||||
@@ -365,6 +404,7 @@
|
||||
"notification.moderation_warning.action_silence": "ਤੁਹਾਡੇ ਖਾਤੇ ਨੂੰ ਸੀਮਿਤ ਕੀਤਾ ਗਿਆ ਹੈ।",
|
||||
"notification.moderation_warning.action_suspend": "ਤੁਹਾਡੇ ਖਾਤੇ ਨੂੰ ਮੁਅੱਤਲ ਕੀਤਾ ਗਿਆ ਹੈ।",
|
||||
"notification.reblog": "{name} boosted your status",
|
||||
"notification.relationships_severance_event": "{name} ਨਾਲ ਕਨੈਕਸ਼ਨ ਗੁਆਚੇ",
|
||||
"notification.relationships_severance_event.learn_more": "ਹੋਰ ਜਾਣੋ",
|
||||
"notification.status": "{name} ਨੇ ਹੁਣੇ ਪੋਸਟ ਕੀਤਾ",
|
||||
"notification.update": "{name} ਨੋ ਪੋਸਟ ਨੂੰ ਸੋਧਿਆ",
|
||||
@@ -540,7 +580,10 @@
|
||||
"status.unpin": "ਪਰੋਫਾਈਲ ਤੋਂ ਲਾਹੋ",
|
||||
"subscribed_languages.save": "ਤਬਦੀਲੀਆਂ ਸੰਭਾਲੋ",
|
||||
"tabs_bar.home": "ਘਰ",
|
||||
"tabs_bar.menu": "ਮੇਨੂ",
|
||||
"tabs_bar.notifications": "ਸੂਚਨਾਵਾਂ",
|
||||
"tabs_bar.publish": "ਨਵੀਂ ਪੋਸਟ",
|
||||
"tabs_bar.search": "ਖੋਜੋ",
|
||||
"terms_of_service.title": "ਸੇਵਾ ਦੀਆਂ ਸ਼ਰਤਾਂ",
|
||||
"time_remaining.days": "{number, plural, one {# ਦਿਨ} other {# ਦਿਨ}} ਬਾਕੀ",
|
||||
"time_remaining.hours": "{number, plural, one {# ਘੰਟਾ} other {# ਘੰਟੇ}} ਬਾਕੀ",
|
||||
@@ -561,6 +604,9 @@
|
||||
"video.expand": "ਵੀਡੀਓ ਨੂੰ ਫੈਲਾਓ",
|
||||
"video.fullscreen": "ਪੂਰੀ ਸਕਰੀਨ",
|
||||
"video.hide": "ਵੀਡੀਓ ਨੂੰ ਲੁਕਾਓ",
|
||||
"video.mute": "ਮੌਨ",
|
||||
"video.pause": "ਠਹਿਰੋ",
|
||||
"video.play": "ਚਲਾਓ"
|
||||
"video.play": "ਚਲਾਓ",
|
||||
"video.volume_down": "ਅਵਾਜ਼ ਘਟਾਓ",
|
||||
"video.volume_up": "ਅਵਾਜ਼ ਵਧਾਓ"
|
||||
}
|
||||
@@ -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": "私下提及",
|
||||
|
||||
Reference in new issue
Block a user