Allow HTML, add language picker
Utilizes code from Mastodon LanguageDropdown: https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/features/compose/components/language_dropdown.jsx Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
4ceef0fe81
commit
2e43f8967f
11 changed files with 444 additions and 15 deletions
|
@ -113,6 +113,7 @@
|
|||
"escape-html": "^1.0.3",
|
||||
"eslint-plugin-formatjs": "^4.12.2",
|
||||
"exifr": "^7.1.3",
|
||||
"fuzzysort": "^3.0.0",
|
||||
"graphemesplit": "^2.4.4",
|
||||
"html-react-parser": "^5.0.0",
|
||||
"http-link-header": "^1.0.2",
|
||||
|
|
|
@ -5,6 +5,7 @@ import { defineMessages, IntlShape } from 'react-intl';
|
|||
import api from 'soapbox/api';
|
||||
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||
import emojiSearch from 'soapbox/features/emoji/search';
|
||||
import { Language } from 'soapbox/features/preferences';
|
||||
import { normalizeTag } from 'soapbox/normalizers';
|
||||
import { selectAccount, selectOwnAccount, makeGetAccount } from 'soapbox/selectors';
|
||||
import { tagHistory } from 'soapbox/settings';
|
||||
|
@ -59,6 +60,7 @@ const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const;
|
|||
const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE' as const;
|
||||
const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const;
|
||||
const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const;
|
||||
const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE' as const;
|
||||
const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const;
|
||||
|
||||
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const;
|
||||
|
@ -372,6 +374,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
content_type: compose.content_type,
|
||||
poll: compose.poll,
|
||||
scheduled_at: compose.schedule,
|
||||
language: compose.language,
|
||||
to,
|
||||
};
|
||||
|
||||
|
@ -725,6 +728,12 @@ const changeComposeVisibility = (composeId: string, value: string) => ({
|
|||
value,
|
||||
});
|
||||
|
||||
const changeComposeLanguage = (composeId: string, value: Language | null) => ({
|
||||
type: COMPOSE_LANGUAGE_CHANGE,
|
||||
id: composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
const insertEmojiCompose = (composeId: string, position: number, emoji: Emoji, needsSpace: boolean) => ({
|
||||
type: COMPOSE_EMOJI_INSERT,
|
||||
id: composeId,
|
||||
|
@ -908,6 +917,7 @@ type ComposeAction =
|
|||
| ReturnType<typeof changeComposeContentType>
|
||||
| ReturnType<typeof changeComposeSpoilerText>
|
||||
| ReturnType<typeof changeComposeVisibility>
|
||||
| ReturnType<typeof changeComposeLanguage>
|
||||
| ReturnType<typeof insertEmojiCompose>
|
||||
| ReturnType<typeof addPoll>
|
||||
| ReturnType<typeof removePoll>
|
||||
|
@ -953,6 +963,7 @@ export {
|
|||
COMPOSE_TYPE_CHANGE,
|
||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
COMPOSE_VISIBILITY_CHANGE,
|
||||
COMPOSE_LANGUAGE_CHANGE,
|
||||
COMPOSE_LISTABILITY_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
|
@ -1012,6 +1023,7 @@ export {
|
|||
changeComposeContentType,
|
||||
changeComposeSpoilerText,
|
||||
changeComposeVisibility,
|
||||
changeComposeLanguage,
|
||||
insertEmojiCompose,
|
||||
addPoll,
|
||||
removePoll,
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { ButtonSizes, ButtonThemes } from './useButtonStyles';
|
|||
|
||||
interface IButton extends Pick<
|
||||
React.ComponentProps<'button'>,
|
||||
'children' | 'className' | 'disabled' | 'onClick' | 'onMouseDown' | 'onKeyDown' | 'title' | 'type'
|
||||
'children' | 'className' | 'disabled' | 'onClick' | 'onMouseDown' | 'onKeyDown' | 'onKeyPress' | 'title' | 'type'
|
||||
> {
|
||||
/** Whether this button expands the width of its container. */
|
||||
block?: boolean;
|
||||
|
|
|
@ -18,17 +18,17 @@ const alignItemsOptions = {
|
|||
};
|
||||
|
||||
const spaces = {
|
||||
0: 'gap-x-0',
|
||||
[0.5]: 'gap-x-0.5',
|
||||
1: 'gap-x-1',
|
||||
1.5: 'gap-x-1.5',
|
||||
2: 'gap-x-2',
|
||||
2.5: 'gap-x-2.5',
|
||||
3: 'gap-x-3',
|
||||
4: 'gap-x-4',
|
||||
5: 'gap-x-5',
|
||||
6: 'gap-x-6',
|
||||
8: 'gap-x-8',
|
||||
0: 'gap-0',
|
||||
[0.5]: 'gap-0.5',
|
||||
1: 'gap-1',
|
||||
1.5: 'gap-1.5',
|
||||
2: 'gap-2',
|
||||
2.5: 'gap-2.5',
|
||||
3: 'gap-3',
|
||||
4: 'gap-4',
|
||||
5: 'gap-5',
|
||||
6: 'gap-6',
|
||||
8: 'gap-8',
|
||||
};
|
||||
|
||||
interface IHStack extends Pick<React.HTMLAttributes<HTMLDivElement>, 'children' | 'className' | 'onClick' | 'style' | 'title'> {
|
||||
|
|
|
@ -27,6 +27,7 @@ import { $createEmojiNode } from '../editor/nodes/emoji-node';
|
|||
import { countableText } from '../util/counter';
|
||||
|
||||
import ContentTypeButton from './content-type-button';
|
||||
import LanguageDropdown from './language-dropdown';
|
||||
import PollButton from './poll-button';
|
||||
import PollForm from './polls/poll-form';
|
||||
import PrivacyDropdown from './privacy-dropdown';
|
||||
|
@ -242,6 +243,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
if (features.privacyScopes && !group && !groupId) selectButtons.push(<PrivacyDropdown composeId={id} />);
|
||||
if (features.richText) selectButtons.push(<ContentTypeButton composeId={id} />);
|
||||
selectButtons.push(<LanguageDropdown composeId={id} />);
|
||||
|
||||
return (
|
||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||
|
|
69
src/features/compose/components/content-type-button.tsx
Normal file
69
src/features/compose/components/content-type-button.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeComposeContentType } from 'soapbox/actions/compose';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
content_type_plaintext: { id: 'preferences.options.content_type_plaintext', defaultMessage: 'Plain text' },
|
||||
content_type_markdown: { id: 'preferences.options.content_type_markdown', defaultMessage: 'Markdown' },
|
||||
content_type_html: { id: 'preferences.options.content_type_html', defaultMessage: 'HTML' },
|
||||
change_content_type: { id: 'compose_form.content_type.change', defaultMessage: 'Change content type' },
|
||||
});
|
||||
|
||||
interface IContentTypeButton {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const ContentTypeButton: React.FC<IContentTypeButton> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const contentType = useCompose(composeId).content_type;
|
||||
|
||||
const handleChange = (contentType: string) => () => dispatch(changeComposeContentType(composeId, contentType));
|
||||
|
||||
const options = [
|
||||
{
|
||||
icon: require('@tabler/icons/outline/pilcrow.svg'),
|
||||
text: intl.formatMessage(messages.content_type_plaintext),
|
||||
value: 'text/plain',
|
||||
},
|
||||
{ icon: require('@tabler/icons/outline/markdown.svg'),
|
||||
text: intl.formatMessage(messages.content_type_markdown),
|
||||
value: 'text/markdown',
|
||||
},
|
||||
{
|
||||
icon: require('@tabler/icons/outline/html.svg'),
|
||||
text: intl.formatMessage(messages.content_type_html),
|
||||
value: 'text/html',
|
||||
},
|
||||
];
|
||||
|
||||
const option = options.find(({ value }) => value === contentType);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
items={options.map(({ icon, text, value }) => ({
|
||||
icon,
|
||||
text,
|
||||
action: handleChange(value),
|
||||
active: contentType === value,
|
||||
}))}
|
||||
>
|
||||
<Button
|
||||
theme='muted'
|
||||
size='xs'
|
||||
text={option?.text}
|
||||
icon={option?.icon}
|
||||
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
|
||||
title={intl.formatMessage(messages.change_content_type)}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export { ContentTypeButton as default };
|
329
src/features/compose/components/language-dropdown.tsx
Normal file
329
src/features/compose/components/language-dropdown.tsx
Normal file
|
@ -0,0 +1,329 @@
|
|||
import { offset, useFloating, flip, arrow, shift } from '@floating-ui/react';
|
||||
import clsx from 'clsx';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeComposeLanguage } from 'soapbox/actions/compose';
|
||||
import { Button, Icon, Input, Portal } from 'soapbox/components/ui';
|
||||
import { type Language, languages as languagesObject } from 'soapbox/features/preferences';
|
||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
const languages = Object.entries(languagesObject) as Array<[Language, string]>;
|
||||
|
||||
const messages = defineMessages({
|
||||
languagePrompt: { id: 'compose.language_dropdown.prompt', defaultMessage: 'Select language' },
|
||||
search: { id: 'compose.language_dropdown.search', defaultMessage: 'Search language…' },
|
||||
});
|
||||
|
||||
interface ILanguageDropdown {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const focusedItem = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const arrowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
|
||||
placement: 'top',
|
||||
middleware: [
|
||||
offset(12),
|
||||
flip(),
|
||||
shift({
|
||||
padding: 8,
|
||||
}),
|
||||
arrow({
|
||||
element: arrowRef,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const language = useCompose(composeId).language;
|
||||
|
||||
const handleClick: React.EventHandler<
|
||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
> = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleKeyPress: React.EventHandler<React.KeyboardEvent<HTMLButtonElement>> = (event) => {
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleClick(event);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (language: Language | null) => dispatch(changeComposeLanguage(composeId, language));
|
||||
|
||||
const handleOptionKeyDown: React.KeyboardEventHandler = e => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
const index = results.findIndex(([key]) => key === value);
|
||||
let element: ChildNode | null | undefined = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
setIsOpen(false);
|
||||
break;
|
||||
case 'Enter':
|
||||
handleOptionClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = node.current?.childNodes[index + 1] || node.current?.firstChild;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = node.current?.childNodes[index - 1] || node.current?.lastChild;
|
||||
break;
|
||||
case 'Home':
|
||||
element = node.current?.firstChild;
|
||||
break;
|
||||
case 'End':
|
||||
element = node.current?.lastChild;
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOptionClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
|
||||
const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index') as Language;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
setIsOpen(false);
|
||||
handleChange(value);
|
||||
};
|
||||
|
||||
const handleClear: React.MouseEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setSearchValue('');
|
||||
};
|
||||
|
||||
const search = () => {
|
||||
if (searchValue === '') {
|
||||
return [...languages].sort((a, b) => {
|
||||
// Push current selection to the top of the list
|
||||
|
||||
if (a[0] === language) {
|
||||
return -1;
|
||||
} else if (b[0] === language) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return fuzzysort.go(searchValue, languages, {
|
||||
keys: ['0', '1'],
|
||||
limit: 5,
|
||||
threshold: -10000,
|
||||
}).map(result => result.obj);
|
||||
};
|
||||
|
||||
const handleDocumentClick = (event: Event) => {
|
||||
if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!refs.floating.current) return;
|
||||
|
||||
const items = Array.from(refs.floating.current.getElementsByTagName('a'));
|
||||
const index = items.indexOf(document.activeElement as any);
|
||||
|
||||
let element = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
element = items[index + 1] || items[0];
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = items[index - 1] || items[items.length - 1];
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = items[index - 1] || items[items.length - 1];
|
||||
} else {
|
||||
element = items[index + 1] || items[0];
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = items[0];
|
||||
break;
|
||||
case 'End':
|
||||
element = items[items.length - 1];
|
||||
break;
|
||||
case 'Escape':
|
||||
setIsOpen(false);
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const arrowProps: React.CSSProperties = useMemo(() => {
|
||||
if (middlewareData.arrow) {
|
||||
const { x, y } = middlewareData.arrow;
|
||||
|
||||
const staticPlacement = {
|
||||
top: 'bottom',
|
||||
right: 'left',
|
||||
bottom: 'top',
|
||||
left: 'right',
|
||||
}[placement.split('-')[0]];
|
||||
|
||||
return {
|
||||
left: x !== null ? `${x}px` : '',
|
||||
top: y !== null ? `${y}px` : '',
|
||||
// Ensure the static side gets unset when
|
||||
// flipping to other placements' axes.
|
||||
right: '',
|
||||
bottom: '',
|
||||
[staticPlacement as string]: `${(-(arrowRef.current?.offsetWidth || 0)) / 2}px`,
|
||||
transform: 'rotate(45deg)',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}, [middlewareData.arrow, placement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (refs.floating.current) {
|
||||
(refs.floating.current?.querySelector('li a[role=\'button\']') as HTMLAnchorElement)?.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleDocumentClick, false);
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('touchend', handleDocumentClick);
|
||||
};
|
||||
}
|
||||
}, [isOpen, refs.floating.current]);
|
||||
|
||||
const isSearching = searchValue !== '';
|
||||
const results = search();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme='muted'
|
||||
size='xs'
|
||||
text={language ? languagesObject[language] : intl.formatMessage(messages.languagePrompt)}
|
||||
icon={require('@tabler/icons/outline/language.svg')}
|
||||
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
|
||||
title={intl.formatMessage(messages.languagePrompt)}
|
||||
onClick={handleClick}
|
||||
onKeyPress={handleKeyPress}
|
||||
ref={refs.setReference}
|
||||
/>
|
||||
{isOpen ? (
|
||||
<Portal>
|
||||
<div
|
||||
id='language-dropdown'
|
||||
ref={refs.setFloating}
|
||||
className={clsx('z-[1001] flex flex-col rounded-md bg-white text-sm shadow-lg transition-opacity duration-100 focus:outline-none black:border black:border-gray-800 black:bg-black dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
|
||||
'opacity-0 pointer-events-none': !isOpen,
|
||||
})}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
}}
|
||||
role='listbox'
|
||||
>
|
||||
<label className='relative grow p-2'>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<Input
|
||||
className='w-64'
|
||||
type='text'
|
||||
value={searchValue}
|
||||
onChange={({ target }) => setSearchValue(target.value)}
|
||||
outerClassName='mt-0'
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
<div role='button' tabIndex={0} className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-5 rtl:left-0 rtl:right-auto' onClick={handleClear}>
|
||||
<Icon
|
||||
className='h-5 w-5 text-gray-600'
|
||||
src={isSearching ? require('@tabler/icons/outline/backspace.svg') : require('@tabler/icons/outline/search.svg')}
|
||||
aria-label={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<div className='h-96 w-full overflow-scroll' ref={node} tabIndex={-1}>
|
||||
{results.map(([code, name]) => {
|
||||
const active = code === language;
|
||||
|
||||
return (
|
||||
<div
|
||||
role='option'
|
||||
tabIndex={0}
|
||||
key={code}
|
||||
data-index={code}
|
||||
onKeyDown={handleOptionKeyDown}
|
||||
onClick={handleOptionClick}
|
||||
className={clsx(
|
||||
'flex cursor-pointer p-2.5 text-sm text-gray-700 hover:bg-gray-100 black:hover:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800',
|
||||
{ 'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700': active },
|
||||
)}
|
||||
aria-selected={active}
|
||||
ref={active ? focusedItem : null}
|
||||
>
|
||||
<div
|
||||
className={clsx('flex-auto text-primary-600 dark:text-primary-400', {
|
||||
'text-black dark:text-white': active,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
ref={arrowRef}
|
||||
style={arrowProps}
|
||||
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white black:bg-black dark:bg-gray-900'
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export { LanguageDropdown as default };
|
|
@ -73,7 +73,9 @@ const languages = {
|
|||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
};
|
||||
} as const;
|
||||
|
||||
type Language = keyof typeof languages;
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.preferences', defaultMessage: 'Preferences' },
|
||||
|
@ -242,4 +244,4 @@ const Preferences = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export { Preferences as default, languages };
|
||||
export { Preferences as default, languages, type Language };
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"compose.character_counter.title": "Used {chars} out of {maxChars} {maxChars, plural, one {character} other {characters}}",
|
||||
"compose.edit_success": "Your post was edited",
|
||||
"compose.invalid_schedule": "You must schedule a post at least 5 minutes out.",
|
||||
"compose.language_dropdown.prompt": "Select language",
|
||||
"compose.reply_group_indicator.message": "Posting to {groupLink}",
|
||||
"compose.submit_success": "Your post was sent!",
|
||||
"compose_event.create": "Create",
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
COMPOSE_TYPE_CHANGE,
|
||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
COMPOSE_VISIBILITY_CHANGE,
|
||||
COMPOSE_LANGUAGE_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
|
@ -52,9 +53,9 @@ import {
|
|||
COMPOSE_SET_STATUS,
|
||||
COMPOSE_EVENT_REPLY,
|
||||
COMPOSE_EDITOR_STATE_SET,
|
||||
ComposeAction,
|
||||
COMPOSE_CHANGE_MEDIA_ORDER,
|
||||
COMPOSE_ADD_SUGGESTED_QUOTE,
|
||||
ComposeAction,
|
||||
} from '../actions/compose';
|
||||
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
|
||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me';
|
||||
|
@ -64,6 +65,7 @@ import { normalizeAttachment } from '../normalizers/attachment';
|
|||
import { unescapeHTML } from '../utils/html';
|
||||
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
import type { Language } from 'soapbox/features/preferences';
|
||||
import type {
|
||||
APIEntity,
|
||||
Attachment as AttachmentEntity,
|
||||
|
@ -111,6 +113,7 @@ const ReducerCompose = ImmutableRecord({
|
|||
to: ImmutableOrderedSet<string>(),
|
||||
parent_reblogged_by: null as string | null,
|
||||
dismissed_quotes: ImmutableOrderedSet<string>(),
|
||||
language: null as Language | null,
|
||||
});
|
||||
|
||||
type State = ImmutableMap<string, Compose>;
|
||||
|
@ -301,6 +304,11 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
|||
return updateCompose(state, action.id, compose => compose
|
||||
.set('privacy', action.value)
|
||||
.set('idempotencyKey', uuid()));
|
||||
case COMPOSE_LANGUAGE_CHANGE:
|
||||
return updateCompose(state, action.id, compose => compose.withMutations(map => {
|
||||
map.set('language', action.value);
|
||||
map.set('idempotencyKey', uuid());
|
||||
}));
|
||||
case COMPOSE_CHANGE:
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('text', action.text)
|
||||
|
|
|
@ -4953,6 +4953,11 @@ functions-have-names@^1.2.3:
|
|||
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
||||
|
||||
fuzzysort@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-3.0.0.tgz#3dbc8fb712a506b8aa5ebefc7bc0e5e733e5040a"
|
||||
integrity sha512-FIq+LaBRMt4aybcsNpFVWSsMNZOY9LbCRyy559+vvk6Ckgl4LptjpgTE7pgwTMQ+K1Q6u6LyFmJ1N0qUJIjp+Q==
|
||||
|
||||
gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
|
|
Loading…
Reference in a new issue