diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 131ee86d..ef05e014 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -6,11 +6,11 @@ import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' import { useSession } from '~/context/session' import { - Author, QueryLoad_Reactions_ByArgs, Reaction, ReactionKind, - ReactionSort + ReactionSort, + Shout } from '~/graphql/schema/core.gen' import { byCreated, byStat } from '~/lib/sort' import { SortFunction } from '~/types/common' @@ -24,22 +24,22 @@ import { Comment } from './Comment' const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor')) type Props = { - articleAuthors: Author[] - shoutSlug: string - shoutId: number + shout: Shout } const COMMENTS_PER_PAGE = 50 export const CommentsTree = (props: Props) => { const { session } = useSession() const { t } = useLocalize() + const { reactionEntities, createReaction, loadReactionsBy, addReactions } = useReactions() + const { seen } = useFeed() const [commentsOrder, setCommentsOrder] = createSignal(ReactionSort.Newest) const [onlyNew, setOnlyNew] = createSignal(false) const [newReactions, setNewReactions] = createSignal([]) const [clearEditor, setClearEditor] = createSignal(false) const [clickedReplyId, setClickedReplyId] = createSignal() - const { reactionEntities, createReaction, loadReactionsBy, addReactions } = useReactions() + const shoutLastSeen = createMemo(() => seen()[props.shout.slug] ?? 0) const comments = createMemo(() => Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') ) @@ -57,12 +57,9 @@ export const CommentsTree = (props: Props) => { } return newSortedComments }) - const { seen } = useFeed() - const shoutLastSeen = createMemo(() => seen()[props.shoutSlug] ?? 0) - onMount(() => { const currentDate = new Date() - const setCookie = () => localStorage?.setItem(`${props.shoutSlug}`, `${currentDate}`) + const setCookie = () => localStorage?.setItem(`${props.shout.slug}`, `${currentDate}`) if (!shoutLastSeen()) { setCookie() } else if (currentDate.getTime() > shoutLastSeen()) { @@ -80,24 +77,6 @@ export const CommentsTree = (props: Props) => { } }) const [posting, setPosting] = createSignal(false) - const handleSubmitComment = async (value: string) => { - setPosting(true) - try { - await createReaction({ - reaction: { - kind: ReactionKind.Comment, - body: value, - shout: props.shoutId - } - }) - setClearEditor(true) - await loadReactionsBy({ by: { shout: props.shoutSlug } }) - } catch (error) { - console.error('[handleCreate reaction]:', error) - } - setClearEditor(false) - setPosting(false) - } const [commentsLoading, setCommentsLoading] = createSignal(false) const [pagination, setPagination] = createSignal(0) const loadMoreComments = async () => { @@ -105,7 +84,7 @@ export const CommentsTree = (props: Props) => { const next = pagination() + 1 const offset = next * COMMENTS_PER_PAGE const opts: QueryLoad_Reactions_ByArgs = { - by: { comment: true, shout: props.shoutSlug }, + by: { comment: true, shout: props.shout.slug }, limit: COMMENTS_PER_PAGE, offset } @@ -116,6 +95,24 @@ export const CommentsTree = (props: Props) => { return rrr as LoadMoreItems } + const handleSubmitComment = async (value: string) => { + setPosting(true) + try { + await createReaction({ + reaction: { + kind: ReactionKind.Comment, + body: value, + shout: props.shout.id + } + }) + setClearEditor(true) + await loadMoreComments() + } catch (error) { + console.error('[handleCreate reaction]:', error) + } + setClearEditor(false) + setPosting(false) + } return ( <>
@@ -159,16 +156,14 @@ export const CommentsTree = (props: Props) => {
(commentsRef = el)}> - +
diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index 405749a9..4f87ec2c 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -162,7 +162,7 @@ export const AuthorCard = (props: Props) => { {(subscription) => 'name' in subscription ? ( - + ) : ( ) diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index 79db649f..9f28d7b2 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -1,3 +1,4 @@ +import { Editor } from '@tiptap/core' import { Blockquote } from '@tiptap/extension-blockquote' import { Bold } from '@tiptap/extension-bold' import { BubbleMenu } from '@tiptap/extension-bubble-menu' @@ -10,7 +11,7 @@ import { Paragraph } from '@tiptap/extension-paragraph' import { Placeholder } from '@tiptap/extension-placeholder' import { Text } from '@tiptap/extension-text' import { clsx } from 'clsx' -import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import { Show, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js' import { Portal } from 'solid-js/web' import { createEditorTransaction, @@ -19,26 +20,23 @@ import { useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' - -import { useEditorContext } from '~/context/editor' -import { useLocalize } from '~/context/localize' +import { Modal } from '~/components/_shared/Modal' +import { useUI } from '~/context/ui' import { UploadedFile } from '~/types/upload' +import { useEditorContext } from '../../context/editor' +import { useLocalize } from '../../context/localize' import { Button } from '../_shared/Button' import { Icon } from '../_shared/Icon' import { Loading } from '../_shared/Loading' -import { Modal } from '../_shared/Modal' import { Popover } from '../_shared/Popover' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { LinkBubbleMenuModule } from './LinkBubbleMenu' +import styles from './SimplifiedEditor.module.scss' import { TextBubbleMenu } from './TextBubbleMenu' import { UploadModalContent } from './UploadModalContent' import { Figcaption } from './extensions/Figcaption' import { Figure } from './extensions/Figure' -import { Editor } from '@tiptap/core' -import { useUI } from '~/context/ui' -import styles from './SimplifiedEditor.module.scss' - type Props = { placeholder: string initialContent?: string @@ -67,98 +65,87 @@ type Props = { const DEFAULT_MAX_LENGTH = 400 const SimplifiedEditor = (props: Props) => { - const { t } = useLocalize() + const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH + let wrapperEditorElRef: HTMLElement | undefined + let editorElRef: HTMLElement | undefined + let textBubbleMenuRef: HTMLDivElement | undefined + let linkBubbleMenuRef: HTMLDivElement | undefined const { showModal, hideModal } = useUI() + const { t } = useLocalize() const [counter, setCounter] = createSignal(0) const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false) const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false) - const [editorElement, setEditorElement] = createSignal() - const { editor, setEditor } = useEditorContext() - - const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH - let wrapperEditorElRef: HTMLElement | undefined - let textBubbleMenuRef: HTMLDivElement | undefined - let linkBubbleMenuRef: HTMLDivElement | undefined + const { setEditor, editor } = useEditorContext() const ImageFigure = Figure.extend({ name: 'capturedImage', content: 'figcaption image' }) - - createEffect( - on( - () => editorElement(), - (ee: HTMLDivElement | undefined) => { - if (ee && textBubbleMenuRef && linkBubbleMenuRef) { - const freshEditor = createTiptapEditor(() => ({ - element: ee, - editorProps: { - attributes: { - class: styles.simplifiedEditorField - } - }, - extensions: [ - Document, - Text, - Paragraph, - Bold, - Italic, - Link.extend({ - inclusive: false - }).configure({ - autolink: true, - openOnClick: false - }), - CharacterCount.configure({ - limit: props.noLimits ? null : maxLength - }), - Blockquote.configure({ - HTMLAttributes: { - class: styles.blockQuote - } - }), - BubbleMenu.configure({ - pluginKey: 'textBubbleMenu', - element: textBubbleMenuRef, - shouldShow: ({ view, state }) => { - if (!props.onlyBubbleControls) return false - const { selection } = state - const { empty } = selection - return view.hasFocus() && !empty - } - }), - BubbleMenu.configure({ - pluginKey: 'linkBubbleMenu', - element: linkBubbleMenuRef, - shouldShow: ({ state }) => { - const { selection } = state - const { empty } = selection - return !empty && shouldShowLinkBubbleMenu() - }, - tippyOptions: { - placement: 'bottom' - } - }), - ImageFigure, - Image, - Figcaption, - Placeholder.configure({ - emptyNodeClass: styles.emptyNode, - placeholder: props.placeholder - }) - ], - autofocus: props.autoFocus, - content: props.initialContent || null - })) - const editorInstance = freshEditor() - if (!editorInstance) return - setEditor(editorInstance) + createEffect(() => { + const e = createTiptapEditor(() => ({ + element: editorElRef as HTMLElement, + editorProps: { + attributes: { + class: styles.simplifiedEditorField } }, - { defer: true } - ) - ) + extensions: [ + Document, + Text, + Paragraph, + Bold, + Italic, + Link.extend({ + inclusive: false + }).configure({ + autolink: true, + openOnClick: false + }), + CharacterCount.configure({ + limit: props.noLimits ? null : maxLength + }), + Blockquote.configure({ + HTMLAttributes: { + class: styles.blockQuote + } + }), + BubbleMenu.configure({ + pluginKey: 'textBubbleMenu', + element: textBubbleMenuRef, + shouldShow: ({ view, state }) => { + if (!props.onlyBubbleControls) return false + const { selection } = state + const { empty } = selection + return view.hasFocus() && !empty + } + }), + BubbleMenu.configure({ + pluginKey: 'linkBubbleMenu', + element: linkBubbleMenuRef, + shouldShow: ({ state }) => { + const { selection } = state + const { empty } = selection + return !empty && shouldShowLinkBubbleMenu() + }, + tippyOptions: { + placement: 'bottom' + } + }), + ImageFigure, + Image, + Figcaption, + Placeholder.configure({ + emptyNodeClass: styles.emptyNode, + placeholder: props.placeholder + }) + ], + autofocus: props.autoFocus, + content: content ?? null + })) + e() && setEditor(e() as Editor) + }) + const content = props.initialContent const isEmpty = useEditorIsEmpty(() => editor()) const isFocused = useEditorIsFocused(() => editor()) @@ -211,7 +198,7 @@ const SimplifiedEditor = (props: Props) => { } if (props.resetToInitial) { editor()?.commands.clearContent(true) - if (props.initialContent) editor()?.commands.setContent(props.initialContent) + props.initialContent && editor()?.commands.setContent(props.initialContent) } }) @@ -290,7 +277,11 @@ const SimplifiedEditor = (props: Props) => { 0}>
{props.label}
-
+ + (editorElRef = el)} />}> +
(editorElRef = el)} /> + +
@@ -379,27 +370,25 @@ const SimplifiedEditor = (props: Props) => { - { - renderImage(value as UploadedFile) - }} - /> + value && renderImage(value)} /> - - + + (textBubbleMenuRef = el)} + /> + + (textBubbleMenuRef = el)} + ref={(el) => (linkBubbleMenuRef = el)} + onClose={handleHideLinkBubble} /> - (linkBubbleMenuRef = el)} - onClose={handleHideLinkBubble} - />
) diff --git a/src/components/HeaderNav/Header.module.scss b/src/components/HeaderNav/Header.module.scss index a4c8b461..1bf3481d 100644 --- a/src/components/HeaderNav/Header.module.scss +++ b/src/components/HeaderNav/Header.module.scss @@ -753,7 +753,12 @@ white-space: nowrap; } - + .rightItem { + margin-right: 0; + position: absolute; + right: 0; + top: 0; + } } a:link, @@ -796,6 +801,13 @@ } } +.rightItemIcon { + display: inline-block; + margin-left: 0.3em; + position: relative; + top: 0.15em; +} + .editorPopup { border: 1px solid rgb(0 0 0 / 15%) !important; border-radius: 1.6rem;