webapp/src/components/Article/CommentsTree.tsx

195 lines
6.7 KiB
TypeScript
Raw Normal View History

2024-03-01 13:04:28 +00:00
import { clsx } from 'clsx'
import { For, Show, createMemo, createSignal, lazy, onMount } from 'solid-js'
2024-07-13 17:22:32 +00:00
import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize'
2024-07-22 11:24:36 +00:00
import { COMMENTS_PER_PAGE, useReactions } from '~/context/reactions'
import { useSession } from '~/context/session'
2024-07-21 14:25:46 +00:00
import {
Reaction,
ReactionKind,
2024-07-22 05:41:10 +00:00
ReactionSort,
Shout
2024-07-21 14:25:46 +00:00
} from '~/graphql/schema/core.gen'
2024-07-13 11:47:31 +00:00
import { byCreated, byStat } from '~/lib/sort'
2024-07-13 17:22:32 +00:00
import { SortFunction } from '~/types/common'
2024-03-01 13:04:28 +00:00
import { Button } from '../_shared/Button'
2024-07-21 14:25:46 +00:00
import { InlineLoader } from '../_shared/InlineLoader'
import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper'
2024-03-01 13:04:28 +00:00
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import styles from './Article.module.scss'
2024-07-13 17:22:32 +00:00
import { Comment } from './Comment'
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
type Props = {
2024-07-22 05:41:10 +00:00
shout: Shout
}
export const CommentsTree = (props: Props) => {
2024-06-24 17:50:27 +00:00
const { session } = useSession()
2023-04-17 10:31:20 +00:00
const { t } = useLocalize()
2024-07-22 11:24:36 +00:00
const { reactionEntities, createReaction, loadShoutComments } = useReactions()
2024-07-22 05:41:10 +00:00
const { seen } = useFeed()
2024-02-29 17:51:07 +00:00
const [commentsOrder, setCommentsOrder] = createSignal<ReactionSort>(ReactionSort.Newest)
const [onlyNew, setOnlyNew] = createSignal(false)
2023-04-17 10:31:20 +00:00
const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>()
2023-02-11 10:49:31 +00:00
2024-07-22 05:41:10 +00:00
const shoutLastSeen = createMemo(() => seen()[props.shout.slug] ?? 0)
2023-02-17 09:21:02 +00:00
const comments = createMemo(() =>
2024-06-26 08:22:05 +00:00
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
2023-02-17 09:21:02 +00:00
)
const sortedComments = createMemo(() => {
let newSortedComments = [...comments()]
newSortedComments = newSortedComments.sort(byCreated)
2024-02-29 17:51:07 +00:00
if (onlyNew()) {
return newReactions().sort(byCreated).reverse()
2023-02-17 09:21:02 +00:00
}
2024-02-29 17:51:07 +00:00
if (commentsOrder() === ReactionSort.Like) {
2024-06-24 17:50:27 +00:00
newSortedComments = newSortedComments.sort(byStat('rating') as SortFunction<Reaction>)
2023-02-21 12:32:51 +00:00
}
2023-02-17 09:21:02 +00:00
return newSortedComments
})
2023-03-06 14:06:48 +00:00
onMount(() => {
2024-07-06 06:24:37 +00:00
const currentDate = new Date()
2024-07-22 05:41:10 +00:00
const setCookie = () => localStorage?.setItem(`${props.shout.slug}`, `${currentDate}`)
2024-05-07 08:51:17 +00:00
if (!shoutLastSeen()) {
2023-02-21 12:32:51 +00:00
setCookie()
2024-05-07 08:51:17 +00:00
} else if (currentDate.getTime() > shoutLastSeen()) {
2023-02-21 12:32:51 +00:00
const newComments = comments().filter((c) => {
2024-06-24 17:50:27 +00:00
if (
(session()?.user?.app_data?.profile?.id && c.reply_to) ||
c.created_by.id === session()?.user?.app_data?.profile?.id
) {
2023-02-28 17:13:14 +00:00
return
}
2024-05-07 08:51:17 +00:00
return (c.updated_at || c.created_at) > shoutLastSeen()
2023-02-21 12:32:51 +00:00
})
setNewReactions(newComments)
setCookie()
2023-02-11 10:49:31 +00:00
}
2023-02-17 09:21:02 +00:00
})
2024-05-06 18:45:17 +00:00
const [posting, setPosting] = createSignal(false)
2024-07-21 14:25:46 +00:00
const [commentsLoading, setCommentsLoading] = createSignal(false)
const [pagination, setPagination] = createSignal(0)
const loadMoreComments = async () => {
setCommentsLoading(true)
const next = pagination() + 1
const offset = next * COMMENTS_PER_PAGE
2024-07-22 11:24:36 +00:00
const rrr = await loadShoutComments(props.shout.id, COMMENTS_PER_PAGE, offset)
2024-07-21 14:25:46 +00:00
rrr && setPagination(next)
setCommentsLoading(false)
return rrr as LoadMoreItems
}
2024-07-22 05:41:10 +00:00
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)
}
2022-11-26 16:51:08 +00:00
return (
2023-02-28 17:13:14 +00:00
<>
<div class={styles.commentsHeaderWrapper}>
<h2 class={styles.commentsHeader}>
2023-02-28 17:13:14 +00:00
{t('Comments')} {comments().length.toString() || ''}
2023-03-06 14:06:48 +00:00
<Show when={newReactions().length > 0}>
2024-07-05 19:40:54 +00:00
<span class={styles.newReactions}>{` +${newReactions().length}`}</span>
2023-02-28 17:13:14 +00:00
</Show>
</h2>
2023-04-17 10:31:20 +00:00
<Show when={comments().length > 0}>
<ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}>
<Show when={newReactions().length > 0}>
2024-02-29 17:51:07 +00:00
<li classList={{ 'view-switcher__item--selected': onlyNew() }}>
2024-03-01 13:04:28 +00:00
<Button variant="light" value={t('New only')} onClick={() => setOnlyNew(!onlyNew())} />
2023-04-17 10:31:20 +00:00
</li>
</Show>
2024-02-29 17:51:07 +00:00
<li classList={{ 'view-switcher__item--selected': commentsOrder() === ReactionSort.Newest }}>
2023-02-07 12:48:45 +00:00
<Button
2023-05-22 22:01:04 +00:00
variant="light"
2023-04-17 10:31:20 +00:00
value={t('By time')}
2023-02-07 12:48:45 +00:00
onClick={() => {
2024-02-29 17:51:07 +00:00
setCommentsOrder(ReactionSort.Newest)
}}
2023-02-07 12:48:45 +00:00
/>
2022-11-26 21:27:54 +00:00
</li>
2024-02-29 17:51:07 +00:00
<li classList={{ 'view-switcher__item--selected': commentsOrder() === ReactionSort.Like }}>
2023-04-17 10:31:20 +00:00
<Button
2023-05-22 22:01:04 +00:00
variant="light"
2023-04-17 10:31:20 +00:00
value={t('By rating')}
onClick={() => {
2024-02-29 17:51:07 +00:00
setCommentsOrder(ReactionSort.Like)
2023-04-17 10:31:20 +00:00
}}
/>
</li>
</ul>
</Show>
2023-02-28 17:13:14 +00:00
</div>
2024-07-21 14:25:46 +00:00
<Show when={commentsLoading()}>
<InlineLoader />
</Show>
<LoadMoreWrapper
loadFunction={loadMoreComments}
pageSize={COMMENTS_PER_PAGE}
2024-07-22 05:41:10 +00:00
hidden={commentsLoading() || comments().length >= (props.shout?.stat?.commented || 0)}
2024-07-21 14:25:46 +00:00
>
<ul class={styles.comments}>
<For each={sortedComments().filter((r) => !r.reply_to)}>
{(reaction) => (
<Comment
sortedComments={sortedComments()}
2024-07-22 05:41:10 +00:00
isArticleAuthor={props.shout.authors?.some((a) => a && reaction.created_by.id === a.id)}
2024-07-21 14:25:46 +00:00
comment={reaction}
clickedReply={(id) => setClickedReplyId(id)}
clickedReplyId={clickedReplyId()}
lastSeen={shoutLastSeen()}
/>
)}
</For>
</ul>
</LoadMoreWrapper>
2023-02-28 17:13:14 +00:00
<ShowIfAuthenticated
fallback={
<div class={styles.signInMessage}>
2023-03-10 17:42:48 +00:00
{t('To write a comment, you must')}{' '}
2024-01-27 06:21:48 +00:00
<a href="?m=auth&mode=register" class={styles.link}>
2023-02-28 17:13:14 +00:00
{t('sign up')}
2023-03-10 17:42:48 +00:00
</a>{' '}
2024-07-05 19:40:54 +00:00
{t('or')}{' '}
2024-01-27 06:21:48 +00:00
<a href="?m=auth&mode=login" class={styles.link}>
2023-02-28 17:13:14 +00:00
{t('sign in')}
</a>
</div>
}
>
<SimplifiedEditor
quoteEnabled={true}
imageEnabled={true}
autoFocus={false}
submitByCtrlEnter={true}
2023-02-28 17:13:14 +00:00
placeholder={t('Write a comment...')}
onSubmit={(value) => handleSubmitComment(value)}
setClear={clearEditor()}
2024-05-06 18:45:17 +00:00
isPosting={posting()}
2023-02-28 17:13:14 +00:00
/>
</ShowIfAuthenticated>
</>
2022-11-26 16:51:08 +00:00
)
}