Handle sensitive and spoiler separately when writing a post

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-08-24 00:45:45 +02:00
parent d33589d103
commit 604ebdd24a
9 changed files with 38 additions and 102 deletions

View file

@ -188,7 +188,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
if (spoilerText) { if (spoilerText) {
output.push( output.push(
<Text className='mb-2' size='2xl' weight='medium'> <Text size='2xl' weight='medium'>
<span dangerouslySetInnerHTML={{ __html: spoilerText }} /> <span dangerouslySetInnerHTML={{ __html: spoilerText }} />
{expandable && ( {expandable && (
<Button <Button

View file

@ -92,7 +92,6 @@ const ChatMessage = (props: IChatMessage) => {
})} })}
media={[chatMessage.attachment]} media={[chatMessage.attachment]}
onOpenMedia={onOpenMedia} onOpenMedia={onOpenMedia}
visible
/> />
); );
}; };

View file

@ -12,11 +12,10 @@ import {
selectComposeSuggestion, selectComposeSuggestion,
uploadCompose, uploadCompose,
} from 'soapbox/actions/compose'; } from 'soapbox/actions/compose';
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
import { Button, HStack, Stack } from 'soapbox/components/ui'; import { Button, HStack, Stack } from 'soapbox/components/ui';
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container'; import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
import { ComposeEditor } from 'soapbox/features/ui/util/async-components'; import { ComposeEditor } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector, useCompose, useDraggedFiles, useFeatures, useInstance, usePrevious } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useCompose, useDraggedFiles, useFeatures, useInstance } from 'soapbox/hooks';
import QuotedStatusContainer from '../containers/quoted-status-container'; import QuotedStatusContainer from '../containers/quoted-status-container';
import ReplyIndicatorContainer from '../containers/reply-indicator-container'; import ReplyIndicatorContainer from '../containers/reply-indicator-container';
@ -41,6 +40,7 @@ import UploadForm from './upload-form';
import VisualCharacterCounter from './visual-character-counter'; import VisualCharacterCounter from './visual-character-counter';
import Warning from './warning'; import Warning from './warning';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
import type { Emoji } from 'soapbox/features/emoji'; import type { Emoji } from 'soapbox/features/emoji';
const messages = defineMessages({ const messages = defineMessages({
@ -77,7 +77,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const features = useFeatures(); const features = useFeatures();
const { const {
spoiler,
spoiler_text: spoilerText, spoiler_text: spoilerText,
privacy, privacy,
is_submitting: isSubmitting, is_submitting: isSubmitting,
@ -90,17 +89,13 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
modified_language: modifiedLanguage, modified_language: modifiedLanguage,
} = compose; } = compose;
const prevSpoiler = usePrevious(spoiler);
const hasPoll = !!compose.poll; const hasPoll = !!compose.poll;
const isEditing = compose.id !== null; const isEditing = compose.id !== null;
const anyMedia = compose.media_attachments.size > 0; const anyMedia = compose.media_attachments.size > 0;
const [composeFocused, setComposeFocused] = useState(false); const [composeFocused, setComposeFocused] = useState(false);
const firstRender = useRef(true);
const formRef = useRef<HTMLDivElement>(null); const formRef = useRef<HTMLDivElement>(null);
const spoilerTextRef = useRef<AutosuggestInput>(null);
const editorRef = useRef<LexicalEditor>(null); const editorRef = useRef<LexicalEditor>(null);
const { isDraggedOver } = useDraggedFiles(formRef); const { isDraggedOver } = useDraggedFiles(formRef);
@ -171,10 +166,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
dispatch(uploadCompose(id, files, intl)); dispatch(uploadCompose(id, files, intl));
}; };
const focusSpoilerInput = () => {
spoilerTextRef.current?.input?.focus();
};
useEffect(() => { useEffect(() => {
document.addEventListener('click', handleClick, true); document.addEventListener('click', handleClick, true);
@ -183,16 +174,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
}; };
}, []); }, []);
useEffect(() => {
if (spoiler && firstRender.current) {
firstRender.current = false;
} else if (!spoiler && prevSpoiler) {
//
} else if (spoiler && !prevSpoiler) {
focusSpoilerInput();
}
}, [spoiler]);
const renderButtons = useCallback(() => ( const renderButtons = useCallback(() => (
<HStack alignItems='center' space={2}> <HStack alignItems='center' space={2}>
<UploadButtonContainer composeId={id} /> <UploadButtonContainer composeId={id} />
@ -208,14 +189,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
<UploadForm composeId={id} onSubmit={handleSubmit} /> <UploadForm composeId={id} onSubmit={handleSubmit} />
<PollForm composeId={id} /> <PollForm composeId={id} />
<SpoilerInput
composeId={id}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSpoilerSuggestionSelected}
ref={spoilerTextRef}
/>
<ScheduleForm composeId={id} /> <ScheduleForm composeId={id} />
</Stack> </Stack>
); );
@ -280,6 +253,13 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
</HStack> </HStack>
)} )}
<SpoilerInput
composeId={id}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSpoilerSuggestionSelected}
/>
<div> <div>
<Suspense> <Suspense>
<ComposeEditor <ComposeEditor

View file

@ -7,8 +7,8 @@ import { useAppDispatch, useCompose } from 'soapbox/hooks';
import ComposeFormButton from './compose-form-button'; import ComposeFormButton from './compose-form-button';
const messages = defineMessages({ const messages = defineMessages({
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' }, marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Media is marked as sensitive' },
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' }, unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Media is not marked as sensitive' },
}); });
interface ISpoilerButton { interface ISpoilerButton {
@ -19,7 +19,7 @@ const SpoilerButton: React.FC<ISpoilerButton> = ({ composeId }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const active = useCompose(composeId).spoiler; const active = useCompose(composeId).sensitive;
const onClick = () => const onClick = () =>
dispatch(changeComposeSpoilerness(composeId)); dispatch(changeComposeSpoilerness(composeId));

View file

@ -1,16 +1,13 @@
import clsx from 'clsx';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { changeComposeSpoilerness, changeComposeSpoilerText } from 'soapbox/actions/compose'; import { changeComposeSpoilerText } from 'soapbox/actions/compose';
import AutosuggestInput, { IAutosuggestInput } from 'soapbox/components/autosuggest-input'; import AutosuggestInput, { IAutosuggestInput } from 'soapbox/components/autosuggest-input';
import { Divider, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useCompose } from 'soapbox/hooks'; import { useAppDispatch, useCompose } from 'soapbox/hooks';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'compose_form.spoiler_title', defaultMessage: 'Sensitive content' }, placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Subject (optional)' },
placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' },
remove: { id: 'compose_form.spoiler_remove', defaultMessage: 'Remove sensitive' },
}); });
interface ISpoilerInput extends Pick<IAutosuggestInput, 'onSuggestionsFetchRequested' | 'onSuggestionsClearRequested' | 'onSuggestionSelected'> { interface ISpoilerInput extends Pick<IAutosuggestInput, 'onSuggestionsFetchRequested' | 'onSuggestionsClearRequested' | 'onSuggestionSelected'> {
@ -26,56 +23,28 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
}, ref) => { }, ref) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { language, modified_language, spoiler, spoiler_text: spoilerText, spoilerTextMap, suggestions } = useCompose(composeId); const { language, modified_language, spoiler_text: spoilerText, spoilerTextMap, suggestions } = useCompose(composeId);
const handleChangeSpoilerText: React.ChangeEventHandler<HTMLInputElement> = (e) => { const handleChangeSpoilerText: React.ChangeEventHandler<HTMLInputElement> = (e) => {
dispatch(changeComposeSpoilerText(composeId, e.target.value)); dispatch(changeComposeSpoilerText(composeId, e.target.value));
}; };
const handleRemove = () => {
dispatch(changeComposeSpoilerness(composeId));
};
const value = !modified_language || modified_language === language ? spoilerText : spoilerTextMap.get(modified_language, ''); const value = !modified_language || modified_language === language ? spoilerText : spoilerTextMap.get(modified_language, '');
return ( return (
<Stack <AutosuggestInput
space={4} placeholder={intl.formatMessage(messages.placeholder)}
className={clsx({ value={value}
'relative transition-height': true, onChange={handleChangeSpoilerText}
'hidden': !spoiler, suggestions={suggestions}
})} onSuggestionsFetchRequested={onSuggestionsFetchRequested}
> onSuggestionsClearRequested={onSuggestionsClearRequested}
<Divider /> onSuggestionSelected={onSuggestionSelected}
searchTokens={[':']}
<Stack space={2}> id='cw-spoiler-input'
<Text weight='medium'> className='rounded-md !bg-transparent dark:!bg-transparent'
{intl.formatMessage(messages.title)} ref={ref}
</Text> />
<AutosuggestInput
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={handleChangeSpoilerText}
disabled={!spoiler}
suggestions={suggestions}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
className='rounded-md !bg-transparent dark:!bg-transparent'
ref={ref}
autoFocus
/>
<div className='text-center'>
<button type='button' className='text-danger-500' onClick={handleRemove}>
{intl.formatMessage(messages.remove)}
</button>
</div>
</Stack>
</Stack>
); );
}); });

View file

@ -63,7 +63,6 @@ const StatusCheckBox: React.FC<IStatusCheckBox> = ({ id, disabled }) => {
media={status.media_attachments} media={status.media_attachments}
height={110} height={110}
onOpenMedia={noop} onOpenMedia={noop}
visible
/> />
); );
} }

View file

@ -485,11 +485,9 @@
"compose_form.schedule": "Schedule", "compose_form.schedule": "Schedule",
"compose_form.scheduled_statuses.click_here": "Click here", "compose_form.scheduled_statuses.click_here": "Click here",
"compose_form.scheduled_statuses.message": "You have scheduled posts. {click_here} to see them.", "compose_form.scheduled_statuses.message": "You have scheduled posts. {click_here} to see them.",
"compose_form.spoiler.marked": "Text is hidden behind warning", "compose_form.spoiler.marked": "Media is marked as sensitive",
"compose_form.spoiler.unmarked": "Text is not hidden", "compose_form.spoiler.unmarked": "Media is not marked as sensitive",
"compose_form.spoiler_placeholder": "Write your warning here (optional)", "compose_form.spoiler_placeholder": "Subject (optional)",
"compose_form.spoiler_remove": "Remove sensitive",
"compose_form.spoiler_title": "Sensitive content",
"confirmation_modal.cancel": "Cancel", "confirmation_modal.cancel": "Cancel",
"confirmations.admin.deactivate_user.confirm": "Deactivate @{name}", "confirmations.admin.deactivate_user.confirm": "Deactivate @{name}",
"confirmations.admin.deactivate_user.heading": "Deactivate @{acct}", "confirmations.admin.deactivate_user.heading": "Deactivate @{acct}",

View file

@ -485,11 +485,9 @@
"compose_form.schedule": "Zaplanuj", "compose_form.schedule": "Zaplanuj",
"compose_form.scheduled_statuses.click_here": "Naciśnij tutaj", "compose_form.scheduled_statuses.click_here": "Naciśnij tutaj",
"compose_form.scheduled_statuses.message": "Masz zaplanowane wpisy. {click_here}, aby je zobaczyć.", "compose_form.scheduled_statuses.message": "Masz zaplanowane wpisy. {click_here}, aby je zobaczyć.",
"compose_form.spoiler.marked": "Tekst jest ukryty za ostrzeżeniem", "compose_form.spoiler.marked": "Media są oznaczone jako wrażliwe",
"compose_form.spoiler.unmarked": "Tekst nie jest ukryty", "compose_form.spoiler.unmarked": "Media nie są oznaczone jako wrażliwe",
"compose_form.spoiler_placeholder": "Wprowadź swoje ostrzeżenie o zawartości", "compose_form.spoiler_placeholder": "Temat (nieobowiązkowy)",
"compose_form.spoiler_remove": "Usuń zaznaczenie jako wrażliwe",
"compose_form.spoiler_title": "Treści wrażliwe",
"confirmation_modal.cancel": "Anuluj", "confirmation_modal.cancel": "Anuluj",
"confirmations.admin.deactivate_user.confirm": "Dezaktywuj @{name}", "confirmations.admin.deactivate_user.confirm": "Dezaktywuj @{name}",
"confirmations.admin.deactivate_user.heading": "Dezaktywuj @{acct}", "confirmations.admin.deactivate_user.heading": "Dezaktywuj @{acct}",

View file

@ -105,7 +105,6 @@ const ReducerCompose = ImmutableRecord({
resetFileKey: null as number | null, resetFileKey: null as number | null,
schedule: null as Date | null, schedule: null as Date | null,
sensitive: false, sensitive: false,
spoiler: false,
spoiler_text: '', spoiler_text: '',
spoilerTextMap: ImmutableMap<Language, string>(), spoilerTextMap: ImmutableMap<Language, string>(),
suggestions: ImmutableList<string>(), suggestions: ImmutableList<string>(),
@ -165,7 +164,7 @@ const appendMedia = (compose: Compose, media: MediaAttachment, defaultSensitive?
map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
if (prevSize === 0 && (defaultSensitive || compose.spoiler)) { if (prevSize === 0 && (defaultSensitive || compose.sensitive)) {
map.set('sensitive', true); map.set('sensitive', true);
} }
}); });
@ -293,9 +292,7 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
})); }));
case COMPOSE_SPOILERNESS_CHANGE: case COMPOSE_SPOILERNESS_CHANGE:
return updateCompose(state, action.composeId, compose => compose.withMutations(map => { return updateCompose(state, action.composeId, compose => compose.withMutations(map => {
map.set('spoiler_text', ''); map.set('sensitive', !compose.sensitive);
map.set('spoiler', !compose.spoiler);
map.set('sensitive', !compose.spoiler);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
})); }));
case COMPOSE_SPOILER_TEXT_CHANGE: case COMPOSE_SPOILER_TEXT_CHANGE:
@ -342,7 +339,6 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('content_type', defaultCompose.content_type); map.set('content_type', defaultCompose.content_type);
if (action.preserveSpoilers && action.status.spoiler_text) { if (action.preserveSpoilers && action.status.spoiler_text) {
map.set('spoiler', true);
map.set('sensitive', true); map.set('sensitive', true);
map.set('spoiler_text', action.status.spoiler_text); map.set('spoiler_text', action.status.spoiler_text);
} }
@ -367,7 +363,6 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
map.set('caretPosition', null); map.set('caretPosition', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('content_type', defaultCompose.content_type); map.set('content_type', defaultCompose.content_type);
map.set('spoiler', false);
map.set('spoiler_text', ''); map.set('spoiler_text', '');
if (action.status.visibility === 'group') { if (action.status.visibility === 'group') {
@ -484,10 +479,8 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
} }
if (action.status.spoiler_text.length > 0) { if (action.status.spoiler_text.length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.spoiler_text); map.set('spoiler_text', action.status.spoiler_text);
} else { } else {
map.set('spoiler', false);
map.set('spoiler_text', ''); map.set('spoiler_text', '');
} }