2023-03-03 18:26:26 +00:00
|
|
|
import { clsx } from 'clsx'
|
2024-05-06 20:26:16 +00:00
|
|
|
import { Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
|
|
|
import { useLocalize } from '../../context/localize'
|
|
|
|
import { useReactions } from '../../context/reactions'
|
|
|
|
import { useSession } from '../../context/session'
|
2024-05-06 20:26:16 +00:00
|
|
|
import { useSnackbar } from '../../context/snackbar'
|
2024-02-15 12:51:04 +00:00
|
|
|
import { Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
|
2024-05-06 20:26:16 +00:00
|
|
|
import { useRouter } from '../../stores/router'
|
2023-03-03 18:26:26 +00:00
|
|
|
import { loadShout } from '../../stores/zine/articles'
|
2024-02-07 16:54:52 +00:00
|
|
|
import { byCreated } from '../../utils/sortby'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { Icon } from '../_shared/Icon'
|
2023-03-03 18:26:26 +00:00
|
|
|
import { Popup } from '../_shared/Popup'
|
2023-03-09 23:56:19 +00:00
|
|
|
import { VotersList } from '../_shared/VotersList'
|
2024-02-15 12:51:04 +00:00
|
|
|
import stylesComment from './CommentRatingControl.module.scss'
|
|
|
|
import stylesShout from './ShoutRatingControl.module.scss'
|
|
|
|
|
|
|
|
interface RatingControlProps {
|
|
|
|
shout?: Shout
|
|
|
|
comment?: Reaction
|
2024-02-07 16:54:52 +00:00
|
|
|
ratings?: Reaction[]
|
2023-03-03 18:26:26 +00:00
|
|
|
class?: string
|
|
|
|
}
|
|
|
|
|
2024-02-15 12:51:04 +00:00
|
|
|
export const RatingControl = (props: RatingControlProps) => {
|
2024-02-16 11:29:27 +00:00
|
|
|
const { t, lang } = useLocalize()
|
|
|
|
const { changeSearchParams } = useRouter()
|
2024-05-06 20:26:16 +00:00
|
|
|
const snackbar = useSnackbar()
|
|
|
|
const { author } = useSession()
|
2024-03-03 15:10:30 +00:00
|
|
|
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
|
2024-05-06 20:26:16 +00:00
|
|
|
|
|
|
|
const checkReaction = (reactionKind: ReactionKind) =>
|
|
|
|
Object.values(reactionEntities).some(
|
|
|
|
(r) =>
|
|
|
|
r.kind === reactionKind &&
|
|
|
|
r.created_by.slug === author()?.slug &&
|
|
|
|
r.shout.id === props.comment.shout.id &&
|
|
|
|
r.reply_to === props.comment.id,
|
|
|
|
)
|
|
|
|
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
|
|
|
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
2024-02-07 16:54:52 +00:00
|
|
|
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
|
2024-05-06 20:26:16 +00:00
|
|
|
const [total, setTotal] = createSignal(props.comment?.stat?.rating || props.shout?.stat?.rating || 0)
|
|
|
|
|
|
|
|
const [ratingReactions, setRatingReactions] = createSignal<Reaction[]>([])
|
|
|
|
createEffect(() => {
|
|
|
|
const shout = props.comment.shout.id || props.shout.id
|
|
|
|
if (shout && !ratingReactions()) {
|
|
|
|
let result = Object.values(reactionEntities).filter(
|
|
|
|
(r) => [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && r.shout.id === shout,
|
|
|
|
)
|
|
|
|
if (props.comment?.id) result = result.filter((r) => r.reply_to === props.comment.id)
|
|
|
|
setRatingReactions(result)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const deleteRating = async (reactionKind: ReactionKind) => {
|
|
|
|
const reactionToDelete = Object.values(reactionEntities).find(
|
|
|
|
(r) =>
|
|
|
|
r.kind === reactionKind &&
|
|
|
|
r.created_by.slug === author()?.slug &&
|
|
|
|
r.shout.id === props.comment.shout.id &&
|
|
|
|
r.reply_to === props.comment.id,
|
|
|
|
)
|
|
|
|
return deleteReaction(reactionToDelete.id)
|
|
|
|
}
|
|
|
|
|
|
|
|
const [isLoading, setIsLoading] = createSignal(false)
|
|
|
|
const handleRatingChange = async (isUpvote: boolean) => {
|
|
|
|
setIsLoading(true)
|
|
|
|
try {
|
|
|
|
if (isUpvoted()) {
|
|
|
|
await deleteRating(ReactionKind.Like)
|
|
|
|
} else if (isDownvoted()) {
|
|
|
|
await deleteRating(ReactionKind.Dislike)
|
|
|
|
} else {
|
|
|
|
await createReaction({
|
|
|
|
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
|
|
|
shout: props.comment.shout.id,
|
|
|
|
reply_to: props.comment.id,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
snackbar?.showSnackbar({ type: 'error', body: t('Error') })
|
|
|
|
}
|
|
|
|
|
|
|
|
await loadShout(props.comment.shout.slug)
|
|
|
|
await loadReactionsBy({
|
|
|
|
by: { shout: props.comment.shout.slug },
|
|
|
|
})
|
|
|
|
setIsLoading(false)
|
|
|
|
}
|
2024-02-15 13:41:14 +00:00
|
|
|
|
|
|
|
createEffect(
|
|
|
|
on(
|
2024-02-16 08:01:40 +00:00
|
|
|
() => props.comment,
|
|
|
|
(comment) => {
|
2024-02-16 08:29:06 +00:00
|
|
|
if (comment) {
|
|
|
|
setTotal(comment?.stat?.rating)
|
|
|
|
}
|
2024-02-16 08:01:40 +00:00
|
|
|
},
|
|
|
|
{ defer: true },
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
createEffect(
|
|
|
|
on(
|
|
|
|
() => props.shout,
|
|
|
|
(shout) => {
|
2024-02-16 08:29:06 +00:00
|
|
|
if (shout) {
|
2024-02-16 18:47:42 +00:00
|
|
|
setTotal(shout.stat?.rating)
|
2024-02-16 08:29:06 +00:00
|
|
|
}
|
2024-02-15 13:41:14 +00:00
|
|
|
},
|
|
|
|
{ defer: true },
|
|
|
|
),
|
|
|
|
)
|
2024-03-03 16:39:25 +00:00
|
|
|
createEffect(
|
|
|
|
on(
|
2024-03-07 12:52:03 +00:00
|
|
|
() => reactionEntities,
|
2024-03-03 17:06:58 +00:00
|
|
|
(reactions) => {
|
|
|
|
const ratings = Object.values(reactions).filter((r) => !r?.reply_to)
|
|
|
|
const likes = ratings.filter((rating) => rating.kind === 'LIKE').length
|
|
|
|
const dislikes = ratings.filter((rating) => rating.kind === 'DISLIKE').length
|
|
|
|
const total = likes - dislikes
|
|
|
|
setTotal(total)
|
2024-03-03 16:39:25 +00:00
|
|
|
},
|
2024-03-03 17:06:58 +00:00
|
|
|
{ defer: true },
|
|
|
|
),
|
|
|
|
)
|
2024-01-23 14:41:49 +00:00
|
|
|
|
2024-02-07 16:54:52 +00:00
|
|
|
createEffect(
|
|
|
|
on(
|
2024-05-06 20:26:16 +00:00
|
|
|
[ratingReactions, author],
|
2024-02-07 16:54:52 +00:00
|
|
|
([reactions, me]) => {
|
2024-03-03 17:06:58 +00:00
|
|
|
console.debug('[RatingControl] on reactions update')
|
|
|
|
const ratingVotes = Object.values(reactions).filter((r) => !r.reply_to)
|
2024-05-06 20:26:16 +00:00
|
|
|
setRatingReactions((_) => ratingVotes.sort(byCreated))
|
2024-03-03 17:06:58 +00:00
|
|
|
const myReaction = reactions.find((r) => r.created_by.id === me?.id)
|
|
|
|
setMyRate((_) => myReaction)
|
2024-02-07 16:54:52 +00:00
|
|
|
},
|
|
|
|
{ defer: true },
|
2024-01-23 14:41:49 +00:00
|
|
|
),
|
2024-03-03 17:06:58 +00:00
|
|
|
)
|
2024-01-23 14:41:49 +00:00
|
|
|
|
2024-02-16 08:01:40 +00:00
|
|
|
const getTrigger = createMemo(() => {
|
2024-05-06 20:26:16 +00:00
|
|
|
return (
|
2024-02-15 12:51:04 +00:00
|
|
|
<div
|
|
|
|
class={clsx(stylesComment.commentRatingValue, {
|
2024-05-06 20:26:16 +00:00
|
|
|
[stylesComment.commentRatingPositive]: total() > 0 && Boolean(props.comment?.id),
|
|
|
|
[stylesComment.commentRatingNegative]: total() < 0 && Boolean(props.comment?.id),
|
|
|
|
[stylesShout.ratingValue]: !props.comment?.id,
|
2024-02-15 12:51:04 +00:00
|
|
|
})}
|
|
|
|
>
|
2024-05-06 20:26:16 +00:00
|
|
|
{total()}
|
2024-02-15 12:51:04 +00:00
|
|
|
</div>
|
|
|
|
)
|
2024-02-16 08:01:40 +00:00
|
|
|
})
|
|
|
|
|
2024-05-06 20:26:16 +00:00
|
|
|
return props.comment?.id ? (
|
|
|
|
<div class={stylesComment.commentRating}>
|
|
|
|
<button
|
|
|
|
role="button"
|
|
|
|
disabled={!author()}
|
|
|
|
onClick={() => handleRatingChange(true)}
|
|
|
|
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
|
|
|
|
[stylesComment.voted]: isUpvoted(),
|
|
|
|
})}
|
|
|
|
/>
|
|
|
|
<Popup
|
|
|
|
trigger={
|
|
|
|
<div
|
|
|
|
class={clsx(stylesComment.commentRatingValue, {
|
|
|
|
[stylesComment.commentRatingPositive]: props.comment.stat.rating > 0,
|
|
|
|
[stylesComment.commentRatingNegative]: props.comment.stat.rating < 0,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
{props.comment.stat.rating || 0}
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
variant="tiny"
|
|
|
|
>
|
|
|
|
<VotersList
|
|
|
|
reactions={ratingReactions()}
|
|
|
|
fallbackMessage={t('This comment has not yet been rated')}
|
|
|
|
/>
|
|
|
|
</Popup>
|
|
|
|
<button
|
|
|
|
role="button"
|
|
|
|
disabled={!author()}
|
|
|
|
onClick={() => handleRatingChange(false)}
|
|
|
|
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
|
|
|
|
[stylesComment.voted]: isDownvoted(),
|
|
|
|
})}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : (
|
2024-02-15 12:51:04 +00:00
|
|
|
<div class={clsx(props.comment ? stylesComment.commentRating : stylesShout.rating, props.class)}>
|
|
|
|
<button
|
2024-05-06 20:26:16 +00:00
|
|
|
onClick={() => handleRatingChange(false)}
|
2024-02-15 12:51:04 +00:00
|
|
|
disabled={isLoading()}
|
|
|
|
class={
|
2024-02-16 08:02:00 +00:00
|
|
|
props.comment
|
|
|
|
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
|
|
|
|
[stylesComment.voted]: myRate()?.kind === 'LIKE',
|
|
|
|
})
|
|
|
|
: ''
|
2024-02-15 12:51:04 +00:00
|
|
|
}
|
|
|
|
>
|
|
|
|
<Show when={!props.comment}>
|
|
|
|
<Icon
|
2024-05-06 20:26:16 +00:00
|
|
|
name={isDownvoted() ? 'rating-control-checked' : 'rating-control-less'}
|
2024-02-15 12:51:04 +00:00
|
|
|
class={isLoading() ? 'rotating' : ''}
|
|
|
|
/>
|
|
|
|
</Show>
|
2023-03-03 18:26:26 +00:00
|
|
|
</button>
|
2024-02-15 12:51:04 +00:00
|
|
|
<Popup trigger={getTrigger()} variant="tiny">
|
2024-02-16 11:29:27 +00:00
|
|
|
<Show
|
|
|
|
when={author()}
|
|
|
|
fallback={
|
|
|
|
<>
|
2024-02-16 11:30:29 +00:00
|
|
|
<span class="link" onClick={() => changeSearchParams({ mode: 'login', modal: 'auth' })}>
|
2024-02-16 11:29:27 +00:00
|
|
|
{t('Enter')}
|
|
|
|
</span>
|
|
|
|
{lang() === 'ru' ? ', ' : ' '}
|
|
|
|
{t('to see the voters')}
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
>
|
2024-02-16 10:59:32 +00:00
|
|
|
<VotersList
|
2024-05-06 20:26:16 +00:00
|
|
|
reactions={ratingReactions()}
|
2024-02-16 10:59:32 +00:00
|
|
|
fallbackMessage={isLoading() ? t('Loading') : t('No one rated yet')}
|
|
|
|
/>
|
|
|
|
</Show>
|
2023-03-03 18:26:26 +00:00
|
|
|
</Popup>
|
2024-02-15 12:51:04 +00:00
|
|
|
<button
|
2024-05-06 20:26:16 +00:00
|
|
|
onClick={() => handleRatingChange(true)}
|
2024-02-15 12:51:04 +00:00
|
|
|
disabled={isLoading()}
|
|
|
|
class={
|
2024-02-16 08:02:00 +00:00
|
|
|
props.comment
|
|
|
|
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
|
|
|
|
[stylesComment.voted]: myRate()?.kind === 'DISLIKE',
|
|
|
|
})
|
|
|
|
: ''
|
2024-02-15 12:51:04 +00:00
|
|
|
}
|
|
|
|
>
|
|
|
|
<Show when={!props.comment}>
|
|
|
|
<Icon
|
2024-05-06 20:26:16 +00:00
|
|
|
name={isUpvoted() ? 'rating-control-checked' : 'rating-control-more'}
|
2024-02-15 12:51:04 +00:00
|
|
|
class={isLoading() ? 'rotating' : ''}
|
|
|
|
/>
|
|
|
|
</Show>
|
2023-03-03 18:26:26 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|