This commit is contained in:
Untone 2024-07-22 08:41:10 +03:00
parent 0061b68257
commit 751157b421
5 changed files with 138 additions and 146 deletions

View File

@ -6,11 +6,11 @@ import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { import {
Author,
QueryLoad_Reactions_ByArgs, QueryLoad_Reactions_ByArgs,
Reaction, Reaction,
ReactionKind, ReactionKind,
ReactionSort ReactionSort,
Shout
} from '~/graphql/schema/core.gen' } from '~/graphql/schema/core.gen'
import { byCreated, byStat } from '~/lib/sort' import { byCreated, byStat } from '~/lib/sort'
import { SortFunction } from '~/types/common' import { SortFunction } from '~/types/common'
@ -24,22 +24,22 @@ import { Comment } from './Comment'
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor')) const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
type Props = { type Props = {
articleAuthors: Author[] shout: Shout
shoutSlug: string
shoutId: number
} }
const COMMENTS_PER_PAGE = 50 const COMMENTS_PER_PAGE = 50
export const CommentsTree = (props: Props) => { export const CommentsTree = (props: Props) => {
const { session } = useSession() const { session } = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const { reactionEntities, createReaction, loadReactionsBy, addReactions } = useReactions()
const { seen } = useFeed()
const [commentsOrder, setCommentsOrder] = createSignal<ReactionSort>(ReactionSort.Newest) const [commentsOrder, setCommentsOrder] = createSignal<ReactionSort>(ReactionSort.Newest)
const [onlyNew, setOnlyNew] = createSignal(false) const [onlyNew, setOnlyNew] = createSignal(false)
const [newReactions, setNewReactions] = createSignal<Reaction[]>([]) const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>() const [clickedReplyId, setClickedReplyId] = createSignal<number>()
const { reactionEntities, createReaction, loadReactionsBy, addReactions } = useReactions()
const shoutLastSeen = createMemo(() => seen()[props.shout.slug] ?? 0)
const comments = createMemo(() => const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
) )
@ -57,12 +57,9 @@ export const CommentsTree = (props: Props) => {
} }
return newSortedComments return newSortedComments
}) })
const { seen } = useFeed()
const shoutLastSeen = createMemo(() => seen()[props.shoutSlug] ?? 0)
onMount(() => { onMount(() => {
const currentDate = new Date() const currentDate = new Date()
const setCookie = () => localStorage?.setItem(`${props.shoutSlug}`, `${currentDate}`) const setCookie = () => localStorage?.setItem(`${props.shout.slug}`, `${currentDate}`)
if (!shoutLastSeen()) { if (!shoutLastSeen()) {
setCookie() setCookie()
} else if (currentDate.getTime() > shoutLastSeen()) { } else if (currentDate.getTime() > shoutLastSeen()) {
@ -80,24 +77,6 @@ export const CommentsTree = (props: Props) => {
} }
}) })
const [posting, setPosting] = createSignal(false) 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 [commentsLoading, setCommentsLoading] = createSignal(false)
const [pagination, setPagination] = createSignal(0) const [pagination, setPagination] = createSignal(0)
const loadMoreComments = async () => { const loadMoreComments = async () => {
@ -105,7 +84,7 @@ export const CommentsTree = (props: Props) => {
const next = pagination() + 1 const next = pagination() + 1
const offset = next * COMMENTS_PER_PAGE const offset = next * COMMENTS_PER_PAGE
const opts: QueryLoad_Reactions_ByArgs = { const opts: QueryLoad_Reactions_ByArgs = {
by: { comment: true, shout: props.shoutSlug }, by: { comment: true, shout: props.shout.slug },
limit: COMMENTS_PER_PAGE, limit: COMMENTS_PER_PAGE,
offset offset
} }
@ -116,6 +95,24 @@ export const CommentsTree = (props: Props) => {
return rrr as LoadMoreItems 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 ( return (
<> <>
<div class={styles.commentsHeaderWrapper}> <div class={styles.commentsHeaderWrapper}>
@ -159,16 +156,14 @@ export const CommentsTree = (props: Props) => {
<LoadMoreWrapper <LoadMoreWrapper
loadFunction={loadMoreComments} loadFunction={loadMoreComments}
pageSize={COMMENTS_PER_PAGE} pageSize={COMMENTS_PER_PAGE}
hidden={commentsLoading()} hidden={commentsLoading() || comments().length >= (props.shout?.stat?.commented || 0)}
> >
<ul class={styles.comments}> <ul class={styles.comments}>
<For each={sortedComments().filter((r) => !r.reply_to)}> <For each={sortedComments().filter((r) => !r.reply_to)}>
{(reaction) => ( {(reaction) => (
<Comment <Comment
sortedComments={sortedComments()} sortedComments={sortedComments()}
isArticleAuthor={Boolean( isArticleAuthor={props.shout.authors?.some((a) => a && reaction.created_by.id === a.id)}
props.articleAuthors.some((a) => a?.id === reaction.created_by.id)
)}
comment={reaction} comment={reaction}
clickedReply={(id) => setClickedReplyId(id)} clickedReply={(id) => setClickedReplyId(id)}
clickedReplyId={clickedReplyId()} clickedReplyId={clickedReplyId()}

View File

@ -560,11 +560,7 @@ export const FullArticle = (props: Props) => {
</For> </For>
</div> </div>
<div id="comments" ref={(el) => (commentsRef = el)}> <div id="comments" ref={(el) => (commentsRef = el)}>
<CommentsTree <CommentsTree shout={props.article} />
shoutId={props.article.id}
shoutSlug={props.article.slug}
articleAuthors={props.article.authors as Author[]}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -162,7 +162,7 @@ export const AuthorCard = (props: Props) => {
<For each={authorSubs()}> <For each={authorSubs()}>
{(subscription) => {(subscription) =>
'name' in subscription ? ( 'name' in subscription ? (
<AuthorBadge author={subscription as Author} subscriptionsMode={true} /> <AuthorBadge author={subscription as Author} nameOnly={true} />
) : ( ) : (
<TopicBadge topic={subscription as Topic} subscriptionsMode={true} /> <TopicBadge topic={subscription as Topic} subscriptionsMode={true} />
) )

View File

@ -1,3 +1,4 @@
import { Editor } from '@tiptap/core'
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { BubbleMenu } from '@tiptap/extension-bubble-menu'
@ -10,7 +11,7 @@ import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder' import { Placeholder } from '@tiptap/extension-placeholder'
import { Text } from '@tiptap/extension-text' import { Text } from '@tiptap/extension-text'
import { clsx } from 'clsx' 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 { Portal } from 'solid-js/web'
import { import {
createEditorTransaction, createEditorTransaction,
@ -19,26 +20,23 @@ import {
useEditorIsEmpty, useEditorIsEmpty,
useEditorIsFocused useEditorIsFocused
} from 'solid-tiptap' } from 'solid-tiptap'
import { Modal } from '~/components/_shared/Modal'
import { useEditorContext } from '~/context/editor' import { useUI } from '~/context/ui'
import { useLocalize } from '~/context/localize'
import { UploadedFile } from '~/types/upload' import { UploadedFile } from '~/types/upload'
import { useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Loading } from '../_shared/Loading' import { Loading } from '../_shared/Loading'
import { Modal } from '../_shared/Modal'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { LinkBubbleMenuModule } from './LinkBubbleMenu' import { LinkBubbleMenuModule } from './LinkBubbleMenu'
import styles from './SimplifiedEditor.module.scss'
import { TextBubbleMenu } from './TextBubbleMenu' import { TextBubbleMenu } from './TextBubbleMenu'
import { UploadModalContent } from './UploadModalContent' import { UploadModalContent } from './UploadModalContent'
import { Figcaption } from './extensions/Figcaption' import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure' import { Figure } from './extensions/Figure'
import { Editor } from '@tiptap/core'
import { useUI } from '~/context/ui'
import styles from './SimplifiedEditor.module.scss'
type Props = { type Props = {
placeholder: string placeholder: string
initialContent?: string initialContent?: string
@ -67,31 +65,25 @@ type Props = {
const DEFAULT_MAX_LENGTH = 400 const DEFAULT_MAX_LENGTH = 400
const SimplifiedEditor = (props: Props) => { 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 { showModal, hideModal } = useUI()
const { t } = useLocalize()
const [counter, setCounter] = createSignal<number>(0) const [counter, setCounter] = createSignal<number>(0)
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false) const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false) const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>() const { setEditor, editor } = useEditorContext()
const { editor, setEditor } = useEditorContext()
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
let wrapperEditorElRef: HTMLElement | undefined
let textBubbleMenuRef: HTMLDivElement | undefined
let linkBubbleMenuRef: HTMLDivElement | undefined
const ImageFigure = Figure.extend({ const ImageFigure = Figure.extend({
name: 'capturedImage', name: 'capturedImage',
content: 'figcaption image' content: 'figcaption image'
}) })
createEffect(() => {
createEffect( const e = createTiptapEditor(() => ({
on( element: editorElRef as HTMLElement,
() => editorElement(),
(ee: HTMLDivElement | undefined) => {
if (ee && textBubbleMenuRef && linkBubbleMenuRef) {
const freshEditor = createTiptapEditor<HTMLElement>(() => ({
element: ee,
editorProps: { editorProps: {
attributes: { attributes: {
class: styles.simplifiedEditorField class: styles.simplifiedEditorField
@ -148,17 +140,12 @@ const SimplifiedEditor = (props: Props) => {
}) })
], ],
autofocus: props.autoFocus, autofocus: props.autoFocus,
content: props.initialContent || null content: content ?? null
})) }))
const editorInstance = freshEditor()
if (!editorInstance) return
setEditor(editorInstance)
}
},
{ defer: true }
)
)
e() && setEditor(e() as Editor)
})
const content = props.initialContent
const isEmpty = useEditorIsEmpty(() => editor()) const isEmpty = useEditorIsEmpty(() => editor())
const isFocused = useEditorIsFocused(() => editor()) const isFocused = useEditorIsFocused(() => editor())
@ -211,7 +198,7 @@ const SimplifiedEditor = (props: Props) => {
} }
if (props.resetToInitial) { if (props.resetToInitial) {
editor()?.commands.clearContent(true) 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) => {
<Show when={props.label && counter() > 0}> <Show when={props.label && counter() > 0}>
<div class={styles.label}>{props.label}</div> <div class={styles.label}>{props.label}</div>
</Show> </Show>
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={setEditorElement} />
<Show when={props.maxHeight} fallback={<div ref={(el) => (editorElRef = el)} />}>
<div style={maxHeightStyle} ref={(el) => (editorElRef = el)} />
</Show>
<Show when={!props.onlyBubbleControls}> <Show when={!props.onlyBubbleControls}>
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}> <div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
<div class={styles.actions}> <div class={styles.actions}>
@ -379,14 +370,11 @@ const SimplifiedEditor = (props: Props) => {
<Show when={props.imageEnabled}> <Show when={props.imageEnabled}>
<Portal> <Portal>
<Modal variant="narrow" name="simplifiedEditorUploadImage"> <Modal variant="narrow" name="simplifiedEditorUploadImage">
<UploadModalContent <UploadModalContent onClose={(value) => value && renderImage(value)} />
onClose={(value) => {
renderImage(value as UploadedFile)
}}
/>
</Modal> </Modal>
</Portal> </Portal>
</Show> </Show>
<Show when={!!editor()}>
<Show when={props.onlyBubbleControls}> <Show when={props.onlyBubbleControls}>
<TextBubbleMenu <TextBubbleMenu
shouldShow={true} shouldShow={true}
@ -400,6 +388,7 @@ const SimplifiedEditor = (props: Props) => {
ref={(el) => (linkBubbleMenuRef = el)} ref={(el) => (linkBubbleMenuRef = el)}
onClose={handleHideLinkBubble} onClose={handleHideLinkBubble}
/> />
</Show>
</div> </div>
</ShowOnlyOnClient> </ShowOnlyOnClient>
) )

View File

@ -753,7 +753,12 @@
white-space: nowrap; white-space: nowrap;
} }
.rightItem {
margin-right: 0;
position: absolute;
right: 0;
top: 0;
}
} }
a:link, a:link,
@ -796,6 +801,13 @@
} }
} }
.rightItemIcon {
display: inline-block;
margin-left: 0.3em;
position: relative;
top: 0.15em;
}
.editorPopup { .editorPopup {
border: 1px solid rgb(0 0 0 / 15%) !important; border: 1px solid rgb(0 0 0 / 15%) !important;
border-radius: 1.6rem; border-radius: 1.6rem;