diff --git a/package.json b/package.json index 64d1b3ef3..4750bc864 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/compose.ts b/src/actions/compose.ts index 50c8afca1..a930e3270 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -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 | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType @@ -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, diff --git a/src/components/ui/button/button.tsx b/src/components/ui/button/button.tsx index 281709431..1aace03fb 100644 --- a/src/components/ui/button/button.tsx +++ b/src/components/ui/button/button.tsx @@ -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; diff --git a/src/components/ui/hstack/hstack.tsx b/src/components/ui/hstack/hstack.tsx index 5e1cfc66c..65a5b03d2 100644 --- a/src/components/ui/hstack/hstack.tsx +++ b/src/components/ui/hstack/hstack.tsx @@ -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, 'children' | 'className' | 'onClick' | 'style' | 'title'> { diff --git a/src/features/compose/components/compose-form.tsx b/src/features/compose/components/compose-form.tsx index 944adc84d..3e76705aa 100644 --- a/src/features/compose/components/compose-form.tsx +++ b/src/features/compose/components/compose-form.tsx @@ -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, shouldCondense, autoFocus, clickab if (features.privacyScopes && !group && !groupId) selectButtons.push(); if (features.richText) selectButtons.push(); + selectButtons.push(); return ( diff --git a/src/features/compose/components/content-type-button.tsx b/src/features/compose/components/content-type-button.tsx new file mode 100644 index 000000000..01fac99eb --- /dev/null +++ b/src/features/compose/components/content-type-button.tsx @@ -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 = ({ 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 ( + ({ + icon, + text, + action: handleChange(value), + active: contentType === value, + }))} + > +