From b15640603c7dd45d62e2a2cec3b5a52687f9be94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 11 Apr 2023 23:22:34 +0200 Subject: [PATCH] Lexical: Use in ComposeEventModal, style improvements, types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../dropdown-menu/dropdown-menu-item.tsx | 3 +- app/soapbox/components/ui/portal/portal.tsx | 3 +- app/soapbox/components/ui/slider/slider.tsx | 3 +- app/soapbox/containers/soapbox.tsx | 1 - app/soapbox/entity-store/hooks/utils.ts | 3 +- app/soapbox/features/admin/user-index.tsx | 1 - .../__tests__/chat-message-list.test.tsx | 1 - .../compose/components/compose-form.tsx | 67 +++---------------- app/soapbox/features/compose/editor/index.tsx | 51 ++++++++++++-- .../editor/plugins/autosuggest-plugin.tsx | 57 +++++++--------- .../compose/editor/plugins/mention-plugin.tsx | 1 - .../emoji-picker-dropdown-container.tsx | 1 - .../components/icon-picker-menu.tsx | 1 - .../compose-event-modal.tsx | 21 +++--- app/soapbox/normalizers/group.ts | 1 - app/soapbox/reducers/compose.ts | 5 ++ app/soapbox/utils/scopes.ts | 3 +- 17 files changed, 100 insertions(+), 123 deletions(-) diff --git a/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx b/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx index 8b0ca7755..97c2ff045 100644 --- a/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx +++ b/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx @@ -35,7 +35,6 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => { if (!item) return; if (onClick) onClick(); - if (item.to) { event.preventDefault(); history.push(item.to); @@ -106,4 +105,4 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => { ); }; -export default DropdownMenuItem; \ No newline at end of file +export default DropdownMenuItem; diff --git a/app/soapbox/components/ui/portal/portal.tsx b/app/soapbox/components/ui/portal/portal.tsx index 4f83b98b4..964a9a738 100644 --- a/app/soapbox/components/ui/portal/portal.tsx +++ b/app/soapbox/components/ui/portal/portal.tsx @@ -15,7 +15,6 @@ const Portal: React.FC = ({ children }) => { setIsRendered(true); }, []); - if (!isRendered) { return null; } @@ -28,4 +27,4 @@ const Portal: React.FC = ({ children }) => { ); }; -export default Portal; \ No newline at end of file +export default Portal; diff --git a/app/soapbox/components/ui/slider/slider.tsx b/app/soapbox/components/ui/slider/slider.tsx index 4449f7f5b..75abd2d39 100644 --- a/app/soapbox/components/ui/slider/slider.tsx +++ b/app/soapbox/components/ui/slider/slider.tsx @@ -99,7 +99,6 @@ const findElementPosition = (el: HTMLElement) => { }; }; - const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Point => { const box = findElementPosition(el); const boxW = el.offsetWidth; @@ -121,4 +120,4 @@ const getPointerPosition = (el: HTMLElement, event: MouseEvent & TouchEvent): Po }; }; -export default Slider; \ No newline at end of file +export default Slider; diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index fb6ce9481..9ba769ffb 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -11,7 +11,6 @@ import { CompatRouter } from 'react-router-dom-v5-compat'; // @ts-ignore: it doesn't have types import { ScrollContext } from 'react-router-scroll-4'; - import { loadInstance } from 'soapbox/actions/instance'; import { fetchMe } from 'soapbox/actions/me'; import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox'; diff --git a/app/soapbox/entity-store/hooks/utils.ts b/app/soapbox/entity-store/hooks/utils.ts index 8b9269a2e..cdb059ee4 100644 --- a/app/soapbox/entity-store/hooks/utils.ts +++ b/app/soapbox/entity-store/hooks/utils.ts @@ -12,5 +12,4 @@ function parseEntitiesPath(expandedPath: ExpandedEntitiesPath) { }; } - -export { parseEntitiesPath }; \ No newline at end of file +export { parseEntitiesPath }; diff --git a/app/soapbox/features/admin/user-index.tsx b/app/soapbox/features/admin/user-index.tsx index f4150b859..06dcde6e6 100644 --- a/app/soapbox/features/admin/user-index.tsx +++ b/app/soapbox/features/admin/user-index.tsx @@ -37,7 +37,6 @@ const UserIndex: React.FC = () => { updateQuery(); }, []); - const hasMore = items.count() < total && !!next; const showLoading = isLoading && items.isEmpty(); diff --git a/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx b/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx index d538bee34..7c270da7f 100644 --- a/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx +++ b/app/soapbox/features/chats/components/__tests__/chat-message-list.test.tsx @@ -2,7 +2,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { VirtuosoMockContext } from 'react-virtuoso'; - import { ChatContext } from 'soapbox/contexts/chat-context'; import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers'; import { IAccount } from 'soapbox/queries/accounts'; diff --git a/app/soapbox/features/compose/components/compose-form.tsx b/app/soapbox/features/compose/components/compose-form.tsx index 59d6c1251..01f485caf 100644 --- a/app/soapbox/features/compose/components/compose-form.tsx +++ b/app/soapbox/features/compose/components/compose-form.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; 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 { 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 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)' }, publish: { id: 'compose_form.publish', defaultMessage: 'Post' }, publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, @@ -74,12 +71,11 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab const compose = useCompose(id); 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 scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size); 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 hasPoll = !!compose.poll; @@ -93,17 +89,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab const autosuggestTextareaRef = useRef(null); const editorStateRef = useRef(null); - const handleChange: React.ChangeEventHandler = (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 = () => { return clickableAreaRef ? clickableAreaRef.current : formRef.current; }; @@ -139,11 +124,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab const handleSubmit = (e?: React.FormEvent) => { 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: const fulltext = [spoilerText, countableText(text)].join(''); @@ -167,10 +147,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab 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) => { dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text'])); }; @@ -245,7 +221,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab let publishText: string | JSX.Element = ''; let publishIcon: string | undefined = undefined; - let textareaPlaceholder: MessageDescriptor; if (isEditing) { publishText = intl.formatMessage(messages.saveChanges); @@ -263,14 +238,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab publishText = intl.formatMessage(messages.schedule); } - if (event) { - textareaPlaceholder = messages.eventPlaceholder; - } else if (hasPoll) { - textareaPlaceholder = messages.pollPlaceholder; - } else { - textareaPlaceholder = messages.placeholder; - } - return ( {scheduledStatusCount > 0 && !event && !group && ( @@ -301,10 +268,14 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab
{!condensed && ( @@ -323,26 +294,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab )}
- - <> - - {extra &&
{extra}
} diff --git a/app/soapbox/features/compose/editor/index.tsx b/app/soapbox/features/compose/editor/index.tsx index 910719595..820f9f101 100644 --- a/app/soapbox/features/compose/editor/index.tsx +++ b/app/soapbox/features/compose/editor/index.tsx @@ -46,7 +46,29 @@ const StatePlugin = ({ composeId }: { composeId: string }) => { return null; }; -const ComposeEditor = React.forwardRef(({ composeId, condensed, onFocus, autoFocus }, editorStateRef) => { +interface IComposeEditor { + className?: string + composeId: string + condensed?: boolean + eventDiscussion?: boolean + hasPoll?: boolean + autoFocus?: boolean + onFocus?: React.FocusEventHandler + onPaste?: (files: FileList) => void + placeholder?: JSX.Element | string +} + +const ComposeEditor = React.forwardRef(({ + className, + composeId, + condensed, + eventDiscussion, + hasPoll, + autoFocus, + onFocus, + onPaste, + placeholder, +}, editorStateRef) => { const dispatch = useAppDispatch(); const features = useFeatures(); @@ -111,14 +133,29 @@ const ComposeEditor = React.forwardRef(({ composeId, condensed, onF } }; + const handlePaste: React.ClipboardEventHandler = (e) => { + if (onPaste && e.clipboardData && e.clipboardData.files.length === 1) { + onPaste(e.clipboardData.files); + e.preventDefault(); + } + }; + + let textareaPlaceholder = placeholder || ; + + if (eventDiscussion) { + textareaPlaceholder = ; + } else if (hasPoll) { + textareaPlaceholder = ; + } + return ( -
+
+
(({ composeId, condensed, onF
} placeholder={( -
- +
+ {textareaPlaceholder}
)} ErrorBoundary={LexicalErrorBoundary} diff --git a/app/soapbox/features/compose/editor/plugins/autosuggest-plugin.tsx b/app/soapbox/features/compose/editor/plugins/autosuggest-plugin.tsx index f5991b754..17ac6aa2e 100644 --- a/app/soapbox/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/app/soapbox/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -51,7 +51,7 @@ export type MenuRenderFn = ( anchorElementRef: MutableRefObject, ) => ReactPortal | JSX.Element | null; -function tryToPositionRange(leadOffset: number, range: Range): boolean { +const tryToPositionRange = (leadOffset: number, range: Range): boolean => { const domSelection = window.getSelection(); if (domSelection === null || !domSelection.isCollapsed) { return false; @@ -72,12 +72,12 @@ function tryToPositionRange(leadOffset: number, range: Range): boolean { } return true; -} +}; -function isSelectionOnEntityBoundary( +const isSelectionOnEntityBoundary = ( editor: LexicalEditor, offset: number, -): boolean { +): boolean => { if (offset !== 0) { return false; } @@ -91,21 +91,21 @@ function isSelectionOnEntityBoundary( } return false; }); -} +}; -function startTransition(callback: () => void) { +const startTransition = (callback: () => void) => { if (React.startTransition) { React.startTransition(callback); } else { callback(); } -} +}; // Got from https://stackoverflow.com/a/42543908/2013580 -export function getScrollParent( +export const getScrollParent = ( element: HTMLElement, includeHidden: boolean, -): HTMLElement | HTMLBodyElement { +): HTMLElement | HTMLBodyElement => { let style = getComputedStyle(element); const excludeStaticParent = style.position === 'absolute'; const overflowRegex = includeHidden @@ -130,24 +130,24 @@ export function getScrollParent( } } return document.body; -} +}; -function isTriggerVisibleInNearestScrollContainer( +const isTriggerVisibleInNearestScrollContainer = ( targetElement: HTMLElement, containerElement: HTMLElement, -): boolean { +): boolean => { const tRect = targetElement.getBoundingClientRect(); const cRect = containerElement.getBoundingClientRect(); return tRect.top > cRect.top && tRect.top < cRect.bottom; -} +}; // Reposition the menu on scroll, window resize, and element resize. -export function useDynamicPositioning( +export const useDynamicPositioning = ( resolution: Resolution | null, targetElement: HTMLElement | null, onReposition: () => void, onVisibilityChange?: (isInView: boolean) => void, -) { +) => { const [editor] = useLexicalComposerContext(); useEffect(() => { if (targetElement && resolution) { @@ -161,9 +161,9 @@ export function useDynamicPositioning( targetElement, rootScrollParent, ); - const handleScroll = function () { + const handleScroll = () => { if (!ticking) { - window.requestAnimationFrame(function () { + window.requestAnimationFrame(() => { onReposition(); ticking = false; }); @@ -194,22 +194,17 @@ export function useDynamicPositioning( }; } }, [targetElement, editor, onVisibilityChange, onReposition, resolution]); -} +}; -function LexicalPopoverMenu({ - anchorElementRef, - menuRenderFn, -}: { +const LexicalPopoverMenu = ({ anchorElementRef, menuRenderFn }: { anchorElementRef: MutableRefObject menuRenderFn: MenuRenderFn -}): JSX.Element | null { - return menuRenderFn(anchorElementRef); -} +}): JSX.Element | null => menuRenderFn(anchorElementRef); -function useMenuAnchorRef( +const useMenuAnchorRef = ( resolution: Resolution | null, setResolution: (r: Resolution | null) => void, -): MutableRefObject { +): MutableRefObject => { const [editor] = useLexicalComposerContext(); const anchorElementRef = useRef(document.createElement('div')); const positionMenu = useCallback(() => { @@ -272,7 +267,7 @@ function useMenuAnchorRef( ); return anchorElementRef; -} +}; export type AutosuggestPluginProps = { composeId: string @@ -280,11 +275,11 @@ export type AutosuggestPluginProps = { setSuggestionsHidden: (value: boolean) => void }; -export function AutosuggestPlugin({ +export const AutosuggestPlugin = ({ composeId, suggestionsHidden, setSuggestionsHidden, -}: AutosuggestPluginProps): JSX.Element | null { +}: AutosuggestPluginProps): JSX.Element | null => { const { suggestions } = useCompose(composeId); const dispatch = useAppDispatch(); @@ -473,4 +468,4 @@ export function AutosuggestPlugin({ } /> ); -} +}; diff --git a/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx b/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx index 18cbec467..1bfbaabd4 100644 --- a/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx +++ b/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx @@ -12,7 +12,6 @@ import { useCallback, useEffect } from 'react'; import { $createMentionNode, MentionNode } from '../nodes/mention-node'; - import type { TextNode } from 'lexical'; export const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i'); diff --git a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx index 5929d1711..7bc3cd055 100644 --- a/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx +++ b/app/soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx @@ -75,5 +75,4 @@ const EmojiPickerDropdownContainer = ( ); }; - export default EmojiPickerDropdownContainer; diff --git a/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx b/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx index 765d282c2..755bdb0b6 100644 --- a/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx +++ b/app/soapbox/features/soapbox-config/components/icon-picker-menu.tsx @@ -11,7 +11,6 @@ const messages = defineMessages({ const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - interface IIconPickerMenu { icons: Record> onClose: () => void diff --git a/app/soapbox/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx b/app/soapbox/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx index 2ecbbda4f..98196b8a0 100644 --- a/app/soapbox/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx +++ b/app/soapbox/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx @@ -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 { @@ -21,9 +21,10 @@ import { closeModal, openModal } from 'soapbox/actions/modals'; import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location'; import LocationSearch from 'soapbox/components/location-search'; 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 { 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 { DatePicker } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; @@ -94,13 +95,14 @@ const ComposeEventModal: React.FC = ({ onClose }) => { const intl = useIntl(); const dispatch = useAppDispatch(); + const editorStateRef = useRef(null); + const [tab, setTab] = useState<'edit' | 'pending'>('edit'); const banner = useAppSelector((state) => state.compose_event.banner); const isUploading = useAppSelector((state) => state.compose_event.is_uploading); 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 endTime = useAppSelector((state) => state.compose_event.end_time); const approvalRequired = useAppSelector((state) => state.compose_event.approval_required); @@ -114,10 +116,6 @@ const ComposeEventModal: React.FC = ({ onClose }) => { dispatch(changeEditEventName(target.value)); }; - const onChangeDescription: React.ChangeEventHandler = ({ target }) => { - dispatch(changeEditEventDescription(target.value)); - }; - const onChangeStartTime = (date: Date) => { dispatch(changeEditEventStartTime(date)); }; @@ -170,6 +168,7 @@ const ComposeEventModal: React.FC = ({ onClose }) => { }; const handleSubmit = () => { + dispatch(changeEditEventDescription(editorStateRef.current!)); dispatch(submitEvent()); }; @@ -238,11 +237,11 @@ const ComposeEventModal: React.FC = ({ onClose }) => { labelText={} hintText={} > -