WIP multilang posting
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -62,6 +62,8 @@ 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_LANGUAGE_ADD = 'COMPOSE_LANGUAGE_ADD' as const;
|
||||
const COMPOSE_LANGUAGE_DELETE = 'COMPOSE_LANGUAGE_DELETE' as const;
|
||||
const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const;
|
||||
|
||||
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const;
|
||||
@ -741,6 +743,18 @@ const changeComposeLanguage = (composeId: string, value: Language | null) => ({
|
||||
value,
|
||||
});
|
||||
|
||||
const addComposeLanguage = (composeId: string, value: Language | null) => ({
|
||||
type: COMPOSE_LANGUAGE_ADD,
|
||||
id: composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
const deleteComposeLanguage = (composeId: string, value: Language | null) => ({
|
||||
type: COMPOSE_LANGUAGE_DELETE,
|
||||
id: composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
const insertEmojiCompose = (composeId: string, position: number, emoji: Emoji, needsSpace: boolean) => ({
|
||||
type: COMPOSE_EMOJI_INSERT,
|
||||
id: composeId,
|
||||
@ -931,6 +945,8 @@ type ComposeAction =
|
||||
| ReturnType<typeof changeComposeSpoilerText>
|
||||
| ReturnType<typeof changeComposeVisibility>
|
||||
| ReturnType<typeof changeComposeLanguage>
|
||||
| ReturnType<typeof addComposeLanguage>
|
||||
| ReturnType<typeof deleteComposeLanguage>
|
||||
| ReturnType<typeof insertEmojiCompose>
|
||||
| ReturnType<typeof addPoll>
|
||||
| ReturnType<typeof removePoll>
|
||||
@ -978,6 +994,8 @@ export {
|
||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
COMPOSE_VISIBILITY_CHANGE,
|
||||
COMPOSE_LANGUAGE_CHANGE,
|
||||
COMPOSE_LANGUAGE_ADD,
|
||||
COMPOSE_LANGUAGE_DELETE,
|
||||
COMPOSE_LISTABILITY_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
@ -1039,6 +1057,8 @@ export {
|
||||
changeComposeSpoilerText,
|
||||
changeComposeVisibility,
|
||||
changeComposeLanguage,
|
||||
addComposeLanguage,
|
||||
deleteComposeLanguage,
|
||||
insertEmojiCompose,
|
||||
addPoll,
|
||||
removePoll,
|
||||
|
||||
@ -23,6 +23,7 @@ interface IInstanceFavicon {
|
||||
|
||||
const messages = defineMessages({
|
||||
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
||||
languageVersions: { id: 'status.language_versions', defaultMessage: 'The post has multiple language versions.' },
|
||||
});
|
||||
|
||||
const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||
@ -90,6 +91,7 @@ interface IAccount {
|
||||
withLinkToProfile?: boolean;
|
||||
withRelationship?: boolean;
|
||||
showEdit?: boolean;
|
||||
showMultiLanguage?: boolean;
|
||||
approvalStatus?: StatusApprovalStatus;
|
||||
emoji?: string;
|
||||
emojiUrl?: string;
|
||||
@ -116,6 +118,7 @@ const Account = ({
|
||||
withLinkToProfile = true,
|
||||
withRelationship = true,
|
||||
showEdit = false,
|
||||
showMultiLanguage = false,
|
||||
approvalStatus,
|
||||
emoji,
|
||||
emojiUrl,
|
||||
@ -268,6 +271,16 @@ const Account = ({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{showMultiLanguage ? (
|
||||
<>
|
||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
||||
<button title={intl.formatMessage(messages.languageVersions)}>
|
||||
<Icon className='h-5 w-5 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/language.svg')} />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{actionType === 'muting' && account.mute_expires_at ? (
|
||||
<>
|
||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
||||
@ -428,6 +428,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||
action={accountAction}
|
||||
hideActions={!accountAction}
|
||||
showEdit={!!actualStatus.edited_at}
|
||||
showMultiLanguage={!!actualStatus.content_map && actualStatus.content_map?.count() > 1}
|
||||
showProfileHoverCard={hoverable}
|
||||
withLinkToProfile={hoverable}
|
||||
approvalStatus={actualStatus.approval_status}
|
||||
|
||||
@ -7,10 +7,10 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { changeComposeLanguage } from 'soapbox/actions/compose';
|
||||
import { addComposeLanguage, changeComposeLanguage, deleteComposeLanguage } 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, useAppSelector, useCompose } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
const getFrequentlyUsedLanguages = createSelector([
|
||||
state => state.settings.get('frequentlyUsedLanguages', ImmutableMap()),
|
||||
@ -28,7 +28,10 @@ const languages = Object.entries(languagesObject) as Array<[Language, string]>;
|
||||
const messages = defineMessages({
|
||||
languagePrompt: { id: 'compose.language_dropdown.prompt', defaultMessage: 'Select language' },
|
||||
languageSuggestion: { id: 'compose.language_dropdown.suggestion', defaultMessage: '{language} (detected)' },
|
||||
multipleLanguages: { id: 'compose.language_dropdown.more_languages', defaultMessage: '{count, plural, one {# more language} other {# more languages}}' },
|
||||
search: { id: 'compose.language_dropdown.search', defaultMessage: 'Search language…' },
|
||||
addLanguage: { id: 'compose.language_dropdown.add_language', defaultMessage: 'Add language' },
|
||||
deleteLanguage: { id: 'compose.language_dropdown.delete_language', defaultMessage: 'DElete language' },
|
||||
});
|
||||
|
||||
interface ILanguageDropdown {
|
||||
@ -37,6 +40,7 @@ interface ILanguageDropdown {
|
||||
|
||||
const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const features = useFeatures();
|
||||
const dispatch = useAppDispatch();
|
||||
const frequentlyUsedLanguages = useAppSelector(getFrequentlyUsedLanguages);
|
||||
|
||||
@ -62,7 +66,7 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
],
|
||||
});
|
||||
|
||||
const { language, suggested_language: suggestedLanguage } = useCompose(composeId);
|
||||
const { language, suggested_language: suggestedLanguage, textMap } = useCompose(composeId);
|
||||
|
||||
const handleClick: React.EventHandler<
|
||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
@ -127,6 +131,26 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
handleChange(value);
|
||||
};
|
||||
|
||||
const handleAddLanguageClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
|
||||
const value = (e.currentTarget as HTMLElement)?.parentElement?.getAttribute('data-index') as Language;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setIsOpen(false);
|
||||
dispatch(addComposeLanguage(composeId, value));
|
||||
};
|
||||
|
||||
const handleDeleteLanguageClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
|
||||
const value = (e.currentTarget as HTMLElement)?.parentElement?.getAttribute('data-index') as Language;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setIsOpen(false);
|
||||
dispatch(deleteComposeLanguage(composeId, value));
|
||||
};
|
||||
|
||||
const handleClear: React.MouseEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -139,6 +163,14 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
return [...languages].sort((a, b) => {
|
||||
// Push current selection to the top of the list
|
||||
|
||||
if (textMap.has(a[0])) {
|
||||
if (b[0] === language) return 1;
|
||||
return -1;
|
||||
}
|
||||
if (textMap.has(b[0])) {
|
||||
if (a[0] === language) return -1;
|
||||
return 1;
|
||||
}
|
||||
if (a[0] === language) {
|
||||
return -1;
|
||||
} else if (b[0] === language) {
|
||||
@ -255,8 +287,13 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
const results = search();
|
||||
|
||||
let buttonLabel = intl.formatMessage(messages.languagePrompt);
|
||||
if (language) buttonLabel = languagesObject[language];
|
||||
else if (suggestedLanguage) buttonLabel = intl.formatMessage(messages.languageSuggestion, {
|
||||
if (language) {
|
||||
const list: string[] = [languagesObject[language]];
|
||||
if (textMap.size) list.push(intl.formatMessage(messages.multipleLanguages, {
|
||||
count: textMap.size,
|
||||
}));
|
||||
buttonLabel = intl.formatList(list);
|
||||
} else if (suggestedLanguage) buttonLabel = intl.formatMessage(messages.languageSuggestion, {
|
||||
language: languagesObject[suggestedLanguage as Language] || suggestedLanguage,
|
||||
});
|
||||
|
||||
@ -320,19 +357,30 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
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',
|
||||
'flex cursor-pointer gap-2 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', {
|
||||
className={clsx('flex-auto grow text-primary-600 dark:text-primary-400', {
|
||||
'text-black dark:text-white': active,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
{features.multiLanguage && !!language && !active && (
|
||||
textMap.has(code) ? (
|
||||
<button title={intl.formatMessage(messages.deleteLanguage)} onClick={handleDeleteLanguageClick}>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/outline/minus.svg')} />
|
||||
</button>
|
||||
) : (
|
||||
<button title={intl.formatMessage(messages.addLanguage)} onClick={handleAddLanguageClick}>
|
||||
<Icon className='h-4 w-4' src={require('@tabler/icons/outline/plus.svg')} />
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -50,6 +50,7 @@ const StatusRecord = ImmutableRecord({
|
||||
bookmarked: false,
|
||||
card: null as Card | null,
|
||||
content: '',
|
||||
content_map: null as ImmutableMap<string, string> | null,
|
||||
created_at: '',
|
||||
dislikes_count: 0,
|
||||
disliked: false,
|
||||
|
||||
@ -57,6 +57,8 @@ import {
|
||||
COMPOSE_ADD_SUGGESTED_QUOTE,
|
||||
ComposeAction,
|
||||
COMPOSE_ADD_SUGGESTED_LANGUAGE,
|
||||
COMPOSE_LANGUAGE_ADD,
|
||||
COMPOSE_LANGUAGE_DELETE,
|
||||
} 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';
|
||||
@ -111,6 +113,7 @@ const ReducerCompose = ImmutableRecord({
|
||||
suggestion_token: null as string | null,
|
||||
tagHistory: ImmutableList<string>(),
|
||||
text: '',
|
||||
textMap: ImmutableMap<Language, string>(),
|
||||
to: ImmutableOrderedSet<string>(),
|
||||
parent_reblogged_by: null as string | null,
|
||||
dismissed_quotes: ImmutableOrderedSet<string>(),
|
||||
@ -553,11 +556,13 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
||||
return list.splice(indexA, 1).splice(indexB, 0, moveItem);
|
||||
}));
|
||||
case COMPOSE_ADD_SUGGESTED_QUOTE:
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('quote', action.quoteId));
|
||||
return updateCompose(state, action.id, compose => compose.set('quote', action.quoteId));
|
||||
case COMPOSE_ADD_SUGGESTED_LANGUAGE:
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.set('suggested_language', action.language));
|
||||
return updateCompose(state, action.id, compose => compose.set('suggested_language', action.language));
|
||||
case COMPOSE_LANGUAGE_ADD:
|
||||
return updateCompose(state, action.id, compose => compose.setIn(['textMap', action.value], ''));
|
||||
case COMPOSE_LANGUAGE_DELETE:
|
||||
return updateCompose(state, action.id, compose => compose.removeIn(['textMap', action.value]));
|
||||
case COMPOSE_QUOTE_CANCEL:
|
||||
return updateCompose(state, action.id, compose => compose
|
||||
.update('dismissed_quotes', quotes => compose.quote ? quotes.add(compose.quote) : quotes)
|
||||
|
||||
@ -622,6 +622,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||
// v.software === PLEROMA && gte(v.version, '2.1.0'),
|
||||
]),
|
||||
|
||||
/**
|
||||
* Ability to include multiple language variants for a post.
|
||||
* @see POST /api/v1/statuses
|
||||
*/
|
||||
multiLanguage: false, // features.includes('pleroma:multi_language'),
|
||||
|
||||
/**
|
||||
* Ability to hide notifications from people you don't follow.
|
||||
* @see PUT /api/pleroma/notification_settings
|
||||
|
||||
Reference in New Issue
Block a user