From 260b95f692a365b83f73c06aa5ed1c21c3e37c4c Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 6 Sep 2024 08:13:24 +0300 Subject: [PATCH] scrollto+shoutreaction --- src/components/Article/Comment/Comment.tsx | 6 ++-- .../Article/CommentRatingControl.tsx | 6 ++-- src/components/Article/CommentsTree.tsx | 4 +-- src/components/Article/FullArticle.tsx | 35 ++++++++----------- src/components/Article/ShoutRatingControl.tsx | 12 +++---- .../Feed/ArticleCard/ArticleCard.tsx | 8 ++--- src/components/HeaderNav/Header.tsx | 13 ++----- src/components/Views/Author/Author.tsx | 4 +-- src/components/_shared/PageLayout.tsx | 14 +++----- src/context/reactions.tsx | 33 +++++++++-------- src/routes/[slug]/[...tab].tsx | 8 +++-- src/routes/articles/[topic]/[slug].tsx | 2 +- 12 files changed, 62 insertions(+), 83 deletions(-) diff --git a/src/components/Article/Comment/Comment.tsx b/src/components/Article/Comment/Comment.tsx index 5bb4b6e8..039701d3 100644 --- a/src/components/Article/Comment/Comment.tsx +++ b/src/components/Article/Comment/Comment.tsx @@ -47,7 +47,7 @@ export const Comment = (props: Props) => { const [editedBody, setEditedBody] = createSignal() const { session } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) - const { createReaction, updateReaction } = useReactions() + const { createShoutReaction, updateShoutReaction } = useReactions() const { showConfirm } = useUI() const { showSnackbar } = useSnackbar() const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) @@ -99,7 +99,7 @@ export const Comment = (props: Props) => { const handleCreate = async (value: string) => { try { setLoading(true) - await createReaction({ + await createShoutReaction({ reaction: { kind: ReactionKind.Comment, reply_to: props.comment.id, @@ -123,7 +123,7 @@ export const Comment = (props: Props) => { const handleUpdate = async (value: string) => { setLoading(true) try { - const reaction = await updateReaction({ + const reaction = await updateShoutReaction({ reaction: { id: props.comment.id || 0, kind: ReactionKind.Comment, diff --git a/src/components/Article/CommentRatingControl.tsx b/src/components/Article/CommentRatingControl.tsx index 08c8d555..ec0184bb 100644 --- a/src/components/Article/CommentRatingControl.tsx +++ b/src/components/Article/CommentRatingControl.tsx @@ -22,7 +22,7 @@ export const CommentRatingControl = (props: Props) => { const { session } = useSession() const uid = createMemo(() => session()?.user?.app_data?.profile?.id || 0) const { showSnackbar } = useSnackbar() - const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() + const { reactionEntities, createShoutReaction, deleteShoutReaction, loadReactionsBy } = useReactions() const checkReaction = (reactionKind: ReactionKind) => Object.values(reactionEntities).some( @@ -53,7 +53,7 @@ export const CommentRatingControl = (props: Props) => { r.shout.id === props.comment.shout.id && r.reply_to === props.comment.id ) - if (reactionToDelete) return deleteReaction(reactionToDelete.id) + if (reactionToDelete) return deleteShoutReaction(reactionToDelete.id) } const handleRatingChange = async (isUpvote: boolean) => { @@ -63,7 +63,7 @@ export const CommentRatingControl = (props: Props) => { } else if (isDownvoted()) { await deleteCommentReaction(ReactionKind.Dislike) } else { - await createReaction({ + await createShoutReaction({ reaction: { kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, shout: props.comment.shout.id, diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 299c62a1..f10190f0 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -29,7 +29,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, createShoutReaction, loadReactionsBy } = useReactions() const comments = createMemo(() => Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') @@ -74,7 +74,7 @@ export const CommentsTree = (props: Props) => { const handleSubmitComment = async (value: string) => { setPosting(true) try { - await createReaction({ + await createShoutReaction({ reaction: { kind: ReactionKind.Comment, body: value, diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 014288ef..9c598a01 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -38,7 +38,6 @@ import { ShoutRatingControl } from './ShoutRatingControl' type Props = { article: Shout - scrollToComments?: boolean } type IframeSize = { @@ -47,8 +46,7 @@ type IframeSize = { } export type ArticlePageSearchParams = { - scrollTo: 'comments' - commentId: string + commentId?: string slide?: string } @@ -67,7 +65,7 @@ export const COMMENTS_PER_PAGE = 30 const VOTES_PER_PAGE = 50 export const FullArticle = (props: Props) => { - const [searchParams, changeSearchParams] = useSearchParams() + const [searchParams] = useSearchParams() const { showModal } = useUI() const { loadReactionsBy } = useReactions() const [selectedImage, setSelectedImage] = createSignal('') @@ -83,18 +81,20 @@ export const FullArticle = (props: Props) => { createEffect( on( pages, - async (p: Record) => { - await loadReactionsBy({ + (p: Record) => { + console.debug('content paginated') + loadReactionsBy({ by: { shout: props.article.slug, comment: true }, limit: COMMENTS_PER_PAGE, offset: COMMENTS_PER_PAGE * p.comments || 0 }) - await loadReactionsBy({ + loadReactionsBy({ by: { shout: props.article.slug, rating: true }, limit: VOTES_PER_PAGE, offset: VOTES_PER_PAGE * p.rating || 0 }) setIsReactionsLoaded(true) + console.debug('reactions paginated') }, { defer: true } ) @@ -165,15 +165,16 @@ export const FullArticle = (props: Props) => { const media = createMemo(() => JSON.parse(props.article.media || '[]')) let commentsRef: HTMLDivElement | undefined - createEffect(() => { if (searchParams?.commentId && isReactionsLoaded()) { - const commentElement = document.querySelector( - `[id='comment_${searchParams?.commentId}']` - ) + console.debug('comment id is in link, scroll to') + const scrollToElement = + document.querySelector(`[id='comment_${searchParams?.commentId}']`) || + commentsRef || + document.body - if (commentElement) { - requestAnimationFrame(() => scrollTo(commentElement)) + if (scrollToElement) { + requestAnimationFrame(() => scrollTo(scrollToElement)) } } }) @@ -316,14 +317,6 @@ export const FullArticle = (props: Props) => { onCleanup(() => window.removeEventListener('resize', updateIframeSizes)) }) - createEffect(() => props.scrollToComments && commentsRef && scrollTo(commentsRef)) - createEffect(() => { - if (searchParams?.scrollTo === 'comments' && commentsRef) { - requestAnimationFrame(() => commentsRef && scrollTo(commentsRef)) - changeSearchParams({ scrollTo: undefined }) - } - }) - const shareUrl = createMemo(() => getShareUrl({ pathname: `/${props.article.slug || ''}` })) const getAuthorName = (a: Author) => lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name diff --git a/src/components/Article/ShoutRatingControl.tsx b/src/components/Article/ShoutRatingControl.tsx index b1302ed1..8df42e69 100644 --- a/src/components/Article/ShoutRatingControl.tsx +++ b/src/components/Article/ShoutRatingControl.tsx @@ -22,7 +22,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => { const { loadShout } = useFeed() const { requireAuthentication, session } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) - const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() + const { reactionEntities, createShoutReaction, deleteShoutReaction, loadReactionsBy } = useReactions() const [isLoading, setIsLoading] = createSignal(false) const checkReaction = (reactionKind: ReactionKind) => @@ -43,7 +43,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => { ) ) - const deleteShoutReaction = async (reactionKind: ReactionKind) => { + const removeReaction = async (reactionKind: ReactionKind) => { const reactionToDelete = Object.values(reactionEntities).find( (r) => r.kind === reactionKind && @@ -51,18 +51,18 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => { r.shout.id === props.shout.id && !r.reply_to ) - if (reactionToDelete) return deleteReaction(reactionToDelete.id) + if (reactionToDelete) return deleteShoutReaction(reactionToDelete.id) } const handleRatingChange = (isUpvote: boolean) => { requireAuthentication(async () => { setIsLoading(true) if (isUpvoted()) { - await deleteShoutReaction(ReactionKind.Like) + await removeReaction(ReactionKind.Like) } else if (isDownvoted()) { - await deleteShoutReaction(ReactionKind.Dislike) + await removeReaction(ReactionKind.Dislike) } else { - await createReaction({ + await createShoutReaction({ reaction: { kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, shout: props.shout.id diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index 665a2d50..635fe7d2 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -1,4 +1,4 @@ -import { A, useNavigate, useSearchParams } from '@solidjs/router' +import { A, useNavigate } from '@solidjs/router' import { clsx } from 'clsx' import { Accessor, For, Show, createMemo, createSignal } from 'solid-js' import { Icon } from '~/components/_shared/Icon' @@ -105,7 +105,6 @@ export const ArticleCard = (props: ArticleCardProps) => { const { t, lang, formatDate } = useLocalize() const { session } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) - const [, changeSearchParams] = useSearchParams() const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false) const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true) @@ -129,10 +128,7 @@ export const ArticleCard = (props: ArticleCardProps) => { const scrollToComments = (event: MouseEvent & { currentTarget: HTMLAnchorElement; target: Element }) => { event.preventDefault() - changeSearchParams({ - scrollTo: 'comments' - }) - navigate(`/${props.article.slug}`) + navigate(`/${props.article.slug}?commentId=0`) } const onInvite = () => { diff --git a/src/components/HeaderNav/Header.tsx b/src/components/HeaderNav/Header.tsx index 6512dd01..a43584fa 100644 --- a/src/components/HeaderNav/Header.tsx +++ b/src/components/HeaderNav/Header.tsx @@ -23,7 +23,6 @@ type Props = { isHeaderFixed?: boolean desc?: string cover?: string - scrollToComments?: (value: boolean) => void } type HeaderSearchParams = { @@ -38,7 +37,7 @@ export const Header = (props: Props) => { const { t, lang } = useLocalize() const { modal } = useUI() const { requireAuthentication } = useSession() - const [searchParams] = useSearchParams() + const [searchParams, changeSearchParams] = useSearchParams() const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false) const [getIsScrolled, setIsScrolled] = createSignal(false) const [fixed, setFixed] = createSignal(false) @@ -85,14 +84,6 @@ export const Header = (props: Props) => { }) }) - const scrollToComments = ( - event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }, - value: boolean - ) => { - event.preventDefault() - props.scrollToComments?.(value) - } - const handleBookmarkButtonClick = (ev: { preventDefault: () => void }) => { requireAuthentication(() => { // TODO: implement bookmark clicked @@ -320,7 +311,7 @@ export const Header = (props: Props) => { } /> -
scrollToComments(event, true)} class={styles.control}> +
changeSearchParams({ commentId: 0 })} class={styles.control}>
diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index b534aa44..1deaa13b 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -175,7 +175,7 @@ export const AuthorView = (props: AuthorViewProps) => { const [loadMoreCommentsHidden, setLoadMoreCommentsHidden] = createSignal( Boolean(props.author?.stat && props.author?.stat?.comments === 0) ) - const { commentsByAuthor, addReactions } = useReactions() + const { commentsByAuthor, addShoutReactions } = useReactions() const loadMoreComments = async () => { if (!author()) return [] as LoadMoreItems saveScrollPosition() @@ -189,7 +189,7 @@ export const AuthorView = (props: AuthorViewProps) => { offset: commentsByAuthor()[aid]?.length || 0 }) const result = await authorCommentsFetcher() - result && addReactions(result) + result && addShoutReactions(result) restoreScrollPosition() return result as LoadMoreItems } diff --git a/src/components/_shared/PageLayout.tsx b/src/components/_shared/PageLayout.tsx index b27ae4d4..1ae5d22c 100644 --- a/src/components/_shared/PageLayout.tsx +++ b/src/components/_shared/PageLayout.tsx @@ -2,7 +2,7 @@ import { Meta, Title } from '@solidjs/meta' import { useLocation } from '@solidjs/router' import { clsx } from 'clsx' import type { JSX } from 'solid-js' -import { Show, createEffect, createMemo, createSignal } from 'solid-js' +import { Show, createMemo } from 'solid-js' import { useLocalize } from '~/context/localize' import { Shout } from '~/graphql/schema/core.gen' import enKeywords from '~/intl/locales/en/keywords.json' @@ -27,7 +27,6 @@ type PageLayoutProps = { class?: string withPadding?: boolean zeroBottomPadding?: boolean - scrollToComments?: (value: boolean) => void key?: string } @@ -48,12 +47,10 @@ export const PageLayout = (props: PageLayoutProps) => { : imageUrl ) const description = createMemo(() => props.desc || (props.article && descFromBody(props.article.body))) - const keypath = createMemo(() => (props.key || loc?.pathname.split('/')[0]) as keyof typeof ruKeywords) - const keywords = createMemo( - () => props.keywords || (lang() === 'ru' ? ruKeywords[keypath()] : enKeywords[keypath()]) - ) - const [scrollToComments, setScrollToComments] = createSignal(false) - createEffect(() => props.scrollToComments?.(scrollToComments())) + const keywords = createMemo(() => { + const keypath = (props.key || loc?.pathname.split('/')[0]) as keyof typeof ruKeywords + return props.keywords || lang() === 'ru' ? ruKeywords[keypath] : enKeywords[keypath] + }) return ( <> {props.article?.title || t(props.title)} @@ -63,7 +60,6 @@ export const PageLayout = (props: PageLayoutProps) => { desc={props.desc} cover={imageUrl} isHeaderFixed={isHeaderFixed} - scrollToComments={(value) => setScrollToComments(value)} /> diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 02c572aa..4e559cc1 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -24,10 +24,10 @@ type ReactionsContextType = { reactionsByShout: Record commentsByAuthor: Accessor> loadReactionsBy: (args: QueryLoad_Reactions_ByArgs) => Promise - createReaction: (reaction: MutationCreate_ReactionArgs) => Promise - updateReaction: (reaction: MutationUpdate_ReactionArgs) => Promise - deleteReaction: (id: number) => Promise<{ error: string } | null> - addReactions: (rrr: Reaction[]) => void + createShoutReaction: (reaction: MutationCreate_ReactionArgs) => Promise + updateShoutReaction: (reaction: MutationUpdate_ReactionArgs) => Promise + deleteShoutReaction: (id: number) => Promise<{ error: string } | null> + addShoutReactions: (rrr: Reaction[]) => void } const ReactionsContext = createContext({} as ReactionsContextType) @@ -46,7 +46,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const { session } = useSession() const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - const addReactions = (rrr: Reaction[]) => { + const addShoutReactions = (rrr: Reaction[]) => { const newReactionsByShout: Record = { ...reactionsByShout } const newReactionsByAuthor: Record = { ...reactionsByAuthor } const newReactionEntities = rrr.reduce( @@ -80,18 +80,16 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const fetcher = await loadReactions(opts) const result = (await fetcher()) || [] console.debug('[context.reactions] loaded', result) - result && addReactions(result) + result && addShoutReactions(result) return result } - const createReaction = async (input: MutationCreate_ReactionArgs): Promise => { + const createShoutReaction = async (input: MutationCreate_ReactionArgs): Promise => { const resp = await client()?.mutation(createReactionMutation, input).toPromise() const { error, reaction } = resp?.data?.create_reaction || {} if (error) await showSnackbar({ type: 'error', body: t(error) }) if (!reaction) return - const changes = { - [reaction.id]: reaction - } + const changes = { [reaction.id]: reaction } if ([ReactionKind.Like, ReactionKind.Dislike].includes(reaction.kind)) { const oppositeReactionKind = @@ -110,10 +108,11 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { } } - setReactionEntities(changes) + addShoutReactions([reaction]) + return reaction } - const deleteReaction = async ( + const deleteShoutReaction = async ( reaction_id: number ): Promise<{ error: string; reaction?: string } | null> => { if (reaction_id) { @@ -129,7 +128,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { return null } - const updateReaction = async (input: MutationUpdate_ReactionArgs): Promise => { + const updateShoutReaction = async (input: MutationUpdate_ReactionArgs): Promise => { const resp = await client()?.mutation(updateReactionMutation, input).toPromise() const result = resp?.data?.update_reaction if (!result) throw new Error('cannot update reaction') @@ -143,10 +142,10 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const actions = { loadReactionsBy, - createReaction, - updateReaction, - deleteReaction, - addReactions + createShoutReaction, + updateShoutReaction, + deleteShoutReaction, + addShoutReactions } const value: ReactionsContextType = { reactionEntities, reactionsByShout, commentsByAuthor, ...actions } diff --git a/src/routes/[slug]/[...tab].tsx b/src/routes/[slug]/[...tab].tsx index b17bdab7..ef9fc842 100644 --- a/src/routes/[slug]/[...tab].tsx +++ b/src/routes/[slug]/[...tab].tsx @@ -28,7 +28,12 @@ export const route: RouteDefinition = { }) } -export type ArticlePageProps = { article?: Shout; comments?: Reaction[]; votes?: Reaction[]; author?: Author } +export type ArticlePageProps = { + article?: Shout + comments?: Reaction[] + votes?: Reaction[] + author?: Author +} export type SlugPageProps = { article?: Shout @@ -125,4 +130,3 @@ export default function ArticlePage(props: RouteSectionProps) { } return } - diff --git a/src/routes/articles/[topic]/[slug].tsx b/src/routes/articles/[topic]/[slug].tsx index 98ee54f5..0f23a54e 100644 --- a/src/routes/articles/[topic]/[slug].tsx +++ b/src/routes/articles/[topic]/[slug].tsx @@ -1,3 +1,3 @@ -import ArticlePage from "~/routes/[slug]/[...tab]" +import ArticlePage from '~/routes/[slug]/[...tab]' export default ArticlePage