+
{children}
diff --git a/app/javascript/mastodon/components/menu/card.tsx b/app/javascript/mastodon/components/menu/card.tsx
index 39a2b2e521a..90e90beb3f3 100644
--- a/app/javascript/mastodon/components/menu/card.tsx
+++ b/app/javascript/mastodon/components/menu/card.tsx
@@ -18,7 +18,7 @@ export type MenuCardProps
= PolymorphicProps<
As
>;
-export const MenuCard = ({
+export const MenuCard = ({
as: asComp,
children,
className,
diff --git a/app/javascript/mastodon/components/menu/index.tsx b/app/javascript/mastodon/components/menu/index.tsx
index 21d963aae81..e83df85b598 100644
--- a/app/javascript/mastodon/components/menu/index.tsx
+++ b/app/javascript/mastodon/components/menu/index.tsx
@@ -247,7 +247,7 @@ export const Menu: React.FC = ({
return {children};
};
-export const MenuTrigger = ({
+export const MenuTrigger = ({
as: asComp,
children,
...props
diff --git a/app/javascript/mastodon/features/compose/redesign/hints.tsx b/app/javascript/mastodon/features/compose/redesign/hints.tsx
index fe7afe2e887..c87e8919a7e 100644
--- a/app/javascript/mastodon/features/compose/redesign/hints.tsx
+++ b/app/javascript/mastodon/features/compose/redesign/hints.tsx
@@ -15,7 +15,7 @@ import {
useAppSelector,
} from '@/mastodon/store';
-import { useLanguageGuess, useLanguages } from './hooks';
+import { languageName, useLanguageGuess } from './hooks';
import { selectComposeAttachments } from './selectors';
const selectComposeAttachmentsWithoutAlt = createAppSelector(
@@ -105,8 +105,7 @@ const defaultWrapper = (children: React.ReactNode, key: string) => (
);
const LanguageHint: React.FC<{ guess: string }> = ({ guess }) => {
- const languages = useLanguages();
- const language = languages.find(([lang]) => lang === guess);
+ const language = languageName(guess);
const { wasDismissed, dismiss } = useDismissible('compose_language_hint');
@@ -142,9 +141,7 @@ const LanguageHint: React.FC<{ guess: string }> = ({ guess }) => {
);
diff --git a/app/javascript/mastodon/features/compose/redesign/hooks.ts b/app/javascript/mastodon/features/compose/redesign/hooks.ts
index e41800095ed..bf8bc7b3586 100644
--- a/app/javascript/mastodon/features/compose/redesign/hooks.ts
+++ b/app/javascript/mastodon/features/compose/redesign/hooks.ts
@@ -1,13 +1,19 @@
-import { useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+
+import type { Map as ImmutableMap } from 'immutable';
+
+import { useDebouncedCallback } from 'use-debounce';
import type { InitialStateLanguage } from '@/mastodon/initial_state';
import { languages } from '@/mastodon/initial_state';
-import { useAppSelector } from '@/mastodon/store';
-
-const emptyArray: InitialStateLanguage[] = [];
+import { createAppSelector, useAppSelector } from '@/mastodon/store';
export function useLanguages() {
- return languages ?? emptyArray;
+ return languages;
+}
+
+export function languageName(code: string) {
+ return languages?.find(([lang]) => lang === code)?.[1];
}
export function useLanguageGuess() {
@@ -41,3 +47,96 @@ export function useLanguageGuess() {
return guess;
}
+
+const selectFrequentlyUsedLanguages = createAppSelector(
+ [
+ (state) =>
+ state.settings.get('frequentlyUsedLanguages') as
+ | ImmutableMap
+ | undefined,
+ ],
+ (languageCounters) =>
+ !languageCounters
+ ? []
+ : languageCounters
+ .keySeq()
+ .sort(
+ (a, b) =>
+ (languageCounters.get(a) ?? 0) - (languageCounters.get(b) ?? 0),
+ )
+ .reverse()
+ .toArray(),
+);
+
+export function useLanguageList() {
+ const frequentlyUsed = useAppSelector(selectFrequentlyUsedLanguages);
+ const currentLang = useAppSelector(
+ (state) => state.compose.get('language') as string,
+ );
+ const guess = useLanguageGuess();
+
+ const sortedLanguages = useMemo(() => {
+ if (!languages) {
+ return [];
+ }
+ return [...languages].sort((a, b) => {
+ if (guess && a[0] === guess) {
+ // Push guessed language higher than current selection
+ return -1;
+ } else if (guess && b[0] === guess) {
+ return 1;
+ } else if (a[0] === currentLang) {
+ // Push current selection to the top of the list
+ return -1;
+ } else if (b[0] === currentLang) {
+ return 1;
+ } else {
+ // Sort according to frequently used languages
+
+ const indexOfA = frequentlyUsed.indexOf(a[0]);
+ const indexOfB = frequentlyUsed.indexOf(b[0]);
+
+ return (
+ (indexOfA > -1 ? indexOfA : Infinity) -
+ (indexOfB > -1 ? indexOfB : Infinity)
+ );
+ }
+ });
+ }, [currentLang, frequentlyUsed, guess]);
+
+ const fuzzySortRef = useRef(null);
+ useEffect(() => {
+ void import('fuzzysort').then((fuzzySort) => {
+ fuzzySortRef.current = fuzzySort;
+ });
+ }, []);
+
+ const [searchResults, setSearchResults] = useState<
+ InitialStateLanguage[] | null
+ >(null);
+
+ const onSearch = useDebouncedCallback((search: string) => {
+ if (!search || !fuzzySortRef.current) {
+ setSearchResults(null);
+ return;
+ }
+ const results = fuzzySortRef.current
+ .go(search, languages ?? [], {
+ keys: ['0', '1', '2'],
+ limit: 5,
+ threshold: -10000,
+ })
+ .map((result) => result.obj);
+ setSearchResults(results);
+ }, 10);
+
+ const onClear = useCallback(() => {
+ setSearchResults(null);
+ }, []);
+
+ return {
+ onSearch,
+ onClear,
+ languages: searchResults ?? sortedLanguages,
+ };
+}
diff --git a/app/javascript/mastodon/features/compose/redesign/index.tsx b/app/javascript/mastodon/features/compose/redesign/index.tsx
index c1fc123a7bf..2ac686b4263 100644
--- a/app/javascript/mastodon/features/compose/redesign/index.tsx
+++ b/app/javascript/mastodon/features/compose/redesign/index.tsx
@@ -5,17 +5,15 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
-import { LockSimpleOpenIcon } from '@phosphor-icons/react';
+import { LockSimpleOpenIcon, PepperIcon } from '@phosphor-icons/react';
import {
changeComposeSpoilerness,
changeComposeSpoilerText,
insertEmojiCompose,
} from '@/mastodon/actions/compose';
-import {
- ToggleField,
- TextInputField,
-} from '@/mastodon/components/form_fields/redesign';
+import { ToggleButton } from '@/mastodon/components/button/redesign';
+import { TextInputField } from '@/mastodon/components/form_fields/redesign';
import { Icon } from '@/mastodon/components/icon';
import { useScrollSensor } from '@/mastodon/hooks/useScrollSensor';
import {
@@ -42,10 +40,6 @@ import { ComposeTextarea } from './textarea';
import { ComposeVisibility } from './visibility';
const messages = defineMessages({
- sensitive: {
- id: 'compose.sensitive',
- defaultMessage: 'Sensitive',
- },
sensitiveText: {
id: 'compose.sensitive.text',
defaultMessage: 'Sensitive content description',
@@ -95,14 +89,16 @@ export const RedesignComposeForm: React.FC = ({
-
-
+
+
+
+
{type === 'message' && (
diff --git a/app/javascript/mastodon/features/compose/redesign/language.tsx b/app/javascript/mastodon/features/compose/redesign/language.tsx
index ab57a404572..8c701927bc1 100644
--- a/app/javascript/mastodon/features/compose/redesign/language.tsx
+++ b/app/javascript/mastodon/features/compose/redesign/language.tsx
@@ -1,99 +1,101 @@
import type React from 'react';
-import { useCallback, useRef, useState } from 'react';
+import { useCallback } from 'react';
-import { FormattedMessage } from 'react-intl';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
-import { TranslateIcon } from '@phosphor-icons/react';
+import { CaretDownIcon, MagnifyingGlassIcon } from '@phosphor-icons/react';
import { changeComposeLanguage } from '@/mastodon/actions/compose';
-import { IconButton } from '@/mastodon/components/button/redesign';
-import { PopoverMenuCard } from '@/mastodon/components/menu/card';
+import { TextInput } from '@/mastodon/components/form_fields/redesign';
+import {
+ Menu,
+ MenuItem,
+ MenuList,
+ MenuTrigger,
+} from '@/mastodon/components/menu';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
-import { LanguageDropdownMenu } from '../components/language_dropdown';
-
-import { useLanguageGuess } from './hooks';
+import { useLanguageList } from './hooks';
import classes from './styles.module.scss';
+const messages = defineMessages({
+ searchPlaceholder: {
+ id: 'compose.language.search',
+ defaultMessage: 'Search languages...',
+ },
+});
+
export const LanguageButton: React.FC = () => {
- const [open, setOpen] = useState(false);
- const [trigger, setTrigger] = useState(null);
- const activeElementRef = useRef(null);
-
- const handleMouseDown = useCallback(() => {
- if (!open && document.activeElement instanceof HTMLElement) {
- activeElementRef.current = document.activeElement;
- }
- }, [open]);
-
- const handleToggle = useCallback(() => {
- if (open && activeElementRef.current)
- activeElementRef.current.focus({ preventScroll: true });
-
- setOpen(!open);
- }, [open]);
-
- const handleClose = useCallback(() => {
- if (open && activeElementRef.current)
- activeElementRef.current.focus({ preventScroll: true });
-
- setOpen(false);
- }, [open]);
+ const langCode = useAppSelector(
+ (state) => state.compose.get('language') as string,
+ );
return (
- <>
-
-
-
+
);
};
-export const LanguageDropdown: React.FC<{ onClose: () => void }> = ({
- onClose,
-}) => {
- const language = useAppSelector(
- (state) => state.compose.get('language') as string,
- );
- const guess = useLanguageGuess();
+export const LanguageDropdown = () => {
+ const { languages, onSearch } = useLanguageList();
const dispatch = useAppDispatch();
- const handleChange = useCallback(
- (newLanguage: string) => {
- dispatch(changeComposeLanguage(newLanguage));
- onClose();
+ const handleChange: React.MouseEventHandler = useCallback(
+ (event) => {
+ const newLanguage = event.currentTarget.dataset.language;
+ if (newLanguage) {
+ dispatch(changeComposeLanguage(newLanguage));
+ }
},
- [dispatch, onClose],
+ [dispatch],
+ );
+
+ const intl = useIntl();
+ const handleSearch: React.ChangeEventHandler = useCallback(
+ (event) => {
+ onSearch(event.target.value);
+ },
+ [onSearch],
);
return (
-
+ <>
+
+
+ {languages.map((lang) => (
+
+ ))}
+
+ {languages.length === 0 && (
+
+ )}
+
+ >
);
};
diff --git a/app/javascript/mastodon/features/compose/redesign/styles.module.scss b/app/javascript/mastodon/features/compose/redesign/styles.module.scss
index 90c3299b1d2..63f0f2726c6 100644
--- a/app/javascript/mastodon/features/compose/redesign/styles.module.scss
+++ b/app/javascript/mastodon/features/compose/redesign/styles.module.scss
@@ -184,22 +184,30 @@ textarea.textarea {
color: var(--color-text-error);
}
+// Language selector
+
.languageMenu {
- padding: var(--space-xs) var(--space-sm);
+ padding: var(--space-xs);
- :global(.emoji-mart-search) {
- padding: 0;
- padding-inline-end: 0;
+ input {
+ margin-bottom: var(--space-xs);
+
+ &:focus::placeholder {
+ color: transparent;
+ }
}
+}
- :global(.emoji-mart-search-icon) {
- top: 0;
- inset-inline-end: 0;
- }
+.languageList {
+ max-height: 350px;
+ overflow-y: auto;
+}
- :global(.emoji-mart-scroll) {
- padding: 0;
- margin-top: var(--space-xs);
+.languageItem {
+ display: block;
+
+ span {
+ color: var(--color-text-secondary);
}
}
diff --git a/app/javascript/mastodon/features/compose/redesign/visibility.tsx b/app/javascript/mastodon/features/compose/redesign/visibility.tsx
index 92611abcd65..72c21a40035 100644
--- a/app/javascript/mastodon/features/compose/redesign/visibility.tsx
+++ b/app/javascript/mastodon/features/compose/redesign/visibility.tsx
@@ -4,6 +4,7 @@ import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import {
+ CaretDownIcon,
ChatCircleIcon,
MagnifyingGlassIcon,
NewspaperIcon,
@@ -46,7 +47,7 @@ export const ComposeVisibility: React.FC<{ className?: string }> = ({
description='Before button that indicates who a post is for (Public, Followers, mentioned people)'
/>