webapp/src/components/Views/AuthorView.tsx

365 lines
12 KiB
TypeScript
Raw Normal View History

2024-08-28 09:50:04 +00:00
import { A, useLocation, useParams } from '@solidjs/router'
import { clsx } from 'clsx'
2024-09-03 08:07:32 +00:00
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading'
2024-06-24 17:50:27 +00:00
import { useAuthors } from '~/context/authors'
2024-09-03 08:07:32 +00:00
import { SHOUTS_PER_PAGE, useFeed } from '~/context/feed'
import { useFollowing } from '~/context/following'
import { useLocalize } from '~/context/localize'
2024-09-03 10:21:59 +00:00
import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session'
2024-09-03 10:21:59 +00:00
import { loadReactions, loadShouts } from '~/graphql/api/public'
import getAuthorFollowersQuery from '~/graphql/query/core/author-followers'
import getAuthorFollowsQuery from '~/graphql/query/core/author-follows'
import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen'
2024-09-03 08:07:32 +00:00
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
2024-08-30 13:45:17 +00:00
import { byCreated } from '~/utils/sort'
2024-10-02 20:12:14 +00:00
import { Comment } from '../Article/Comment'
import { AuthorCard } from '../Author/AuthorCard'
import { AuthorShoutsRating } from '../Author/AuthorShoutsRating'
import { Placeholder } from '../Feed/Placeholder'
import { Row1 } from '../Feed/Row1'
import { Row2 } from '../Feed/Row2'
import { Row3 } from '../Feed/Row3'
import styles from '~/styles/views/Author.module.scss'
import stylesArticle from '../Article/Article.module.scss'
2022-09-09 11:53:35 +00:00
2024-07-13 09:36:23 +00:00
type AuthorViewProps = {
2022-10-05 15:11:14 +00:00
authorSlug: string
2024-08-28 13:10:00 +00:00
shouts: Shout[]
2024-09-03 16:50:26 +00:00
comments: Reaction[]
2024-04-15 18:01:00 +00:00
author?: Author
2022-09-22 09:37:49 +00:00
}
export const PRERENDERED_ARTICLES_COUNT = 12
2024-09-03 10:21:59 +00:00
const COMMENTS_PER_PAGE = 12
2024-08-28 13:10:00 +00:00
// const LOAD_MORE_PAGE_SIZE = 9
2024-07-13 09:36:23 +00:00
export const AuthorView = (props: AuthorViewProps) => {
2024-07-13 07:01:41 +00:00
// contexts
2023-02-17 09:21:02 +00:00
const { t } = useLocalize()
2024-07-13 07:01:41 +00:00
const loc = useLocation()
2024-08-28 09:50:04 +00:00
const params = useParams()
2024-08-28 13:10:00 +00:00
const [currentTab, setCurrentTab] = createSignal<string>(params.tab)
2024-07-30 19:06:17 +00:00
2024-09-24 09:15:50 +00:00
const { session, client } = useSession()
2024-07-30 19:06:17 +00:00
2024-07-13 07:01:41 +00:00
const { loadAuthor, authorsEntities } = useAuthors()
const { followers: myFollowers, follows: myFollows } = useFollowing()
// signals
2023-09-06 22:58:54 +00:00
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
2024-06-24 17:50:27 +00:00
const [author, setAuthor] = createSignal<Author>()
const [followers, setFollowers] = createSignal<Author[]>([] as Author[])
const [following, changeFollowing] = createSignal<Array<Author | Topic>>([] as Array<Author | Topic>) // flat AuthorFollowsResult
2023-09-06 22:58:54 +00:00
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
2024-09-03 16:50:26 +00:00
const [commented, setCommented] = createSignal<Reaction[]>(props.comments || [])
2024-09-03 10:21:59 +00:00
const [followersLoaded, setFollowersLoaded] = createSignal(false)
const [followingsLoaded, setFollowingsLoaded] = createSignal(false)
2023-12-25 06:52:04 +00:00
2024-07-13 07:01:41 +00:00
// derivatives
const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
2024-07-30 17:59:12 +00:00
// Объединенный эффект для загрузки автора и его подписок
2024-09-03 16:50:26 +00:00
createEffect(
on(
() => session()?.user?.app_data?.profile,
async (meData?: Author) => {
const slug = props.authorSlug
if (slug && meData?.slug === slug) {
setAuthor(meData)
setFollowers(myFollowers() || [])
setFollowersLoaded(true)
changeFollowing([...(myFollows?.topics || []), ...(myFollows?.authors || [])])
setFollowingsLoaded(true)
2024-09-03 16:50:26 +00:00
} else if (slug && !author()) {
await loadAuthor({ slug })
const foundAuthor = authorsEntities()[slug]
setAuthor(foundAuthor)
if (foundAuthor) {
const followsResp = await client()
?.query(getAuthorFollowsQuery, { slug: foundAuthor.slug })
.toPromise()
const follows = followsResp?.data?.get_author_followers || {}
changeFollowing([...(follows?.authors || []), ...(follows?.topics || [])])
setFollowingsLoaded(true)
const followersResp = await client()
?.query(getAuthorFollowersQuery, { slug: foundAuthor.slug })
.toPromise()
setFollowers(followersResp?.data?.get_author_followers || [])
setFollowersLoaded(true)
}
}
},
{}
)
)
2024-05-20 23:15:52 +00:00
2024-07-30 17:59:12 +00:00
// Обработка биографии
2024-06-24 17:50:27 +00:00
let bioContainerRef: HTMLDivElement
let bioWrapperRef: HTMLDivElement
2024-07-30 17:59:12 +00:00
const checkBioHeight = () => {
if (bioWrapperRef && bioContainerRef) {
const showExpand = bioContainerRef.offsetHeight > bioWrapperRef.offsetHeight
setShowExpandBioControl(showExpand)
}
2024-05-20 23:15:52 +00:00
}
2024-07-30 17:59:12 +00:00
createEffect(() => {
checkBioHeight()
})
const handleDeleteComment = (id: number) => {
2024-09-03 10:29:01 +00:00
setCommented((prev) => (prev || []).filter((comment) => comment.id !== id))
}
2024-08-28 09:50:04 +00:00
const TabNavigator = () => (
<div class="col-md-16">
<ul class="view-switcher">
<li classList={{ 'view-switcher__item--selected': !currentTab() }}>
<A href={`/@${props.authorSlug}`}>{t('Publications')}</A>
<Show when={author()?.stat}>
<span class="view-switcher__counter">{author()?.stat?.shouts || 0}</span>
</Show>
</li>
<li classList={{ 'view-switcher__item--selected': currentTab() === 'comments' }}>
<A href={`/@${props.authorSlug}/comments`}>{t('Comments')}</A>
<Show when={author()?.stat}>
<span class="view-switcher__counter">{author()?.stat?.comments || 0}</span>
</Show>
</li>
<li classList={{ 'view-switcher__item--selected': currentTab() === 'about' }}>
<A onClick={() => checkBioHeight()} href={`/@${props.authorSlug}/about`}>
2024-08-29 15:34:13 +00:00
{t('About')}
2024-08-28 09:50:04 +00:00
</A>
</li>
</ul>
</div>
)
2024-09-03 08:07:32 +00:00
const { feedByAuthor, addFeed } = useFeed()
2024-09-03 16:50:26 +00:00
const [sortedFeed, setSortedFeed] = createSignal<Shout[]>(props.shouts || [])
2024-09-03 08:07:32 +00:00
const [loadMoreHidden, setLoadMoreHidden] = createSignal(false)
const loadMore = async () => {
saveScrollPosition()
const authorShoutsFetcher = loadShouts({
2024-09-03 08:07:32 +00:00
filters: { author: props.authorSlug },
limit: SHOUTS_PER_PAGE,
2024-09-03 10:21:59 +00:00
offset: feedByAuthor()?.[props.authorSlug]?.length || 0
2024-09-03 08:07:32 +00:00
})
const result = await authorShoutsFetcher()
if (result) {
addFeed(result)
}
2024-09-03 08:07:32 +00:00
restoreScrollPosition()
return result as LoadMoreItems
}
// Function to chunk the sortedFeed into arrays of 3 shouts each
const chunkArray = (array: Shout[], chunkSize: number): Shout[][] => {
const chunks: Shout[][] = []
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize))
}
return chunks
}
// Memoize the chunked feed
const feedChunks = createMemo(() => chunkArray(sortedFeed(), 3))
2024-09-03 08:07:32 +00:00
// fx to update author's feed
2024-09-03 10:29:01 +00:00
createEffect(
on(
feedByAuthor,
(byAuthor) => {
const feed = byAuthor[props.authorSlug] as Shout[]
if (!feed) return
setSortedFeed(feed)
},
{}
)
)
2024-09-03 08:07:32 +00:00
2024-09-03 16:50:26 +00:00
const [loadMoreCommentsHidden, setLoadMoreCommentsHidden] = createSignal(
Boolean(props.author?.stat && props.author?.stat?.comments === 0)
)
2024-09-06 05:13:24 +00:00
const { commentsByAuthor, addShoutReactions } = useReactions()
2024-09-03 10:21:59 +00:00
const loadMoreComments = async () => {
if (!author()) return [] as LoadMoreItems
saveScrollPosition()
const aid = author()?.id || 0
const authorCommentsFetcher = loadReactions({
by: {
comment: true,
author: author()?.slug
},
limit: COMMENTS_PER_PAGE,
offset: commentsByAuthor()[aid]?.length || 0
})
const result = await authorCommentsFetcher()
if (result) {
addShoutReactions(result)
}
2024-09-03 10:21:59 +00:00
restoreScrollPosition()
return result as LoadMoreItems
}
createEffect(() => setCurrentTab(params.tab))
2024-09-03 15:37:57 +00:00
// Update commented when author or commentsByAuthor changes
2024-09-03 16:50:26 +00:00
createEffect(
on(
[author, commentsByAuthor],
([a, ccc]) => {
if (a && ccc && ccc[a.id]) {
setCommented(ccc[a.id])
}
},
{}
)
2024-09-03 16:50:26 +00:00
)
2024-09-03 10:29:01 +00:00
createEffect(
on(
2024-09-03 16:50:26 +00:00
[author, commented],
([a, ccc]) => {
if (a && ccc) {
setLoadMoreCommentsHidden((ccc || []).length === a.stat?.comments)
}
},
{}
2024-09-03 10:29:01 +00:00
)
)
2024-09-03 15:37:57 +00:00
2024-09-03 10:29:01 +00:00
createEffect(
on(
[author, feedByAuthor],
([a, feed]) => {
if (a && feed[props.authorSlug]) {
setLoadMoreHidden(feed[props.authorSlug]?.length === a.stat?.shouts)
}
},
2024-09-03 10:29:01 +00:00
{}
)
)
2024-09-03 10:21:59 +00:00
2022-09-09 11:53:35 +00:00
return (
2023-08-27 21:21:40 +00:00
<div class={styles.authorPage}>
2023-02-17 09:21:02 +00:00
<div class="wide-container">
2024-09-03 10:21:59 +00:00
<Show when={author() && followersLoaded() && followingsLoaded()} fallback={<Loading />}>
<>
<div class={styles.authorHeader}>
2024-06-24 17:50:27 +00:00
<AuthorCard
author={author() as Author}
followers={followers() || []}
flatFollows={following() || []}
/>
2022-09-09 11:53:35 +00:00
</div>
<div class={clsx(styles.groupControls, 'row')}>
2024-08-28 09:50:04 +00:00
<TabNavigator />
<div class={clsx('col-md-8', styles.additionalControls)}>
<Show when={typeof author()?.stat?.rating === 'number'}>
2023-12-27 23:14:33 +00:00
<div class={styles.ratingContainer}>
2023-12-28 00:30:09 +00:00
{t('All posts rating')}
2024-06-24 17:50:27 +00:00
<AuthorShoutsRating author={author() as Author} class={styles.ratingControl} />
2023-12-27 23:14:33 +00:00
</div>
</Show>
</div>
</div>
</>
</Show>
2023-02-17 09:21:02 +00:00
</div>
2022-09-09 11:53:35 +00:00
<Switch>
2024-08-28 09:50:04 +00:00
<Match when={currentTab() === 'about'}>
2023-02-17 09:21:02 +00:00
<div class="wide-container">
2023-09-06 22:58:54 +00:00
<div class="row">
<div class="col-md-20 col-lg-18">
<div
2024-06-24 17:50:27 +00:00
ref={(el) => (bioWrapperRef = el)}
class={clsx(styles.longBio, { [styles.longBioExpanded]: isBioExpanded() })}
2023-09-06 22:58:54 +00:00
>
2024-06-24 17:50:27 +00:00
<div ref={(el) => (bioContainerRef = el)} innerHTML={author()?.about || ''} />
2023-09-06 22:58:54 +00:00
</div>
<Show when={showExpandBioControl()}>
<button
class={clsx('button button--subscribe-topic', styles.longBioExpandedControl)}
onClick={() => setIsBioExpanded(!isBioExpanded())}
>
{isBioExpanded() ? t('Show less') : t('Show more')}
2023-09-06 22:58:54 +00:00
</button>
</Show>
</div>
</div>
2023-02-17 09:21:02 +00:00
</div>
</Match>
2024-08-28 09:50:04 +00:00
<Match when={currentTab() === 'comments'}>
<Show when={me()?.slug === props.authorSlug && !me()?.stat?.comments}>
2024-05-18 22:03:06 +00:00
<div class="wide-container">
2024-06-24 17:50:27 +00:00
<Placeholder type={loc?.pathname} mode="profile" />
2024-05-18 22:03:06 +00:00
</div>
</Show>
2024-05-11 17:27:57 +00:00
2024-09-03 10:29:01 +00:00
<LoadMoreWrapper
loadFunction={loadMoreComments}
pageSize={COMMENTS_PER_PAGE}
hidden={loadMoreCommentsHidden()}
>
<div class="wide-container">
<div class="row">
<div class="col-md-20 col-lg-18">
<ul class={stylesArticle.comments}>
<For each={commented()?.sort(byCreated).reverse()}>
{(comment) => (
<Comment
comment={comment}
class={styles.comment}
showArticleLink={true}
onDelete={(id) => handleDeleteComment(id)}
/>
)}
</For>
</ul>
</div>
</div>
</div>
2024-09-03 10:21:59 +00:00
</LoadMoreWrapper>
2023-02-17 09:21:02 +00:00
</Match>
2024-08-28 09:50:04 +00:00
<Match when={!currentTab()}>
<Show when={me()?.slug === props.authorSlug && !me()?.stat?.shouts}>
2024-05-11 17:27:57 +00:00
<div class="wide-container">
2024-06-24 17:50:27 +00:00
<Placeholder type={loc?.pathname} mode="profile" />
2024-05-11 17:27:57 +00:00
</div>
2023-08-27 21:21:40 +00:00
</Show>
2024-09-03 08:07:32 +00:00
<LoadMoreWrapper loadFunction={loadMore} pageSize={SHOUTS_PER_PAGE} hidden={loadMoreHidden()}>
<For each={feedChunks()}>
{(articles) => (
<Switch>
<Match when={articles.length === 1}>
<Row1 article={articles[0]} noauthor={true} nodate={true} />
</Match>
<Match when={articles.length === 2}>
<Row2 articles={articles} noauthor={true} nodate={true} isEqual={true} />
</Match>
<Match when={articles.length === 3}>
<Row3 articles={articles} noauthor={true} nodate={true} />
</Match>
</Switch>
)}
2024-08-28 13:10:00 +00:00
</For>
2024-09-03 08:07:32 +00:00
</LoadMoreWrapper>
2023-02-17 09:21:02 +00:00
</Match>
</Switch>
2022-09-09 11:53:35 +00:00
</div>
)
}