Handle sensitive and spoiler separately when writing a post
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
d33589d103
commit
604ebdd24a
9 changed files with 38 additions and 102 deletions
|
@ -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
|
||||||
|
|
|
@ -92,7 +92,6 @@ const ChatMessage = (props: IChatMessage) => {
|
||||||
})}
|
})}
|
||||||
media={[chatMessage.attachment]}
|
media={[chatMessage.attachment]}
|
||||||
onOpenMedia={onOpenMedia}
|
onOpenMedia={onOpenMedia}
|
||||||
visible
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}",
|
||||||
|
|
|
@ -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}",
|
||||||
|
|
|
@ -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', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue