pl-fe: Show suggestions about hashtag accessibility
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -101,6 +101,9 @@ const COMPOSE_INTERACTION_POLICY_OPTION_CHANGE = 'COMPOSE_INTERACTION_POLICY_OPT
|
||||
const COMPOSE_CLEAR_LINK_SUGGESTION_CREATE = 'COMPOSE_CLEAR_LINK_SUGGESTION_CREATE' as const;
|
||||
const COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE = 'COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE' as const;
|
||||
|
||||
const COMPOSE_HASHTAG_CASING_SUGGESTION_SET = 'COMPOSE_HASHTAG_CASING_SUGGESTION_SET' as const;
|
||||
const COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE = 'COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE' as const;
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -984,6 +987,17 @@ const ignoreClearLinkSuggestion = (composeId: string, key: string) => ({
|
||||
key,
|
||||
});
|
||||
|
||||
const suggestHashtagCasing = (composeId: string, suggestion: string | null) => ({
|
||||
type: COMPOSE_HASHTAG_CASING_SUGGESTION_SET,
|
||||
composeId,
|
||||
suggestion,
|
||||
});
|
||||
|
||||
const ignoreHashtagCasingSuggestion = (composeId: string) => ({
|
||||
type: COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE,
|
||||
composeId,
|
||||
});
|
||||
|
||||
type ComposeAction =
|
||||
ComposeSetStatusAction
|
||||
| ReturnType<typeof changeCompose>
|
||||
@ -1040,7 +1054,9 @@ type ComposeAction =
|
||||
| ReturnType<typeof changeComposeFederated>
|
||||
| ReturnType<typeof changeComposeInteractionPolicyOption>
|
||||
| ReturnType<typeof suggestClearLink>
|
||||
| ReturnType<typeof ignoreClearLinkSuggestion>;
|
||||
| ReturnType<typeof ignoreClearLinkSuggestion>
|
||||
| ReturnType<typeof suggestHashtagCasing>
|
||||
| ReturnType<typeof ignoreHashtagCasingSuggestion>;
|
||||
|
||||
export {
|
||||
COMPOSE_CHANGE,
|
||||
@ -1099,6 +1115,8 @@ export {
|
||||
COMPOSE_INTERACTION_POLICY_OPTION_CHANGE,
|
||||
COMPOSE_CLEAR_LINK_SUGGESTION_CREATE,
|
||||
COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_SET,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE,
|
||||
setComposeToStatus,
|
||||
replyCompose,
|
||||
cancelReplyCompose,
|
||||
@ -1149,6 +1167,8 @@ export {
|
||||
suggestClearLink,
|
||||
ignoreClearLinkSuggestion,
|
||||
cancelPreviewCompose,
|
||||
suggestHashtagCasing,
|
||||
ignoreHashtagCasingSuggestion,
|
||||
type ComposeReplyAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
type ComposeAction,
|
||||
|
||||
@ -37,6 +37,7 @@ import { countableText } from '../util/counter';
|
||||
|
||||
import ClearLinkSuggestion from './clear-link-suggestion';
|
||||
import ContentTypeButton from './content-type-button';
|
||||
import HashtagCasingSuggestion from './hashtag-casing-suggestion';
|
||||
import InteractionPolicyButton from './interaction-policy-button';
|
||||
import LanguageDropdown from './language-dropdown';
|
||||
import PollButton from './poll-button';
|
||||
@ -380,6 +381,8 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
|
||||
<ClearLinkSuggestion composeId={id} handleAccept={onAcceptClearLinkSuggestion} handleReject={onRejectClearLinkSuggestion} />
|
||||
|
||||
<HashtagCasingSuggestion composeId={id} />
|
||||
|
||||
{composeModifiers}
|
||||
|
||||
<QuotedStatusContainer composeId={id} />
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import { ignoreHashtagCasingSuggestion } from 'pl-fe/actions/compose';
|
||||
import { changeSetting } from 'pl-fe/actions/settings';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import OptionalMotion from 'pl-fe/features/ui/util/optional-motion';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useCompose } from 'pl-fe/hooks/use-compose';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
hashtagCasingSuggestionsDisabled: { id: 'compose.hashtag_casing_suggestion.disabled', defaultMessage: 'You will no longer receive suggestions about hashtag capitalization.' },
|
||||
});
|
||||
|
||||
interface IHashtagCasingSuggestion {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const HashtagCasingSuggestion = ({
|
||||
composeId,
|
||||
}: IHashtagCasingSuggestion) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const compose = useCompose(composeId);
|
||||
const suggestion = compose.hashtag_casing_suggestion;
|
||||
|
||||
const onIgnore = () => {
|
||||
dispatch(ignoreHashtagCasingSuggestion(composeId));
|
||||
};
|
||||
|
||||
const onDontAskAgain = () => {
|
||||
dispatch(changeSetting(['ignoreHashtagCasingSuggestions'], true, { showAlert: false, save: true }));
|
||||
toast.info(messages.hashtagCasingSuggestionsDisabled);
|
||||
dispatch(ignoreHashtagCasingSuggestion(composeId));
|
||||
};
|
||||
|
||||
if (!suggestion) return null;
|
||||
|
||||
return (
|
||||
<OptionalMotion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<Stack space={1} className='rounded border border-solid border-gray-400 bg-transparent px-2.5 py-2 text-xs text-gray-900 dark:border-gray-800 dark:text-white' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='compose.hashtag_casing_suggestion.body'
|
||||
defaultMessage='Does the hashtag {hashtag} include more than one word? Prefer capitalizing the first letter of each word for improved readability and accessibility.'
|
||||
values={{ hashtag: <span className='font-medium'>{suggestion}</span> }}
|
||||
/>
|
||||
</span>
|
||||
<HStack space={2} justifyContent='end'>
|
||||
<Button
|
||||
theme='muted'
|
||||
size='xs'
|
||||
onClick={onIgnore}
|
||||
>
|
||||
<FormattedMessage id='compose.hashtag_casing_suggestion.ignore' defaultMessage='Ignore' />
|
||||
</Button>
|
||||
<Button
|
||||
theme='muted'
|
||||
size='xs'
|
||||
onClick={onDontAskAgain}
|
||||
>
|
||||
<FormattedMessage id='compose.hashtag_casing_suggestion.dont_ask_again' defaultMessage='Don’t ask again' />
|
||||
</Button>
|
||||
</HStack>
|
||||
</Stack>
|
||||
)}
|
||||
</OptionalMotion>
|
||||
);
|
||||
};
|
||||
|
||||
export { HashtagCasingSuggestion as default };
|
||||
@ -1,3 +1,4 @@
|
||||
import { HashtagNode } from '@lexical/hashtag';
|
||||
import { AutoLinkNode, LinkNode } from '@lexical/link';
|
||||
import { $convertToMarkdownString } from '@lexical/markdown';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
@ -6,7 +7,7 @@ import debounce from 'lodash/debounce';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { addSuggestedLanguage, addSuggestedQuote, setEditorState, suggestClearLink } from 'pl-fe/actions/compose';
|
||||
import { addSuggestedLanguage, addSuggestedQuote, setEditorState, suggestClearLink, suggestHashtagCasing } from 'pl-fe/actions/compose';
|
||||
import { fetchStatus } from 'pl-fe/actions/statuses';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
@ -30,7 +31,7 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId, isWysiwyg }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const features = useFeatures();
|
||||
const { urlPrivacy } = useSettings();
|
||||
const { urlPrivacy, ignoreHashtagCasingSuggestions } = useSettings();
|
||||
|
||||
const checkUrls = useCallback(debounce((editorState: EditorState) => {
|
||||
dispatch(async (_, getState) => {
|
||||
@ -79,6 +80,32 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId, isWysiwyg }) => {
|
||||
});
|
||||
}, 2000), [urlPrivacy.clearLinksInCompose]);
|
||||
|
||||
const checkHashtagCasingSuggestions = useCallback(debounce((editorState: EditorState) => {
|
||||
dispatch(async (_, getState) => {
|
||||
if (ignoreHashtagCasingSuggestions) return;
|
||||
|
||||
const state = getState();
|
||||
const compose = state.compose[composeId];
|
||||
|
||||
if (compose.hashtag_casing_suggestion_ignored) return;
|
||||
|
||||
editorState.read(() => {
|
||||
const hashtagNodes = $nodesOfType(HashtagNode);
|
||||
|
||||
for (const tag of hashtagNodes) {
|
||||
const text = tag.getTextContent();
|
||||
|
||||
if (text.length > 10 && text.toLowerCase() === text && !text.match(/[0-9]/)) {
|
||||
dispatch(suggestHashtagCasing(composeId, text));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(suggestHashtagCasing(composeId, null));
|
||||
});
|
||||
});
|
||||
}, 1000), [ignoreHashtagCasingSuggestions]);
|
||||
|
||||
const getQuoteSuggestions = useCallback(debounce((text: string) => {
|
||||
dispatch(async (_, getState) => {
|
||||
const state = getState();
|
||||
@ -147,6 +174,7 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId, isWysiwyg }) => {
|
||||
const data = isEmpty ? null : JSON.stringify(editorState.toJSON());
|
||||
dispatch(setEditorState(composeId, data, text));
|
||||
checkUrls(editorState);
|
||||
checkHashtagCasingSuggestions(editorState);
|
||||
getQuoteSuggestions(plainText);
|
||||
detectLanguage(plainText);
|
||||
});
|
||||
|
||||
@ -463,6 +463,10 @@
|
||||
"compose.clear_link_suggestion.ignore": "Ignore",
|
||||
"compose.clear_link_suggestion.remove": "Remove",
|
||||
"compose.edit_success": "Your post was edited",
|
||||
"compose.hashtag_casing_suggestion.body": "Does the hashtag {hashtag} include more than one word? Prefer capitalizing the first letter of each word for improved readability and accessibility.",
|
||||
"compose.hashtag_casing_suggestion.disabled": "You will no longer receive suggestions about hashtag capitalization.",
|
||||
"compose.hashtag_casing_suggestion.dont_ask_again": "Don’t ask again",
|
||||
"compose.hashtag_casing_suggestion.ignore": "Ignore",
|
||||
"compose.invalid_schedule": "You must schedule a post at least 5 minutes out.",
|
||||
"compose.language_dropdown.add_language": "Add language",
|
||||
"compose.language_dropdown.delete_language": "Delete language",
|
||||
|
||||
@ -60,6 +60,8 @@ import {
|
||||
COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE,
|
||||
COMPOSE_PREVIEW_SUCCESS,
|
||||
COMPOSE_PREVIEW_CANCEL,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_SET,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE,
|
||||
type ComposeAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
} from '../actions/compose';
|
||||
@ -142,6 +144,8 @@ interface Compose {
|
||||
dismissed_clear_links_suggestions: Array<string>;
|
||||
clear_link_suggestion: ClearLinkSuggestion | null;
|
||||
preview: Partial<BaseStatus> | null;
|
||||
hashtag_casing_suggestion: string | null;
|
||||
hashtag_casing_suggestion_ignored: boolean | null;
|
||||
}
|
||||
|
||||
const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
@ -186,6 +190,8 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
dismissed_clear_links_suggestions: [],
|
||||
clear_link_suggestion: null,
|
||||
preview: null,
|
||||
hashtag_casing_suggestion: null,
|
||||
hashtag_casing_suggestion_ignored: null,
|
||||
...params,
|
||||
});
|
||||
|
||||
@ -733,6 +739,15 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.preview = null;
|
||||
});
|
||||
case COMPOSE_HASHTAG_CASING_SUGGESTION_SET:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.hashtag_casing_suggestion = action.suggestion;
|
||||
});
|
||||
case COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.hashtag_casing_suggestion = null;
|
||||
compose.hashtag_casing_suggestion_ignored = true;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ const settingsSchema = v.object({
|
||||
boostModal: v.fallback(v.boolean(), false),
|
||||
deleteModal: v.fallback(v.boolean(), true),
|
||||
missingDescriptionModal: v.fallback(v.boolean(), true),
|
||||
ignoreHashtagCasingSuggestions: v.fallback(v.boolean(), false),
|
||||
defaultPrivacy: v.fallback(v.picklist(['public', 'unlisted', 'private', 'direct']), 'public'),
|
||||
defaultContentType: v.fallback(v.picklist(['text/plain', 'text/markdown', 'text/html', 'wysiwyg']), 'text/plain'),
|
||||
themeMode: v.fallback(v.picklist(['system', 'light', 'dark', 'black']), 'system'),
|
||||
|
||||
Reference in New Issue
Block a user