diff --git a/src/components/Article/CommentRatingControl.tsx b/src/components/Article/CommentRatingControl.tsx deleted file mode 100644 index 8b137891..00000000 --- a/src/components/Article/CommentRatingControl.tsx +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 13fffae5..131ee86d 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -5,10 +5,18 @@ import { useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' 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 { SortFunction } from '~/types/common' import { Button } from '../_shared/Button' +import { InlineLoader } from '../_shared/InlineLoader' +import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import styles from './Article.module.scss' import { Comment } from './Comment' @@ -20,6 +28,7 @@ type Props = { shoutSlug: string shoutId: number } +const COMMENTS_PER_PAGE = 50 export const CommentsTree = (props: Props) => { const { session } = useSession() @@ -29,7 +38,7 @@ export const CommentsTree = (props: Props) => { const [newReactions, setNewReactions] = createSignal([]) const [clearEditor, setClearEditor] = createSignal(false) const [clickedReplyId, setClickedReplyId] = createSignal() - const { reactionEntities, createReaction, loadReactionsBy } = useReactions() + const { reactionEntities, createReaction, loadReactionsBy, addReactions } = useReactions() const comments = createMemo(() => Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') @@ -89,6 +98,23 @@ export const CommentsTree = (props: Props) => { setClearEditor(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 ( <> @@ -127,20 +153,31 @@ export const CommentsTree = (props: Props) => { - + + + + diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 4fa83ef9..85c1b1d4 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -6,10 +6,9 @@ import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMou import { isServer } from 'solid-js/web' import { useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' -import { useReactions } from '~/context/reactions' import { useSession } from '~/context/session' 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 { isCyrillic } from '~/intl/translate' import { getImageUrl } from '~/lib/getThumbUrl' @@ -63,15 +62,11 @@ const scrollTo = (el: HTMLElement) => { } const imgSrcRegExp = /]+src\s*=\s*["']([^"']+)["']/gi -const COMMENTS_PER_PAGE = 30 -const VOTES_PER_PAGE = 50 export const FullArticle = (props: Props) => { const [searchParams, changeSearchParams] = useSearchParams() const { showModal } = useUI() - const { loadReactionsBy } = useReactions() const [selectedImage, setSelectedImage] = createSignal('') - const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const { t, formatDate, lang } = useLocalize() const { session, requireAuthentication } = useSession() @@ -79,27 +74,6 @@ export const FullArticle = (props: Props) => { const { addSeen } = useFeed() const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000))) - const [pages, setPages] = createSignal>({}) - createEffect( - on( - pages, - async (p: Record) => { - 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( () => Boolean(author()?.id) && @@ -167,7 +141,7 @@ export const FullArticle = (props: Props) => { let commentsRef: HTMLDivElement | undefined createEffect(() => { - if (searchParams?.commentId && isReactionsLoaded()) { + if (searchParams?.commentId) { const commentElement = document.querySelector( `[id='comment_${searchParams?.commentId}']` ) @@ -314,11 +288,8 @@ export const FullArticle = (props: Props) => { } ) ) - const [ratings, setRatings] = createSignal([]) onMount(async () => { - // install('G-LQ4B87H8C2') - await loadReactionsBy({ by: { shout: props.article.slug } }) addSeen(props.article.slug) document.title = props.article.title updateIframeSizes() @@ -453,11 +424,7 @@ export const FullArticle = (props: Props) => {
- +
@@ -593,13 +560,11 @@ export const FullArticle = (props: Props) => {
(commentsRef = el)}> - - - +
diff --git a/src/components/Article/RatingControl.tsx b/src/components/Article/RatingControl.tsx index 8ef3ab50..31ca8821 100644 --- a/src/components/Article/RatingControl.tsx +++ b/src/components/Article/RatingControl.tsx @@ -1,15 +1,15 @@ +import { useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' import { Show, createEffect, createMemo, createSignal, on } from 'solid-js' - +import { byCreated } from '~/lib/sort' import { useLocalize } from '../../context/localize' import { useReactions } from '../../context/reactions' import { useSession } from '../../context/session' -import { useSnackbar } from '../../context/snackbar' -import { 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 { useSnackbar } from '../../context/ui' +import { QueryLoad_Reactions_ByArgs, Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen' import { Icon } from '../_shared/Icon' +import { InlineLoader } from '../_shared/InlineLoader' +import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper' import { Popup } from '../_shared/Popup' import { VotersList } from '../_shared/VotersList' import stylesComment from './CommentRatingControl.module.scss' @@ -18,38 +18,40 @@ import stylesShout from './ShoutRatingControl.module.scss' interface RatingControlProps { shout?: Shout comment?: Reaction - ratings?: Reaction[] class?: string } export const RatingControl = (props: RatingControlProps) => { const { t, lang } = useLocalize() - const { changeSearchParams } = useRouter() + const [_, changeSearchParams] = useSearchParams() const snackbar = useSnackbar() - const { author } = useSession() - const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() + const { session } = useSession() + const { addReactions } = useReactions() + const { reactionEntities, reactionsByShout, createReaction, deleteReaction, loadReactionsBy } = + useReactions() + const [myRate, setMyRate] = createSignal() + const [ratingReactions, setRatingReactions] = createSignal([]) + const [isLoading, setIsLoading] = createSignal(false) + // reaction kind 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, + r.created_by.slug === session()?.user?.app_data?.profile?.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)) - const [myRate, setMyRate] = createSignal() - const [total, setTotal] = createSignal(props.comment?.stat?.rating || props.shout?.stat?.rating || 0) - const [ratingReactions, setRatingReactions] = createSignal([]) createEffect(() => { - const shout = props.comment.shout.id || props.shout.id + 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, + (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) } }) @@ -58,14 +60,14 @@ export const RatingControl = (props: RatingControlProps) => { 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, + r.created_by.slug === session()?.user?.nickname && + r.shout.id === props.comment?.shout.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) => { setIsLoading(true) try { @@ -74,63 +76,33 @@ export const RatingControl = (props: RatingControlProps) => { } 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, - }) + props.comment?.shout.id && + (await createReaction({ + reaction: { + 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 }, - }) + if (props.comment?.shout.slug) { + const rrr = await loadReactionsBy({ by: { shout: props.comment.shout.slug } }) + addReactions(rrr) + } setIsLoading(false) } - createEffect( - on( - () => props.comment, - (comment) => { - if (comment) { - setTotal(comment?.stat?.rating) - } - }, - { defer: true }, - ), + const total = createMemo(() => + props.comment?.stat?.rating ? props.comment.stat.rating : props.shout?.stat?.rating || 0 ) createEffect( on( - () => props.shout, - (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], + [ratingReactions, () => session()?.user?.app_data?.profile], ([reactions, me]) => { console.debug('[RatingControl] on reactions update') 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) setMyRate((_) => myReaction) }, - { defer: true }, - ), + { defer: true } + ) ) const getTrigger = createMemo(() => { @@ -148,48 +120,78 @@ export const RatingControl = (props: RatingControlProps) => { class={clsx(stylesComment.commentRatingValue, { [stylesComment.commentRatingPositive]: 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()} ) }) - + 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 ? (
} variant="tiny" > - + + + + - changeSearchParams({ mode: 'login', modal: 'auth' })}> + changeSearchParams({ mode: 'login', m: 'auth' })}> {t('Enter')} {lang() === 'ru' ? ', ' : ' '} @@ -238,7 +240,7 @@ export const RatingControl = (props: RatingControlProps) => { class={ props.comment ? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, { - [stylesComment.voted]: myRate()?.kind === 'DISLIKE', + [stylesComment.voted]: myRate()?.kind === 'DISLIKE' }) : '' } diff --git a/src/components/Article/ShoutRatingControl.tsx b/src/components/Article/ShoutRatingControl.tsx deleted file mode 100644 index 8b137891..00000000 --- a/src/components/Article/ShoutRatingControl.tsx +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts index 8b137891..e69de29b 100644 --- a/src/graphql/client/core.ts +++ b/src/graphql/client/core.ts @@ -1 +0,0 @@ - diff --git a/src/intl/locales/ru/translation.json b/src/intl/locales/ru/translation.json index f1bf6452..e108c0ae 100644 --- a/src/intl/locales/ru/translation.json +++ b/src/intl/locales/ru/translation.json @@ -456,7 +456,7 @@ "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 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 email is": "Этот email", "This email is not verified": "Этот email не подтвержден", diff --git a/vite.config.ts b/vite.config.ts index 8b137891..e69de29b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1 +0,0 @@ -