postmerge: loadmorewrappers
This commit is contained in:
parent
87d08dcb75
commit
0061b68257
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -5,10 +5,18 @@ import { useFeed } from '~/context/feed'
|
||||||
import { useLocalize } from '~/context/localize'
|
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 { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/core.gen'
|
import {
|
||||||
|
Author,
|
||||||
|
QueryLoad_Reactions_ByArgs,
|
||||||
|
Reaction,
|
||||||
|
ReactionKind,
|
||||||
|
ReactionSort
|
||||||
|
} 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'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
|
import { InlineLoader } from '../_shared/InlineLoader'
|
||||||
|
import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper'
|
||||||
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||||
import styles from './Article.module.scss'
|
import styles from './Article.module.scss'
|
||||||
import { Comment } from './Comment'
|
import { Comment } from './Comment'
|
||||||
|
@ -20,6 +28,7 @@ type Props = {
|
||||||
shoutSlug: string
|
shoutSlug: string
|
||||||
shoutId: number
|
shoutId: number
|
||||||
}
|
}
|
||||||
|
const COMMENTS_PER_PAGE = 50
|
||||||
|
|
||||||
export const CommentsTree = (props: Props) => {
|
export const CommentsTree = (props: Props) => {
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
|
@ -29,7 +38,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
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 } = useReactions()
|
const { reactionEntities, createReaction, loadReactionsBy, addReactions } = useReactions()
|
||||||
|
|
||||||
const comments = createMemo(() =>
|
const comments = createMemo(() =>
|
||||||
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
|
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
|
||||||
|
@ -89,6 +98,23 @@ export const CommentsTree = (props: Props) => {
|
||||||
setClearEditor(false)
|
setClearEditor(false)
|
||||||
setPosting(false)
|
setPosting(false)
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
const opts: QueryLoad_Reactions_ByArgs = {
|
||||||
|
by: { comment: true, shout: props.shoutSlug },
|
||||||
|
limit: COMMENTS_PER_PAGE,
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
const rrr = await loadReactionsBy(opts)
|
||||||
|
rrr && addReactions(rrr)
|
||||||
|
rrr && setPagination(next)
|
||||||
|
setCommentsLoading(false)
|
||||||
|
return rrr as LoadMoreItems
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -127,20 +153,31 @@ export const CommentsTree = (props: Props) => {
|
||||||
</ul>
|
</ul>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<ul class={styles.comments}>
|
<Show when={commentsLoading()}>
|
||||||
<For each={sortedComments().filter((r) => !r.reply_to)}>
|
<InlineLoader />
|
||||||
{(reaction) => (
|
</Show>
|
||||||
<Comment
|
<LoadMoreWrapper
|
||||||
sortedComments={sortedComments()}
|
loadFunction={loadMoreComments}
|
||||||
isArticleAuthor={Boolean(props.articleAuthors.some((a) => a?.id === reaction.created_by.id))}
|
pageSize={COMMENTS_PER_PAGE}
|
||||||
comment={reaction}
|
hidden={commentsLoading()}
|
||||||
clickedReply={(id) => setClickedReplyId(id)}
|
>
|
||||||
clickedReplyId={clickedReplyId()}
|
<ul class={styles.comments}>
|
||||||
lastSeen={shoutLastSeen()}
|
<For each={sortedComments().filter((r) => !r.reply_to)}>
|
||||||
/>
|
{(reaction) => (
|
||||||
)}
|
<Comment
|
||||||
</For>
|
sortedComments={sortedComments()}
|
||||||
</ul>
|
isArticleAuthor={Boolean(
|
||||||
|
props.articleAuthors.some((a) => a?.id === reaction.created_by.id)
|
||||||
|
)}
|
||||||
|
comment={reaction}
|
||||||
|
clickedReply={(id) => setClickedReplyId(id)}
|
||||||
|
clickedReplyId={clickedReplyId()}
|
||||||
|
lastSeen={shoutLastSeen()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</LoadMoreWrapper>
|
||||||
<ShowIfAuthenticated
|
<ShowIfAuthenticated
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.signInMessage}>
|
<div class={styles.signInMessage}>
|
||||||
|
|
|
@ -6,10 +6,9 @@ import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMou
|
||||||
import { isServer } from 'solid-js/web'
|
import { isServer } from 'solid-js/web'
|
||||||
import { useFeed } from '~/context/feed'
|
import { useFeed } from '~/context/feed'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useReactions } from '~/context/reactions'
|
|
||||||
import { useSession } from '~/context/session'
|
import { useSession } from '~/context/session'
|
||||||
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
|
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
|
||||||
import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
|
import { type Author, type Maybe, type Shout, type Topic } from '~/graphql/schema/core.gen'
|
||||||
import { processPrepositions } from '~/intl/prepositions'
|
import { processPrepositions } from '~/intl/prepositions'
|
||||||
import { isCyrillic } from '~/intl/translate'
|
import { isCyrillic } from '~/intl/translate'
|
||||||
import { getImageUrl } from '~/lib/getThumbUrl'
|
import { getImageUrl } from '~/lib/getThumbUrl'
|
||||||
|
@ -63,15 +62,11 @@ const scrollTo = (el: HTMLElement) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
|
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
|
||||||
const COMMENTS_PER_PAGE = 30
|
|
||||||
const VOTES_PER_PAGE = 50
|
|
||||||
|
|
||||||
export const FullArticle = (props: Props) => {
|
export const FullArticle = (props: Props) => {
|
||||||
const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>()
|
const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>()
|
||||||
const { showModal } = useUI()
|
const { showModal } = useUI()
|
||||||
const { loadReactionsBy } = useReactions()
|
|
||||||
const [selectedImage, setSelectedImage] = createSignal('')
|
const [selectedImage, setSelectedImage] = createSignal('')
|
||||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
|
||||||
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
||||||
const { t, formatDate, lang } = useLocalize()
|
const { t, formatDate, lang } = useLocalize()
|
||||||
const { session, requireAuthentication } = useSession()
|
const { session, requireAuthentication } = useSession()
|
||||||
|
@ -79,27 +74,6 @@ export const FullArticle = (props: Props) => {
|
||||||
const { addSeen } = useFeed()
|
const { addSeen } = useFeed()
|
||||||
const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000)))
|
const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000)))
|
||||||
|
|
||||||
const [pages, setPages] = createSignal<Record<string, number>>({})
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
pages,
|
|
||||||
async (p: Record<string, number>) => {
|
|
||||||
await loadReactionsBy({
|
|
||||||
by: { shout: props.article.slug, comment: true },
|
|
||||||
limit: COMMENTS_PER_PAGE,
|
|
||||||
offset: COMMENTS_PER_PAGE * p.comments || 0
|
|
||||||
})
|
|
||||||
await loadReactionsBy({
|
|
||||||
by: { shout: props.article.slug, rating: true },
|
|
||||||
limit: VOTES_PER_PAGE,
|
|
||||||
offset: VOTES_PER_PAGE * p.rating || 0
|
|
||||||
})
|
|
||||||
setIsReactionsLoaded(true)
|
|
||||||
},
|
|
||||||
{ defer: true }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const canEdit = createMemo(
|
const canEdit = createMemo(
|
||||||
() =>
|
() =>
|
||||||
Boolean(author()?.id) &&
|
Boolean(author()?.id) &&
|
||||||
|
@ -167,7 +141,7 @@ export const FullArticle = (props: Props) => {
|
||||||
let commentsRef: HTMLDivElement | undefined
|
let commentsRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (searchParams?.commentId && isReactionsLoaded()) {
|
if (searchParams?.commentId) {
|
||||||
const commentElement = document.querySelector<HTMLElement>(
|
const commentElement = document.querySelector<HTMLElement>(
|
||||||
`[id='comment_${searchParams?.commentId}']`
|
`[id='comment_${searchParams?.commentId}']`
|
||||||
)
|
)
|
||||||
|
@ -314,11 +288,8 @@ export const FullArticle = (props: Props) => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const [ratings, setRatings] = createSignal<Reaction[]>([])
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// install('G-LQ4B87H8C2')
|
|
||||||
await loadReactionsBy({ by: { shout: props.article.slug } })
|
|
||||||
addSeen(props.article.slug)
|
addSeen(props.article.slug)
|
||||||
document.title = props.article.title
|
document.title = props.article.title
|
||||||
updateIframeSizes()
|
updateIframeSizes()
|
||||||
|
@ -453,11 +424,7 @@ export const FullArticle = (props: Props) => {
|
||||||
<div class="col-md-16 offset-md-5">
|
<div class="col-md-16 offset-md-5">
|
||||||
<div class={styles.shoutStats}>
|
<div class={styles.shoutStats}>
|
||||||
<div class={styles.shoutStatsItem}>
|
<div class={styles.shoutStatsItem}>
|
||||||
<ShoutRatingControl
|
<ShoutRatingControl shout={props.article} class={styles.ratingControl} />
|
||||||
shout={props.article}
|
|
||||||
class={styles.ratingControl}
|
|
||||||
ratings={ratings()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popover content={t('Comment')} disabled={isActionPopupActive()}>
|
<Popover content={t('Comment')} disabled={isActionPopupActive()}>
|
||||||
|
@ -593,13 +560,11 @@ export const FullArticle = (props: Props) => {
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<div id="comments" ref={(el) => (commentsRef = el)}>
|
<div id="comments" ref={(el) => (commentsRef = el)}>
|
||||||
<Show when={isReactionsLoaded()}>
|
<CommentsTree
|
||||||
<CommentsTree
|
shoutId={props.article.id}
|
||||||
shoutId={props.article.id}
|
shoutSlug={props.article.slug}
|
||||||
shoutSlug={props.article.slug}
|
articleAuthors={props.article.authors as Author[]}
|
||||||
articleAuthors={props.article.authors as Author[]}
|
/>
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
|
import { useSearchParams } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
import { Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
||||||
|
import { byCreated } from '~/lib/sort'
|
||||||
import { useLocalize } from '../../context/localize'
|
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 { useSnackbar } from '../../context/snackbar'
|
import { useSnackbar } from '../../context/ui'
|
||||||
import { Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
|
import { QueryLoad_Reactions_ByArgs, Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
|
||||||
import { useRouter } from '../../stores/router'
|
|
||||||
import { loadShout } from '../../stores/zine/articles'
|
|
||||||
import { byCreated } from '../../utils/sortby'
|
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
import { InlineLoader } from '../_shared/InlineLoader'
|
||||||
|
import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper'
|
||||||
import { Popup } from '../_shared/Popup'
|
import { Popup } from '../_shared/Popup'
|
||||||
import { VotersList } from '../_shared/VotersList'
|
import { VotersList } from '../_shared/VotersList'
|
||||||
import stylesComment from './CommentRatingControl.module.scss'
|
import stylesComment from './CommentRatingControl.module.scss'
|
||||||
|
@ -18,38 +18,40 @@ import stylesShout from './ShoutRatingControl.module.scss'
|
||||||
interface RatingControlProps {
|
interface RatingControlProps {
|
||||||
shout?: Shout
|
shout?: Shout
|
||||||
comment?: Reaction
|
comment?: Reaction
|
||||||
ratings?: Reaction[]
|
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RatingControl = (props: RatingControlProps) => {
|
export const RatingControl = (props: RatingControlProps) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { changeSearchParams } = useRouter()
|
const [_, changeSearchParams] = useSearchParams()
|
||||||
const snackbar = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
const { author } = useSession()
|
const { session } = useSession()
|
||||||
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
|
const { addReactions } = useReactions()
|
||||||
|
const { reactionEntities, reactionsByShout, createReaction, deleteReaction, loadReactionsBy } =
|
||||||
|
useReactions()
|
||||||
|
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
|
||||||
|
const [ratingReactions, setRatingReactions] = createSignal<Reaction[]>([])
|
||||||
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
|
|
||||||
|
// reaction kind
|
||||||
const checkReaction = (reactionKind: ReactionKind) =>
|
const checkReaction = (reactionKind: ReactionKind) =>
|
||||||
Object.values(reactionEntities).some(
|
Object.values(reactionEntities).some(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === reactionKind &&
|
r.kind === reactionKind &&
|
||||||
r.created_by.slug === author()?.slug &&
|
r.created_by.slug === session()?.user?.app_data?.profile?.slug &&
|
||||||
r.shout.id === props.comment.shout.id &&
|
r.shout.id === props.comment?.shout.id &&
|
||||||
r.reply_to === props.comment.id,
|
r.reply_to === props.comment?.id
|
||||||
)
|
)
|
||||||
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
||||||
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
||||||
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
|
|
||||||
const [total, setTotal] = createSignal(props.comment?.stat?.rating || props.shout?.stat?.rating || 0)
|
|
||||||
|
|
||||||
const [ratingReactions, setRatingReactions] = createSignal<Reaction[]>([])
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const shout = props.comment.shout.id || props.shout.id
|
const shout = props.comment?.shout.id || props.shout?.id
|
||||||
if (shout && !ratingReactions()) {
|
if (shout && !ratingReactions()) {
|
||||||
let result = Object.values(reactionEntities).filter(
|
let result = Object.values(reactionEntities).filter(
|
||||||
(r) => [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && r.shout.id === shout,
|
(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)
|
if (props.comment?.id) result = result.filter((r) => r.reply_to === props.comment?.id)
|
||||||
setRatingReactions(result)
|
setRatingReactions(result)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -58,14 +60,14 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
const reactionToDelete = Object.values(reactionEntities).find(
|
const reactionToDelete = Object.values(reactionEntities).find(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === reactionKind &&
|
r.kind === reactionKind &&
|
||||||
r.created_by.slug === author()?.slug &&
|
r.created_by.slug === session()?.user?.nickname &&
|
||||||
r.shout.id === props.comment.shout.id &&
|
r.shout.id === props.comment?.shout.id &&
|
||||||
r.reply_to === props.comment.id,
|
r.reply_to === props.comment?.id
|
||||||
)
|
)
|
||||||
return deleteReaction(reactionToDelete.id)
|
return reactionToDelete && deleteReaction(reactionToDelete.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
// rating change
|
||||||
const handleRatingChange = async (isUpvote: boolean) => {
|
const handleRatingChange = async (isUpvote: boolean) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
|
@ -74,63 +76,33 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
} else if (isDownvoted()) {
|
} else if (isDownvoted()) {
|
||||||
await deleteRating(ReactionKind.Dislike)
|
await deleteRating(ReactionKind.Dislike)
|
||||||
} else {
|
} else {
|
||||||
await createReaction({
|
props.comment?.shout.id &&
|
||||||
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
(await createReaction({
|
||||||
shout: props.comment.shout.id,
|
reaction: {
|
||||||
reply_to: props.comment.id,
|
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
||||||
})
|
shout: props.comment.shout.id,
|
||||||
|
reply_to: props.comment?.id
|
||||||
|
}
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
snackbar?.showSnackbar({ type: 'error', body: t('Error') })
|
snackbar?.showSnackbar({ type: 'error', body: t('Error') })
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadShout(props.comment.shout.slug)
|
if (props.comment?.shout.slug) {
|
||||||
await loadReactionsBy({
|
const rrr = await loadReactionsBy({ by: { shout: props.comment.shout.slug } })
|
||||||
by: { shout: props.comment.shout.slug },
|
addReactions(rrr)
|
||||||
})
|
}
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
const total = createMemo<number>(() =>
|
||||||
on(
|
props.comment?.stat?.rating ? props.comment.stat.rating : props.shout?.stat?.rating || 0
|
||||||
() => props.comment,
|
|
||||||
(comment) => {
|
|
||||||
if (comment) {
|
|
||||||
setTotal(comment?.stat?.rating)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.shout,
|
[ratingReactions, () => session()?.user?.app_data?.profile],
|
||||||
(shout) => {
|
|
||||||
if (shout) {
|
|
||||||
setTotal(shout.stat?.rating)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => reactionEntities,
|
|
||||||
(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)
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
[ratingReactions, author],
|
|
||||||
([reactions, me]) => {
|
([reactions, me]) => {
|
||||||
console.debug('[RatingControl] on reactions update')
|
console.debug('[RatingControl] on reactions update')
|
||||||
const ratingVotes = Object.values(reactions).filter((r) => !r.reply_to)
|
const ratingVotes = Object.values(reactions).filter((r) => !r.reply_to)
|
||||||
|
@ -138,8 +110,8 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
const myReaction = reactions.find((r) => r.created_by.id === me?.id)
|
const myReaction = reactions.find((r) => r.created_by.id === me?.id)
|
||||||
setMyRate((_) => myReaction)
|
setMyRate((_) => myReaction)
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true }
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const getTrigger = createMemo(() => {
|
const getTrigger = createMemo(() => {
|
||||||
|
@ -148,48 +120,78 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
class={clsx(stylesComment.commentRatingValue, {
|
class={clsx(stylesComment.commentRatingValue, {
|
||||||
[stylesComment.commentRatingPositive]: total() > 0 && Boolean(props.comment?.id),
|
[stylesComment.commentRatingPositive]: total() > 0 && Boolean(props.comment?.id),
|
||||||
[stylesComment.commentRatingNegative]: total() < 0 && Boolean(props.comment?.id),
|
[stylesComment.commentRatingNegative]: total() < 0 && Boolean(props.comment?.id),
|
||||||
[stylesShout.ratingValue]: !props.comment?.id,
|
[stylesShout.ratingValue]: !props.comment?.id
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{total()}
|
{total()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
const VOTERS_PER_PAGE = 10
|
||||||
|
const [ratingPage, setRatingPage] = createSignal(0)
|
||||||
|
const [ratingLoading, setRatingLoading] = createSignal(false) // FIXME: use loading indication
|
||||||
|
const ratings = createMemo(() =>
|
||||||
|
props.shout
|
||||||
|
? reactionsByShout[props.shout?.slug]?.filter(
|
||||||
|
(r) => r.kind === ReactionKind.Like || r.kind === ReactionKind.Dislike
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
const loadMoreReactions = async () => {
|
||||||
|
setRatingLoading(true)
|
||||||
|
const next = ratingPage() + 1
|
||||||
|
const offset = VOTERS_PER_PAGE * next
|
||||||
|
const opts: QueryLoad_Reactions_ByArgs = {
|
||||||
|
by: { rating: true, shout: props.shout?.slug },
|
||||||
|
limit: VOTERS_PER_PAGE,
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
const rrr = await loadReactionsBy(opts)
|
||||||
|
rrr && addReactions(rrr)
|
||||||
|
rrr && setRatingPage(next)
|
||||||
|
setRatingLoading(false)
|
||||||
|
return rrr as LoadMoreItems
|
||||||
|
}
|
||||||
return props.comment?.id ? (
|
return props.comment?.id ? (
|
||||||
<div class={stylesComment.commentRating}>
|
<div class={stylesComment.commentRating}>
|
||||||
<button
|
<button
|
||||||
role="button"
|
role="button"
|
||||||
disabled={!author()}
|
disabled={!session()?.user?.app_data?.profile}
|
||||||
onClick={() => handleRatingChange(true)}
|
onClick={() => handleRatingChange(true)}
|
||||||
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
|
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
|
||||||
[stylesComment.voted]: isUpvoted(),
|
[stylesComment.voted]: isUpvoted()
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Popup
|
<Popup
|
||||||
trigger={
|
trigger={
|
||||||
<div
|
<div
|
||||||
class={clsx(stylesComment.commentRatingValue, {
|
class={clsx(stylesComment.commentRatingValue, {
|
||||||
[stylesComment.commentRatingPositive]: props.comment.stat.rating > 0,
|
[stylesComment.commentRatingPositive]: (props.comment?.stat?.rating || 0) > 0,
|
||||||
[stylesComment.commentRatingNegative]: props.comment.stat.rating < 0,
|
[stylesComment.commentRatingNegative]: (props.comment?.stat?.rating || 0) < 0
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{props.comment.stat.rating || 0}
|
{props.comment?.stat?.rating || 0}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
variant="tiny"
|
variant="tiny"
|
||||||
>
|
>
|
||||||
<VotersList
|
<Show when={ratingLoading()}>
|
||||||
reactions={ratingReactions()}
|
<InlineLoader />
|
||||||
fallbackMessage={t('This comment has not yet been rated')}
|
</Show>
|
||||||
/>
|
<LoadMoreWrapper
|
||||||
|
loadFunction={loadMoreReactions}
|
||||||
|
pageSize={VOTERS_PER_PAGE}
|
||||||
|
hidden={ratingLoading()}
|
||||||
|
>
|
||||||
|
<VotersList reactions={ratings()} fallbackMessage={t('This comment has not been rated yet')} />
|
||||||
|
</LoadMoreWrapper>
|
||||||
</Popup>
|
</Popup>
|
||||||
<button
|
<button
|
||||||
role="button"
|
role="button"
|
||||||
disabled={!author()}
|
disabled={!session()?.user?.app_data?.profile}
|
||||||
onClick={() => handleRatingChange(false)}
|
onClick={() => handleRatingChange(false)}
|
||||||
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
|
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
|
||||||
[stylesComment.voted]: isDownvoted(),
|
[stylesComment.voted]: isDownvoted()
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -201,7 +203,7 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
class={
|
class={
|
||||||
props.comment
|
props.comment
|
||||||
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
|
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
|
||||||
[stylesComment.voted]: myRate()?.kind === 'LIKE',
|
[stylesComment.voted]: myRate()?.kind === 'LIKE'
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
@ -215,10 +217,10 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
</button>
|
</button>
|
||||||
<Popup trigger={getTrigger()} variant="tiny">
|
<Popup trigger={getTrigger()} variant="tiny">
|
||||||
<Show
|
<Show
|
||||||
when={author()}
|
when={!!session()?.user?.app_data?.profile}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<span class="link" onClick={() => changeSearchParams({ mode: 'login', modal: 'auth' })}>
|
<span class="link" onClick={() => changeSearchParams({ mode: 'login', m: 'auth' })}>
|
||||||
{t('Enter')}
|
{t('Enter')}
|
||||||
</span>
|
</span>
|
||||||
{lang() === 'ru' ? ', ' : ' '}
|
{lang() === 'ru' ? ', ' : ' '}
|
||||||
|
@ -238,7 +240,7 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
class={
|
class={
|
||||||
props.comment
|
props.comment
|
||||||
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
|
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
|
||||||
[stylesComment.voted]: myRate()?.kind === 'DISLIKE',
|
[stylesComment.voted]: myRate()?.kind === 'DISLIKE'
|
||||||
})
|
})
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -456,7 +456,7 @@
|
||||||
"Theory": "Теории",
|
"Theory": "Теории",
|
||||||
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
|
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
|
||||||
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
|
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
|
||||||
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
|
"This comment has not been rated yet": "Этот комментарий еще пока никто не оценил",
|
||||||
"This content is not published yet": "Содержимое ещё не опубликовано",
|
"This content is not published yet": "Содержимое ещё не опубликовано",
|
||||||
"This email is": "Этот email",
|
"This email is": "Этот email",
|
||||||
"This email is not verified": "Этот email не подтвержден",
|
"This email is not verified": "Этот email не подтвержден",
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
Loading…
Reference in New Issue
Block a user