webapp/src/components/Article/Comment/Comment.tsx

280 lines
9.6 KiB
TypeScript
Raw Normal View History

import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
2024-02-04 11:25:21 +00:00
import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js'
import { useConfirm } from '../../../context/confirm'
2023-11-13 14:43:08 +00:00
import { useLocalize } from '../../../context/localize'
import { useReactions } from '../../../context/reactions'
import { useSession } from '../../../context/session'
2023-11-13 14:43:08 +00:00
import { useSnackbar } from '../../../context/snackbar'
2023-11-28 13:18:25 +00:00
import { Author, Reaction, ReactionKind } from '../../../graphql/schema/core.gen'
2023-11-13 14:43:08 +00:00
import { router } from '../../../stores/router'
2023-12-28 00:30:09 +00:00
import { AuthorLink } from '../../Author/AuthorLink'
import { Userpic } from '../../Author/Userpic'
2024-02-04 11:25:21 +00:00
import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl'
import styles from './Comment.module.scss'
2023-03-06 14:06:48 +00:00
2023-11-13 14:43:08 +00:00
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
2022-09-09 11:53:35 +00:00
type Props = {
comment: Reaction
2022-09-09 11:53:35 +00:00
compact?: boolean
isArticleAuthor?: boolean
2023-02-17 09:21:02 +00:00
sortedComments?: Reaction[]
2023-11-28 13:18:25 +00:00
lastSeen?: number
class?: string
showArticleLink?: boolean
clickedReply?: (id: number) => void
clickedReplyId?: number
onDelete?: (id: number) => void
}
2023-01-04 13:26:18 +00:00
export const Comment = (props: Props) => {
2023-02-17 09:21:02 +00:00
const { t } = useLocalize()
2022-11-26 21:27:54 +00:00
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
2023-10-12 14:18:01 +00:00
const [loading, setLoading] = createSignal(false)
const [editMode, setEditMode] = createSignal(false)
const [clearEditor, setClearEditor] = createSignal(false)
2024-03-04 10:47:11 +00:00
const [editedBody, setEditedBody] = createSignal<string>()
2024-02-16 10:21:25 +00:00
const { author, session } = useSession()
2024-02-04 17:40:15 +00:00
const { createReaction, deleteReaction, updateReaction } = useReactions()
const { showConfirm } = useConfirm()
const { showSnackbar } = useSnackbar()
2023-02-17 09:21:02 +00:00
2024-02-16 17:04:05 +00:00
const canEdit = createMemo(
() =>
Boolean(author()?.id) &&
(props.comment?.created_by?.slug === author()?.slug || session()?.user?.roles.includes('editor')),
2024-02-16 10:21:25 +00:00
)
2024-02-16 15:47:48 +00:00
2024-03-04 10:47:11 +00:00
const body = createMemo(() => (editedBody() ? editedBody().trim() : props.comment.body.trim() || ''))
2024-02-04 17:40:15 +00:00
const remove = async () => {
if (props.comment?.id) {
try {
const isConfirmed = await showConfirm({
confirmBody: t('Are you sure you want to delete this comment?'),
confirmButtonLabel: t('Delete'),
confirmButtonVariant: 'danger',
declineButtonVariant: 'primary',
})
if (isConfirmed) {
2024-03-07 07:20:50 +00:00
const { error } = await deleteReaction(props.comment.id)
const notificationType = error ? 'error' : 'success'
const notificationMessage = error
? t('Failed to delete comment')
: t('Comment successfully deleted')
await showSnackbar({ type: notificationType, body: notificationMessage })
if (!error && props.onDelete) {
props.onDelete(props.comment.id)
}
}
} catch (error) {
2024-03-07 07:20:50 +00:00
await showSnackbar({ body: 'error' })
console.error('[deleteReaction]', error)
}
}
}
const handleCreate = async (value) => {
try {
setLoading(true)
2023-02-17 09:21:02 +00:00
await createReaction({
kind: ReactionKind.Comment,
2023-11-28 13:18:25 +00:00
reply_to: props.comment.id,
2023-02-17 09:21:02 +00:00
body: value,
shout: props.comment.shout.id,
2023-02-17 09:21:02 +00:00
})
2023-10-12 14:18:01 +00:00
setClearEditor(true)
setIsReplyVisible(false)
setLoading(false)
} catch (error) {
console.error('[handleCreate reaction]:', error)
2022-09-09 11:53:35 +00:00
}
2023-10-12 14:18:01 +00:00
setClearEditor(false)
2022-09-09 11:53:35 +00:00
}
2023-02-07 12:48:45 +00:00
const toggleEditMode = () => {
setEditMode((oldEditMode) => !oldEditMode)
}
const handleUpdate = async (value) => {
setLoading(true)
try {
2024-03-04 10:47:11 +00:00
const reaction = await updateReaction({
2024-02-16 16:58:24 +00:00
id: props.comment.id,
2023-02-07 12:48:45 +00:00
kind: ReactionKind.Comment,
body: value,
shout: props.comment.shout.id,
2023-02-07 12:48:45 +00:00
})
2024-03-04 10:47:11 +00:00
if (reaction) {
setEditedBody(value)
}
2023-02-07 12:48:45 +00:00
setEditMode(false)
setLoading(false)
} catch (error) {
console.error('[handleCreate reaction]:', error)
}
}
2022-09-09 11:53:35 +00:00
return (
<li
id={`comment_${props.comment.id}`}
class={clsx(styles.comment, props.class, {
2024-05-07 08:51:17 +00:00
[styles.isNew]: props.lastSeen > (props.comment.updated_at || props.comment.created_at),
})}
>
2022-09-09 11:53:35 +00:00
<Show when={!!body()}>
2022-11-26 21:27:54 +00:00
<div class={styles.commentContent}>
2022-09-09 11:53:35 +00:00
<Show
when={!props.compact}
fallback={
2022-11-27 17:02:04 +00:00
<div>
2023-02-17 09:21:02 +00:00
<Userpic
name={props.comment.created_by.name}
userpic={props.comment.created_by.pic}
2023-02-17 09:21:02 +00:00
class={clsx({
[styles.compactUserpic]: props.compact,
2023-02-17 09:21:02 +00:00
})}
/>
<small>
<a href={`#comment_${props.comment?.id}`}>{props.comment?.shout.title || ''}</a>
2022-11-27 17:02:04 +00:00
</small>
2022-09-09 11:53:35 +00:00
</div>
}
>
2022-11-26 21:27:54 +00:00
<div class={styles.commentDetails}>
<div class={styles.commentAuthor}>
<AuthorLink author={props.comment?.created_by as Author} />
2022-09-09 11:53:35 +00:00
</div>
<Show when={props.isArticleAuthor}>
<div class={styles.articleAuthor}>{t('Author')}</div>
</Show>
<Show when={props.showArticleLink}>
<div class={styles.articleLink}>
<Icon name="arrow-right" class={styles.articleLinkIcon} />
<a
href={`${getPagePath(router, 'article', {
slug: props.comment.shout.slug,
})}?commentId=${props.comment.id}`}
>
{props.comment.shout.title}
</a>
</div>
</Show>
<CommentDate showOnHover={true} comment={props.comment} isShort={true} />
<CommentRatingControl comment={props.comment} />
2022-09-09 11:53:35 +00:00
</div>
</Show>
<div class={styles.commentBody}>
2023-11-13 17:14:58 +00:00
<Show when={editMode()} fallback={<div innerHTML={body()} />}>
2023-02-11 09:32:52 +00:00
<Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor
initialContent={editedBody() || props.comment.body}
submitButtonText={t('Save')}
quoteEnabled={true}
imageEnabled={true}
placeholder={t('Write a comment...')}
onSubmit={(value) => handleUpdate(value)}
submitByCtrlEnter={true}
2023-11-13 14:43:08 +00:00
onCancel={() => setEditMode(false)}
2023-10-12 14:18:01 +00:00
setClear={clearEditor()}
/>
2023-02-07 12:48:45 +00:00
</Suspense>
</Show>
2022-09-09 11:53:35 +00:00
</div>
<Show when={!props.compact}>
2023-11-13 14:43:08 +00:00
<div>
2023-02-17 09:21:02 +00:00
<ShowIfAuthenticated>
<button
disabled={loading()}
onClick={() => {
setIsReplyVisible(!isReplyVisible())
props.clickedReply(props.comment.id)
}}
2023-02-17 09:21:02 +00:00
class={clsx(styles.commentControl, styles.commentControlReply)}
>
<Icon name="reply" class={styles.icon} />
{loading() ? t('Loading') : t('Reply')}
</button>
</ShowIfAuthenticated>
2024-02-16 10:21:25 +00:00
<Show when={canEdit()}>
2023-02-07 12:48:45 +00:00
<button
class={clsx(styles.commentControl, styles.commentControlEdit)}
onClick={toggleEditMode}
>
<Icon name="edit" class={styles.icon} />
{t('Edit')}
</button>
2022-11-26 21:27:54 +00:00
<button
class={clsx(styles.commentControl, styles.commentControlDelete)}
onClick={() => remove()}
>
<Icon name="delete" class={styles.icon} />
2022-09-09 11:53:35 +00:00
{t('Delete')}
</button>
</Show>
{/*<SharePopup*/}
2023-12-09 18:35:08 +00:00
{/* title={'article.title'}*/}
{/* description={getDescription(body())}*/}
{/* containerCssClass={stylesHeader.control}*/}
{/* trigger={*/}
{/* <button class={clsx(styles.commentControl, styles.commentControlShare)}>*/}
{/* <Icon name="share" class={styles.icon} />*/}
{/* {t('Share')}*/}
{/* </button>*/}
{/* }*/}
{/*/>*/}
2022-11-26 21:27:54 +00:00
2022-09-09 11:53:35 +00:00
{/*<button*/}
2022-11-26 21:27:54 +00:00
{/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/}
2022-09-09 11:53:35 +00:00
{/* onClick={() => showModal('reportComment')}*/}
{/*>*/}
{/* {t('Report')}*/}
{/*</button>*/}
</div>
2022-11-26 21:27:54 +00:00
<Show when={isReplyVisible() && props.clickedReplyId === props.comment.id}>
2023-02-07 12:48:45 +00:00
<Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor
quoteEnabled={true}
imageEnabled={true}
placeholder={t('Write a comment...')}
2023-04-17 10:31:20 +00:00
onSubmit={(value) => handleCreate(value)}
submitByCtrlEnter={true}
2023-04-17 10:31:20 +00:00
/>
2023-02-07 12:48:45 +00:00
</Suspense>
2022-11-26 21:27:54 +00:00
</Show>
2022-09-09 11:53:35 +00:00
</Show>
</div>
</Show>
2023-02-17 09:21:02 +00:00
<Show when={props.sortedComments}>
<ul>
2023-11-28 13:18:25 +00:00
<For each={props.sortedComments.filter((r) => r.reply_to === props.comment.id)}>
2023-02-17 09:21:02 +00:00
{(c) => (
<Comment
2023-02-17 09:21:02 +00:00
sortedComments={props.sortedComments}
isArticleAuthor={props.isArticleAuthor}
2023-02-17 09:21:02 +00:00
comment={c}
2023-03-06 14:06:48 +00:00
lastSeen={props.lastSeen}
clickedReply={props.clickedReply}
clickedReplyId={props.clickedReplyId}
/>
)}
</For>
</ul>
</Show>
</li>
2022-09-09 11:53:35 +00:00
)
}