webapp/src/components/Views/Author/Author.tsx

298 lines
12 KiB
TypeScript
Raw Normal View History

2024-07-03 21:25:03 +00:00
import { A, useLocation, useParams } from '@solidjs/router'
import { clsx } from 'clsx'
2024-03-15 14:57:03 +00:00
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { Loading } from '~/components/_shared/Loading'
2024-06-24 17:50:27 +00:00
import { useAuthors } from '~/context/authors'
import { useFeed } from '~/context/feed'
import { useFollowing } from '~/context/following'
2024-06-24 17:50:27 +00:00
import { useGraphQL } from '~/context/graphql'
import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session'
2024-07-09 09:13:13 +00:00
import { loadReactions } from '~/graphql/api/public'
import loadShoutsQuery from '~/graphql/query/core/articles-load-by'
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-07-05 14:08:12 +00:00
import { byCreated } from '~/lib/sortby'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { splitToPages } from '~/utils/splitToPages'
import stylesArticle from '../../Article/Article.module.scss'
import { Comment } from '../../Article/Comment'
import { AuthorCard } from '../../Author/AuthorCard'
2023-12-27 23:35:43 +00:00
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'
2024-02-04 11:25:21 +00:00
import styles from './Author.module.scss'
2022-09-09 11:53:35 +00:00
type Props = {
2022-10-05 15:11:14 +00:00
authorSlug: string
2024-04-15 18:01:00 +00:00
shouts?: Shout[]
author?: Author
2024-07-09 17:41:14 +00:00
topics?: Topic[]
2024-07-03 21:25:03 +00:00
selectedTab: string
2022-09-22 09:37:49 +00:00
}
export const PRERENDERED_ARTICLES_COUNT = 12
2023-02-17 09:21:02 +00:00
const LOAD_MORE_PAGE_SIZE = 9
export const AuthorView = (props: Props) => {
2024-07-09 17:41:14 +00:00
console.debug('[components.AuthorView] reactive context init...')
2023-02-17 09:21:02 +00:00
const { t } = useLocalize()
2024-07-03 21:25:03 +00:00
const params = useParams()
2024-05-20 11:16:54 +00:00
const { followers: myFollowers, follows: myFollows } = useFollowing()
2024-06-24 17:50:27 +00:00
const { session } = useSession()
const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
2024-07-06 01:03:00 +00:00
const [authorSlug, setSlug] = createSignal(props.authorSlug)
2024-06-24 17:50:27 +00:00
const { sortedFeed } = useFeed()
const loc = useLocation()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
2023-09-06 22:58:54 +00:00
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
2024-06-24 17:50:27 +00:00
const { loadAuthor, authorsEntities } = useAuthors()
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-07-09 09:13:13 +00:00
const [commented, setCommented] = createSignal<Reaction[]>([])
2024-06-24 17:50:27 +00:00
const { query } = useGraphQL()
2023-12-25 06:52:04 +00:00
2024-05-20 23:15:52 +00:00
// пагинация загрузки ленты постов
const loadMore = async () => {
saveScrollPosition()
2024-06-24 17:50:27 +00:00
const resp = await query(loadShoutsQuery, {
2023-04-20 14:01:15 +00:00
filters: { author: props.authorSlug },
limit: LOAD_MORE_PAGE_SIZE,
2024-06-26 08:22:05 +00:00
offset: sortedFeed().length
})
2024-06-24 17:50:27 +00:00
const hasMore = resp?.data?.load_shouts_by?.hasMore
setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition()
}
2024-06-24 17:50:27 +00:00
// 1 // проверяет не собственный ли это профиль, иначе - загружает
2024-05-20 23:15:52 +00:00
const [isFetching, setIsFetching] = createSignal(false)
createEffect(
2024-07-06 01:03:00 +00:00
on([() => session()?.user?.app_data?.profile, () => props.authorSlug || ''], async ([me, slug]) => {
const my = slug && me?.slug === slug
2024-05-20 23:15:52 +00:00
if (my) {
console.debug('[Author] my profile precached')
2024-06-24 17:50:27 +00:00
if (me) {
setAuthor(me)
if (myFollowers()) setFollowers((myFollowers() || []) as Author[])
changeFollowing([...(myFollows?.topics || []), ...(myFollows?.authors || [])])
}
2024-07-06 01:03:00 +00:00
} else if (slug && !isFetching()) {
2024-06-24 17:50:27 +00:00
setIsFetching(true)
2024-07-06 01:03:00 +00:00
setSlug(slug)
await loadAuthor({ slug })
2024-06-24 17:50:27 +00:00
setIsFetching(false) // Сброс состояния загрузки после завершения
2024-05-20 23:15:52 +00:00
}
2024-06-26 08:22:05 +00:00
})
2024-06-24 17:50:27 +00:00
)
2024-07-09 17:41:14 +00:00
// 2 // догружает подписки автора
2024-06-24 17:50:27 +00:00
createEffect(
on(
2024-07-09 17:41:14 +00:00
[followers, () => props.author || authorsEntities()[authorSlug()]],
2024-06-24 17:50:27 +00:00
async ([current, found]) => {
if (current) return
if (!found) return
setAuthor(found)
2024-07-06 01:03:00 +00:00
console.info(`[Author] profile for @${authorSlug()} fetched`)
const followsResp = await query(getAuthorFollowsQuery, { slug: authorSlug() }).toPromise()
2024-06-24 17:50:27 +00:00
const follows = followsResp?.data?.get_author_followers || {}
changeFollowing([...(follows?.authors || []), ...(follows?.topics || [])])
2024-07-06 01:03:00 +00:00
console.info(`[Author] follows for @${authorSlug()} fetched`)
const followersResp = await query(getAuthorFollowersQuery, { slug: authorSlug() }).toPromise()
2024-06-24 17:50:27 +00:00
setFollowers(followersResp?.data?.get_author_followers || [])
2024-07-06 01:03:00 +00:00
console.info(`[Author] followers for @${authorSlug()} fetched`)
2024-06-24 17:50:27 +00:00
setIsFetching(false)
},
2024-06-26 08:22:05 +00:00
{ defer: true }
)
2024-05-20 23:15:52 +00:00
)
2024-07-09 17:41:14 +00:00
// 3 // догружает ленту и комментарии
2024-05-20 23:15:52 +00:00
createEffect(
2024-06-24 17:50:27 +00:00
on(
() => author() as Author,
async (profile: Author) => {
if (!commented() && profile) {
await loadMore()
2024-05-20 23:15:52 +00:00
2024-07-09 09:13:13 +00:00
const commentsFetcher = loadReactions({
2024-06-26 08:22:05 +00:00
by: { comment: true, created_by: profile.id }
2024-07-09 09:13:13 +00:00
})
const ccc = await commentsFetcher()
if (ccc) setCommented((_) => ccc || [])
2024-06-24 17:50:27 +00:00
}
2024-06-26 08:22:05 +00:00
}
2024-06-24 17:50:27 +00:00
// { defer: true },
2024-06-26 08:22:05 +00:00
)
2024-05-20 23:15:52 +00:00
)
2024-06-24 17:50:27 +00:00
let bioContainerRef: HTMLDivElement
let bioWrapperRef: HTMLDivElement
2024-05-20 23:15:52 +00:00
const checkBioHeight = () => {
2024-06-24 17:50:27 +00:00
if (bioContainerRef) {
setShowExpandBioControl(bioContainerRef.offsetHeight > bioWrapperRef.offsetHeight)
2024-05-20 23:15:52 +00:00
}
}
const pages = createMemo<Shout[][]>(() =>
2024-06-26 08:22:05 +00:00
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
const handleDeleteComment = (id: number) => {
2024-06-24 17:50:27 +00:00
setCommented((prev) => (prev || []).filter((comment) => comment.id !== id))
}
2024-07-09 17:41:14 +00:00
onMount(checkBioHeight)
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-03-15 16:42:55 +00:00
<Show when={author()} 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')}>
<div class="col-md-16">
<ul class="view-switcher">
2024-07-03 21:25:03 +00:00
<li classList={{ 'view-switcher__item--selected': params.tab === '' }}>
2024-06-24 17:50:27 +00:00
<A href={`/author/${props.authorSlug}`}>{t('Publications')}</A>
<Show when={author()?.stat}>
<span class="view-switcher__counter">{author()?.stat?.shouts || 0}</span>
2023-12-24 17:29:16 +00:00
</Show>
</li>
2024-07-03 21:25:03 +00:00
<li classList={{ 'view-switcher__item--selected': params.tab === 'comment' }}>
2024-06-24 17:50:27 +00:00
<A href={`/author/${props.authorSlug}/comments`}>{t('Comments')}</A>
<Show when={author()?.stat}>
<span class="view-switcher__counter">{author()?.stat?.comments || 0}</span>
2023-12-24 17:29:16 +00:00
</Show>
</li>
2024-07-03 21:25:03 +00:00
<li classList={{ 'view-switcher__item--selected': params.tab === 'about' }}>
<A onClick={() => checkBioHeight()} href={`/author/${props.authorSlug}/about`}>
2024-07-03 17:38:43 +00:00
{t('About the author')}
2024-06-24 17:50:27 +00:00
</A>
</li>
</ul>
</div>
<div class={clsx('col-md-8', styles.additionalControls)}>
2024-03-15 16:42:55 +00:00
<Show when={author()?.stat?.rating || author()?.stat?.rating === 0}>
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-07-03 21:25:03 +00:00
<Match when={params.tab === '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)}
2023-09-06 22:58:54 +00:00
class={styles.longBio}
classList={{ [styles.longBioExpanded]: isBioExpanded() }}
>
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())}
>
{t('Show more')}
</button>
</Show>
</div>
</div>
2023-02-17 09:21:02 +00:00
</div>
</Match>
2024-07-03 21:25:03 +00:00
<Match when={params.tab === '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
2023-02-17 09:21:02 +00:00
<div class="wide-container">
<div class="row">
<div class="col-md-20 col-lg-18">
<ul class={stylesArticle.comments}>
2024-02-29 15:05:05 +00:00
<For each={commented()?.sort(byCreated).reverse()}>
{(comment) => (
<Comment
comment={comment}
class={styles.comment}
showArticleLink={true}
onDelete={(id) => handleDeleteComment(id)}
/>
)}
</For>
</ul>
</div>
</div>
2023-02-17 09:21:02 +00:00
</div>
</Match>
2024-07-03 21:25:03 +00:00
<Match when={params.tab === ''}>
<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-06-24 17:50:27 +00:00
<Show when={sortedFeed().length > 0}>
<Row1 article={sortedFeed()[0]} noauthor={true} nodate={true} />
2023-08-27 21:21:40 +00:00
2024-06-24 17:50:27 +00:00
<Show when={sortedFeed().length > 1}>
<Switch>
2024-06-24 17:50:27 +00:00
<Match when={sortedFeed().length === 2}>
<Row2 articles={sortedFeed()} isEqual={true} noauthor={true} nodate={true} />
</Match>
2024-06-24 17:50:27 +00:00
<Match when={sortedFeed().length === 3}>
<Row3 articles={sortedFeed()} noauthor={true} nodate={true} />
</Match>
2024-06-24 17:50:27 +00:00
<Match when={sortedFeed().length > 3}>
<For each={pages()}>
{(page) => (
<>
<Row1 article={page[0]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(1, 3)} isEqual={true} noauthor={true} />
<Row1 article={page[3]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(4, 6)} isEqual={true} noauthor={true} />
<Row1 article={page[6]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(7, 9)} isEqual={true} noauthor={true} />
</>
)}
</For>
</Match>
</Switch>
2024-05-11 17:27:57 +00:00
</Show>
2023-02-17 09:21:02 +00:00
2024-05-11 17:27:57 +00:00
<Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container">
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</Show>
2023-02-17 09:21:02 +00:00
</Show>
</Match>
</Switch>
2022-09-09 11:53:35 +00:00
</div>
)
}