Lexical: Use in ComposeEventModal, style improvements, types

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-04-11 23:22:34 +02:00
parent 2549e72843
commit b15640603c
17 changed files with 100 additions and 123 deletions

View file

@ -35,7 +35,6 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
if (!item) return; if (!item) return;
if (onClick) onClick(); if (onClick) onClick();
if (item.to) { if (item.to) {
event.preventDefault(); event.preventDefault();
history.push(item.to); history.push(item.to);

View file

@ -15,7 +15,6 @@ const Portal: React.FC<IPortal> = ({ children }) => {
setIsRendered(true); setIsRendered(true);
}, []); }, []);
if (!isRendered) { if (!isRendered) {
return null; return null;
} }

View file

@ -99,7 +99,6 @@ const findElementPosition = (el: HTMLElement) => {
}; };
}; };
const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => { const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => {
const box = findElementPosition(el); const box = findElementPosition(el);
const boxW = el.offsetWidth; const boxW = el.offsetWidth;

View file

@ -11,7 +11,6 @@ import { CompatRouter } from 'react-router-dom-v5-compat';
// @ts-ignore: it doesn't have types // @ts-ignore: it doesn't have types
import { ScrollContext } from 'react-router-scroll-4'; import { ScrollContext } from 'react-router-scroll-4';
import { loadInstance } from 'soapbox/actions/instance'; import { loadInstance } from 'soapbox/actions/instance';
import { fetchMe } from 'soapbox/actions/me'; import { fetchMe } from 'soapbox/actions/me';
import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox'; import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox';

View file

@ -12,5 +12,4 @@ function parseEntitiesPath(expandedPath: ExpandedEntitiesPath) {
}; };
} }
export { parseEntitiesPath }; export { parseEntitiesPath };

View file

@ -37,7 +37,6 @@ const UserIndex: React.FC = () => {
updateQuery(); updateQuery();
}, []); }, []);
const hasMore = items.count() < total && !!next; const hasMore = items.count() < total && !!next;
const showLoading = isLoading && items.isEmpty(); const showLoading = isLoading && items.isEmpty();

View file

@ -2,7 +2,6 @@ import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { VirtuosoMockContext } from 'react-virtuoso'; import { VirtuosoMockContext } from 'react-virtuoso';
import { ChatContext } from 'soapbox/contexts/chat-context'; import { ChatContext } from 'soapbox/contexts/chat-context';
import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers'; import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers';
import { IAccount } from 'soapbox/queries/accounts'; import { IAccount } from 'soapbox/queries/accounts';

View file

@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, MessageDescriptor, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import { length } from 'stringz'; import { length } from 'stringz';
@ -45,9 +45,6 @@ import type { Emoji } from 'soapbox/features/emoji';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic…' },
eventPlaceholder: { id: 'compose_form.event_placeholder', defaultMessage: 'Post to this event' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' }, spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' },
publish: { id: 'compose_form.publish', defaultMessage: 'Post' }, publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
@ -74,12 +71,11 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const compose = useCompose(id); const compose = useCompose(id);
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden); const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number; const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const features = useFeatures(); const features = useFeatures();
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt, group_id: groupId } = compose; const { text, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt, group_id: groupId } = compose;
const prevSpoiler = usePrevious(spoiler); const prevSpoiler = usePrevious(spoiler);
const hasPoll = !!compose.poll; const hasPoll = !!compose.poll;
@ -93,17 +89,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null); const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null);
const editorStateRef = useRef<string>(null); const editorStateRef = useRef<string>(null);
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
dispatch(changeCompose(id, e.target.value));
};
const handleKeyDown: React.KeyboardEventHandler = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
handleSubmit();
e.preventDefault(); // Prevent bubbling to other ComposeForm instances
}
};
const getClickableArea = () => { const getClickableArea = () => {
return clickableAreaRef ? clickableAreaRef.current : formRef.current; return clickableAreaRef ? clickableAreaRef.current : formRef.current;
}; };
@ -139,11 +124,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const handleSubmit = (e?: React.FormEvent<Element>) => { const handleSubmit = (e?: React.FormEvent<Element>) => {
dispatch(changeCompose(id, editorStateRef.current!)); dispatch(changeCompose(id, editorStateRef.current!));
// if (text !== autosuggestTextareaRef.current?.textarea?.value) {
// // Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// // Update the state to match the current text
// dispatch(changeCompose(id, autosuggestTextareaRef.current!.textarea!.value));
// }
// Submit disabled: // Submit disabled:
const fulltext = [spoilerText, countableText(text)].join(''); const fulltext = [spoilerText, countableText(text)].join('');
@ -167,10 +147,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
dispatch(fetchComposeSuggestions(id, token as string)); dispatch(fetchComposeSuggestions(id, token as string));
}; };
const onSuggestionSelected = (tokenStart: number, token: string | null, value: string | undefined) => {
if (value) dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['text']));
};
const onSpoilerSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => { const onSpoilerSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text'])); dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text']));
}; };
@ -245,7 +221,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
let publishText: string | JSX.Element = ''; let publishText: string | JSX.Element = '';
let publishIcon: string | undefined = undefined; let publishIcon: string | undefined = undefined;
let textareaPlaceholder: MessageDescriptor;
if (isEditing) { if (isEditing) {
publishText = intl.formatMessage(messages.saveChanges); publishText = intl.formatMessage(messages.saveChanges);
@ -263,14 +238,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
publishText = intl.formatMessage(messages.schedule); publishText = intl.formatMessage(messages.schedule);
} }
if (event) {
textareaPlaceholder = messages.eventPlaceholder;
} else if (hasPoll) {
textareaPlaceholder = messages.pollPlaceholder;
} else {
textareaPlaceholder = messages.placeholder;
}
return ( return (
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}> <Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
{scheduledStatusCount > 0 && !event && !group && ( {scheduledStatusCount > 0 && !event && !group && (
@ -301,10 +268,14 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
<div> <div>
<ComposeEditor <ComposeEditor
ref={editorStateRef} ref={editorStateRef}
condensed={condensed} className='my-2'
onFocus={handleComposeFocus}
autoFocus={shouldAutoFocus}
composeId={id} composeId={id}
condensed={condensed}
eventDiscussion={!!event}
autoFocus={shouldAutoFocus}
hasPoll={hasPoll}
onFocus={handleComposeFocus}
onPaste={onPaste}
/> />
{!condensed && ( {!condensed && (
<Stack space={4} className='compose-form__modifiers'> <Stack space={4} className='compose-form__modifiers'>
@ -323,26 +294,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
)} )}
</div> </div>
<AutosuggestTextarea
ref={(isModalOpen && shouldCondense) ? undefined : autosuggestTextareaRef}
placeholder={intl.formatMessage(textareaPlaceholder)}
disabled={disabled}
value={text}
onChange={handleChange}
suggestions={suggestions}
onKeyDown={handleKeyDown}
onFocus={handleComposeFocus}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
condensed={condensed}
id='compose-textarea'
>
<></>
</AutosuggestTextarea>
<QuotedStatusContainer composeId={id} /> <QuotedStatusContainer composeId={id} />
{extra && <div className={clsx({ 'hidden': condensed })}>{extra}</div>} {extra && <div className={clsx({ 'hidden': condensed })}>{extra}</div>}

View file

@ -46,7 +46,29 @@ const StatePlugin = ({ composeId }: { composeId: string }) => {
return null; return null;
}; };
const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onFocus, autoFocus }, editorStateRef) => { interface IComposeEditor {
className?: string
composeId: string
condensed?: boolean
eventDiscussion?: boolean
hasPoll?: boolean
autoFocus?: boolean
onFocus?: React.FocusEventHandler<HTMLDivElement>
onPaste?: (files: FileList) => void
placeholder?: JSX.Element | string
}
const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
className,
composeId,
condensed,
eventDiscussion,
hasPoll,
autoFocus,
onFocus,
onPaste,
placeholder,
}, editorStateRef) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const features = useFeatures(); const features = useFeatures();
@ -111,14 +133,29 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
} }
}; };
const handlePaste: React.ClipboardEventHandler<HTMLDivElement> = (e) => {
if (onPaste && e.clipboardData && e.clipboardData.files.length === 1) {
onPaste(e.clipboardData.files);
e.preventDefault();
}
};
let textareaPlaceholder = placeholder || <FormattedMessage id='compose_form.placeholder' defaultMessage="What's on your mind?" />;
if (eventDiscussion) {
textareaPlaceholder = <FormattedMessage id='compose_form.event_placeholder' defaultMessage='Post to this event' />;
} else if (hasPoll) {
textareaPlaceholder = <FormattedMessage id='compose_form.poll_placeholder' defaultMessage='Add a poll topic…' />;
}
return ( return (
<LexicalComposer initialConfig={initialConfig}> <LexicalComposer initialConfig={initialConfig}>
<div className='lexical relative' data-markup> <div className={clsx('lexical relative', className)} data-markup>
<RichTextPlugin <RichTextPlugin
contentEditable={ contentEditable={
<div className='editor' ref={onRef} onFocus={onFocus}> <div className='editor' ref={onRef} onFocus={onFocus} onPaste={handlePaste}>
<ContentEditable <ContentEditable
className={clsx('mr-4 py-2 outline-none transition-[min-height] motion-reduce:transition-none', { className={clsx('mr-4 outline-none transition-[min-height] motion-reduce:transition-none', {
'min-fh-[40px]': condensed, 'min-fh-[40px]': condensed,
'min-h-[100px]': !condensed, 'min-h-[100px]': !condensed,
})} })}
@ -127,8 +164,10 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
</div> </div>
} }
placeholder={( placeholder={(
<div className='pointer-events-none absolute top-2 select-none text-gray-600 dark:placeholder:text-gray-600'> <div
<FormattedMessage id='compose_form.placeholder' defaultMessage="What's on your mind" /> className='pointer-events-none absolute top-0 select-none text-gray-600 dark:placeholder:text-gray-600'
>
{textareaPlaceholder}
</div> </div>
)} )}
ErrorBoundary={LexicalErrorBoundary} ErrorBoundary={LexicalErrorBoundary}

View file

@ -51,7 +51,7 @@ export type MenuRenderFn = (
anchorElementRef: MutableRefObject<HTMLElement | null>, anchorElementRef: MutableRefObject<HTMLElement | null>,
) => ReactPortal | JSX.Element | null; ) => ReactPortal | JSX.Element | null;
function tryToPositionRange(leadOffset: number, range: Range): boolean { const tryToPositionRange = (leadOffset: number, range: Range): boolean => {
const domSelection = window.getSelection(); const domSelection = window.getSelection();
if (domSelection === null || !domSelection.isCollapsed) { if (domSelection === null || !domSelection.isCollapsed) {
return false; return false;
@ -72,12 +72,12 @@ function tryToPositionRange(leadOffset: number, range: Range): boolean {
} }
return true; return true;
} };
function isSelectionOnEntityBoundary( const isSelectionOnEntityBoundary = (
editor: LexicalEditor, editor: LexicalEditor,
offset: number, offset: number,
): boolean { ): boolean => {
if (offset !== 0) { if (offset !== 0) {
return false; return false;
} }
@ -91,21 +91,21 @@ function isSelectionOnEntityBoundary(
} }
return false; return false;
}); });
} };
function startTransition(callback: () => void) { const startTransition = (callback: () => void) => {
if (React.startTransition) { if (React.startTransition) {
React.startTransition(callback); React.startTransition(callback);
} else { } else {
callback(); callback();
} }
} };
// Got from https://stackoverflow.com/a/42543908/2013580 // Got from https://stackoverflow.com/a/42543908/2013580
export function getScrollParent( export const getScrollParent = (
element: HTMLElement, element: HTMLElement,
includeHidden: boolean, includeHidden: boolean,
): HTMLElement | HTMLBodyElement { ): HTMLElement | HTMLBodyElement => {
let style = getComputedStyle(element); let style = getComputedStyle(element);
const excludeStaticParent = style.position === 'absolute'; const excludeStaticParent = style.position === 'absolute';
const overflowRegex = includeHidden const overflowRegex = includeHidden
@ -130,24 +130,24 @@ export function getScrollParent(
} }
} }
return document.body; return document.body;
} };
function isTriggerVisibleInNearestScrollContainer( const isTriggerVisibleInNearestScrollContainer = (
targetElement: HTMLElement, targetElement: HTMLElement,
containerElement: HTMLElement, containerElement: HTMLElement,
): boolean { ): boolean => {
const tRect = targetElement.getBoundingClientRect(); const tRect = targetElement.getBoundingClientRect();
const cRect = containerElement.getBoundingClientRect(); const cRect = containerElement.getBoundingClientRect();
return tRect.top > cRect.top && tRect.top < cRect.bottom; return tRect.top > cRect.top && tRect.top < cRect.bottom;
} };
// Reposition the menu on scroll, window resize, and element resize. // Reposition the menu on scroll, window resize, and element resize.
export function useDynamicPositioning( export const useDynamicPositioning = (
resolution: Resolution | null, resolution: Resolution | null,
targetElement: HTMLElement | null, targetElement: HTMLElement | null,
onReposition: () => void, onReposition: () => void,
onVisibilityChange?: (isInView: boolean) => void, onVisibilityChange?: (isInView: boolean) => void,
) { ) => {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
useEffect(() => { useEffect(() => {
if (targetElement && resolution) { if (targetElement && resolution) {
@ -161,9 +161,9 @@ export function useDynamicPositioning(
targetElement, targetElement,
rootScrollParent, rootScrollParent,
); );
const handleScroll = function () { const handleScroll = () => {
if (!ticking) { if (!ticking) {
window.requestAnimationFrame(function () { window.requestAnimationFrame(() => {
onReposition(); onReposition();
ticking = false; ticking = false;
}); });
@ -194,22 +194,17 @@ export function useDynamicPositioning(
}; };
} }
}, [targetElement, editor, onVisibilityChange, onReposition, resolution]); }, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
} };
function LexicalPopoverMenu({ const LexicalPopoverMenu = ({ anchorElementRef, menuRenderFn }: {
anchorElementRef,
menuRenderFn,
}: {
anchorElementRef: MutableRefObject<HTMLElement> anchorElementRef: MutableRefObject<HTMLElement>
menuRenderFn: MenuRenderFn menuRenderFn: MenuRenderFn
}): JSX.Element | null { }): JSX.Element | null => menuRenderFn(anchorElementRef);
return menuRenderFn(anchorElementRef);
}
function useMenuAnchorRef( const useMenuAnchorRef = (
resolution: Resolution | null, resolution: Resolution | null,
setResolution: (r: Resolution | null) => void, setResolution: (r: Resolution | null) => void,
): MutableRefObject<HTMLElement> { ): MutableRefObject<HTMLElement> => {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
const anchorElementRef = useRef<HTMLElement>(document.createElement('div')); const anchorElementRef = useRef<HTMLElement>(document.createElement('div'));
const positionMenu = useCallback(() => { const positionMenu = useCallback(() => {
@ -272,7 +267,7 @@ function useMenuAnchorRef(
); );
return anchorElementRef; return anchorElementRef;
} };
export type AutosuggestPluginProps = { export type AutosuggestPluginProps = {
composeId: string composeId: string
@ -280,11 +275,11 @@ export type AutosuggestPluginProps = {
setSuggestionsHidden: (value: boolean) => void setSuggestionsHidden: (value: boolean) => void
}; };
export function AutosuggestPlugin({ export const AutosuggestPlugin = ({
composeId, composeId,
suggestionsHidden, suggestionsHidden,
setSuggestionsHidden, setSuggestionsHidden,
}: AutosuggestPluginProps): JSX.Element | null { }: AutosuggestPluginProps): JSX.Element | null => {
const { suggestions } = useCompose(composeId); const { suggestions } = useCompose(composeId);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -473,4 +468,4 @@ export function AutosuggestPlugin({
} }
/> />
); );
} };

View file

@ -12,7 +12,6 @@ import { useCallback, useEffect } from 'react';
import { $createMentionNode, MentionNode } from '../nodes/mention-node'; import { $createMentionNode, MentionNode } from '../nodes/mention-node';
import type { TextNode } from 'lexical'; import type { TextNode } from 'lexical';
export const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i'); export const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i');

View file

@ -75,5 +75,4 @@ const EmojiPickerDropdownContainer = (
); );
}; };
export default EmojiPickerDropdownContainer; export default EmojiPickerDropdownContainer;

View file

@ -11,7 +11,6 @@ const messages = defineMessages({
const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
interface IIconPickerMenu { interface IIconPickerMenu {
icons: Record<string, Array<string>> icons: Record<string, Array<string>>
onClose: () => void onClose: () => void

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { import {
@ -21,9 +21,10 @@ import { closeModal, openModal } from 'soapbox/actions/modals';
import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location'; import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location';
import LocationSearch from 'soapbox/components/location-search'; import LocationSearch from 'soapbox/components/location-search';
import { checkEventComposeContent } from 'soapbox/components/modal-root'; import { checkEventComposeContent } from 'soapbox/components/modal-root';
import { Button, Form, FormGroup, HStack, Icon, IconButton, Input, Modal, Spinner, Stack, Tabs, Text, Textarea, Toggle } from 'soapbox/components/ui'; import { Button, Form, FormGroup, HStack, Icon, IconButton, Input, Modal, Spinner, Stack, Tabs, Text, Toggle } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container'; import AccountContainer from 'soapbox/containers/account-container';
import { isCurrentOrFutureDate } from 'soapbox/features/compose/components/schedule-form'; import { isCurrentOrFutureDate } from 'soapbox/features/compose/components/schedule-form';
import ComposeEditor from 'soapbox/features/compose/editor';
import BundleContainer from 'soapbox/features/ui/containers/bundle-container'; import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
import { DatePicker } from 'soapbox/features/ui/util/async-components'; import { DatePicker } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
@ -94,13 +95,14 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const editorStateRef = useRef<string>(null);
const [tab, setTab] = useState<'edit' | 'pending'>('edit'); const [tab, setTab] = useState<'edit' | 'pending'>('edit');
const banner = useAppSelector((state) => state.compose_event.banner); const banner = useAppSelector((state) => state.compose_event.banner);
const isUploading = useAppSelector((state) => state.compose_event.is_uploading); const isUploading = useAppSelector((state) => state.compose_event.is_uploading);
const name = useAppSelector((state) => state.compose_event.name); const name = useAppSelector((state) => state.compose_event.name);
const description = useAppSelector((state) => state.compose_event.status);
const startTime = useAppSelector((state) => state.compose_event.start_time); const startTime = useAppSelector((state) => state.compose_event.start_time);
const endTime = useAppSelector((state) => state.compose_event.end_time); const endTime = useAppSelector((state) => state.compose_event.end_time);
const approvalRequired = useAppSelector((state) => state.compose_event.approval_required); const approvalRequired = useAppSelector((state) => state.compose_event.approval_required);
@ -114,10 +116,6 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
dispatch(changeEditEventName(target.value)); dispatch(changeEditEventName(target.value));
}; };
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
dispatch(changeEditEventDescription(target.value));
};
const onChangeStartTime = (date: Date) => { const onChangeStartTime = (date: Date) => {
dispatch(changeEditEventStartTime(date)); dispatch(changeEditEventStartTime(date));
}; };
@ -170,6 +168,7 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
}; };
const handleSubmit = () => { const handleSubmit = () => {
dispatch(changeEditEventDescription(editorStateRef.current!));
dispatch(submitEvent()); dispatch(submitEvent());
}; };
@ -238,11 +237,11 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />} labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />} hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
> >
<Textarea <ComposeEditor
autoComplete='off' ref={editorStateRef}
className='block w-full rounded-md border-gray-400 bg-white px-3 py-2 text-base text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm'
composeId='compose-event-modal'
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)} placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
value={description}
onChange={onChangeDescription}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup

View file

@ -135,7 +135,6 @@ const normalizeLocked = (group: ImmutableMap<string, any>) => {
return group.set('locked', locked); return group.set('locked', locked);
}; };
/** Rewrite `<p></p>` to empty string. */ /** Rewrite `<p></p>` to empty string. */
const fixNote = (group: ImmutableMap<string, any>) => { const fixNote = (group: ImmutableMap<string, any>) => {
if (group.get('note') === '<p></p>') { if (group.get('note') === '<p></p>') {

View file

@ -54,6 +54,7 @@ import {
COMPOSE_EDITOR_STATE_SET, COMPOSE_EDITOR_STATE_SET,
COMPOSE_SET_GROUP_TIMELINE_VISIBLE, COMPOSE_SET_GROUP_TIMELINE_VISIBLE,
} from '../actions/compose'; } from '../actions/compose';
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET } from '../actions/events';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me';
import { SETTING_CHANGE, FE_NAME } from '../actions/settings'; import { SETTING_CHANGE, FE_NAME } from '../actions/settings';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
@ -507,6 +508,10 @@ export default function compose(state = initialState, action: AnyAction) {
return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value)); return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value));
case COMPOSE_EDITOR_STATE_SET: case COMPOSE_EDITOR_STATE_SET:
return updateCompose(state, action.id, compose => compose.set('editorState', action.editorState)); return updateCompose(state, action.id, compose => compose.set('editorState', action.editorState));
case EVENT_COMPOSE_CANCEL:
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', ''));
case EVENT_FORM_SET:
return updateCompose(state, 'event-compose-modal', compose => compose.set('text', action.text));
default: default:
return state; return state;
} }

View file

@ -24,7 +24,6 @@ const getScopes = (state: RootState) => {
return getInstanceScopes(state.instance); return getInstanceScopes(state.instance);
}; };
export { export {
getInstanceScopes, getInstanceScopes,
getScopes, getScopes,