2024-07-03 17:38:43 +00:00
|
|
|
|
import { A, createAsync, useLocation, useNavigate, useSearchParams } from '@solidjs/router'
|
|
|
|
|
import { clsx } from 'clsx'
|
2024-07-09 12:27:50 +00:00
|
|
|
|
import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
2024-07-04 07:51:15 +00:00
|
|
|
|
import { DropDown } from '~/components/_shared/DropDown'
|
2024-07-03 17:38:43 +00:00
|
|
|
|
import { Option } from '~/components/_shared/DropDown/DropDown'
|
2024-07-04 07:51:15 +00:00
|
|
|
|
import { Icon } from '~/components/_shared/Icon'
|
|
|
|
|
import { InviteMembers } from '~/components/_shared/InviteMembers'
|
|
|
|
|
import { Loading } from '~/components/_shared/Loading'
|
|
|
|
|
import { ShareModal } from '~/components/_shared/ShareModal'
|
|
|
|
|
import { useAuthors } from '~/context/authors'
|
2024-06-24 17:50:27 +00:00
|
|
|
|
import { useGraphQL } from '~/context/graphql'
|
2024-07-04 07:51:15 +00:00
|
|
|
|
import { useLocalize } from '~/context/localize'
|
|
|
|
|
import { useReactions } from '~/context/reactions'
|
|
|
|
|
import { useSession } from '~/context/session'
|
|
|
|
|
import { useTopics } from '~/context/topics'
|
2024-06-24 17:50:27 +00:00
|
|
|
|
import { useUI } from '~/context/ui'
|
2024-07-04 07:51:15 +00:00
|
|
|
|
import { loadUnratedShouts } from '~/graphql/api/private'
|
|
|
|
|
import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
|
2024-07-13 11:42:53 +00:00
|
|
|
|
import { byCreated } from '~/lib/sortBy'
|
2024-07-09 12:26:24 +00:00
|
|
|
|
import { FeedSearchParams } from '~/routes/feed/(feed)'
|
2023-12-14 18:45:50 +00:00
|
|
|
|
import { CommentDate } from '../../Article/CommentDate'
|
2024-01-08 13:02:52 +00:00
|
|
|
|
import { getShareUrl } from '../../Article/SharePopup'
|
2023-12-14 18:45:50 +00:00
|
|
|
|
import { AuthorBadge } from '../../Author/AuthorBadge'
|
2023-12-31 05:01:34 +00:00
|
|
|
|
import { AuthorLink } from '../../Author/AuthorLink'
|
2023-12-14 18:45:50 +00:00
|
|
|
|
import { ArticleCard } from '../../Feed/ArticleCard'
|
2024-06-24 17:50:27 +00:00
|
|
|
|
import stylesBeside from '../../Feed/Beside.module.scss'
|
|
|
|
|
import stylesTopic from '../../Feed/CardTopic.module.scss'
|
2024-05-10 16:52:13 +00:00
|
|
|
|
import { Placeholder } from '../../Feed/Placeholder'
|
2023-12-14 18:45:50 +00:00
|
|
|
|
import { Sidebar } from '../../Feed/Sidebar'
|
2024-02-03 08:16:47 +00:00
|
|
|
|
import { Modal } from '../../Nav/Modal'
|
2024-02-04 11:25:21 +00:00
|
|
|
|
import styles from './Feed.module.scss'
|
2022-09-09 11:53:35 +00:00
|
|
|
|
|
2022-10-28 21:21:47 +00:00
|
|
|
|
export const FEED_PAGE_SIZE = 20
|
2024-07-03 17:38:43 +00:00
|
|
|
|
export type PeriodType = 'week' | 'month' | 'year'
|
|
|
|
|
export type FeedProps = {
|
|
|
|
|
shouts: Shout[]
|
2023-12-29 16:43:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 17:38:43 +00:00
|
|
|
|
export const FeedView = (props: FeedProps) => {
|
2024-07-09 09:13:13 +00:00
|
|
|
|
const { t } = useLocalize()
|
2024-06-24 17:50:27 +00:00
|
|
|
|
const loc = useLocation()
|
2024-07-03 17:38:43 +00:00
|
|
|
|
const client = useGraphQL()
|
|
|
|
|
const unrated = createAsync(async () => {
|
|
|
|
|
if (client) {
|
|
|
|
|
const shoutsLoader = loadUnratedShouts(client, { limit: 5 })
|
|
|
|
|
return await shoutsLoader()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
const navigate = useNavigate()
|
2024-06-24 17:50:27 +00:00
|
|
|
|
const { showModal } = useUI()
|
2023-07-07 16:53:01 +00:00
|
|
|
|
const [isLoading, setIsLoading] = createSignal(false)
|
2023-12-18 13:22:20 +00:00
|
|
|
|
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
|
2024-06-24 17:50:27 +00:00
|
|
|
|
const { session } = useSession()
|
2024-02-04 17:40:15 +00:00
|
|
|
|
const { loadReactionsBy } = useReactions()
|
2024-05-06 23:44:25 +00:00
|
|
|
|
const { topTopics } = useTopics()
|
2024-06-24 17:50:27 +00:00
|
|
|
|
const { topAuthors } = useAuthors()
|
2023-02-17 09:21:02 +00:00
|
|
|
|
const [topComments, setTopComments] = createSignal<Reaction[]>([])
|
2024-07-03 17:38:43 +00:00
|
|
|
|
const [searchParams, changeSearchParams] = useSearchParams<FeedSearchParams>()
|
|
|
|
|
const asOption = (o: string) => ({ value: o, title: t(o) })
|
|
|
|
|
const asOptions = (opts: string[]) => opts.map(asOption)
|
|
|
|
|
const currentPeriod = createMemo(() => asOption(searchParams?.period || ''))
|
2023-12-18 13:22:20 +00:00
|
|
|
|
const loadTopComments = async () => {
|
2024-02-29 17:51:07 +00:00
|
|
|
|
const comments = await loadReactionsBy({ by: { comment: true }, limit: 50 })
|
2024-03-01 13:04:28 +00:00
|
|
|
|
setTopComments(comments.sort(byCreated).reverse())
|
2023-12-18 13:22:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 12:26:24 +00:00
|
|
|
|
createEffect(
|
|
|
|
|
on(
|
|
|
|
|
() => props.shouts,
|
|
|
|
|
(sss: Shout[]) => {
|
|
|
|
|
if (sss) {
|
|
|
|
|
setIsLoading(true)
|
|
|
|
|
Promise.all([
|
|
|
|
|
loadTopComments(),
|
|
|
|
|
loadReactionsBy({ by: { shouts: sss.map((s: Shout) => s.slug) } })
|
|
|
|
|
]).finally(() => {
|
|
|
|
|
setIsRightColumnLoaded(true)
|
|
|
|
|
setIsLoading(false)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ defer: true }
|
|
|
|
|
)
|
|
|
|
|
)
|
2022-10-28 21:21:47 +00:00
|
|
|
|
|
2024-01-08 13:02:52 +00:00
|
|
|
|
const [shareData, setShareData] = createSignal<Shout | undefined>()
|
2024-03-01 13:04:28 +00:00
|
|
|
|
const handleShare = (shared: Shout | undefined) => {
|
2024-01-08 13:02:52 +00:00
|
|
|
|
showModal('share')
|
|
|
|
|
setShareData(shared)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
|
return (
|
2024-07-09 09:13:13 +00:00
|
|
|
|
<div class="feed">
|
2023-09-29 12:48:58 +00:00
|
|
|
|
<div class="row">
|
|
|
|
|
<div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}>
|
2023-10-23 11:15:19 +00:00
|
|
|
|
<Sidebar />
|
2023-09-29 12:48:58 +00:00
|
|
|
|
</div>
|
2023-03-10 17:42:48 +00:00
|
|
|
|
|
2023-09-29 12:48:58 +00:00
|
|
|
|
<div class="col-md-12 offset-xl-1">
|
2024-06-24 17:50:27 +00:00
|
|
|
|
<Show when={!session() && loc?.pathname !== 'feed'}>
|
|
|
|
|
<Placeholder type={loc?.pathname} mode="feed" />
|
2024-05-18 22:03:06 +00:00
|
|
|
|
</Show>
|
|
|
|
|
|
2024-07-03 17:38:43 +00:00
|
|
|
|
<Show when={(session() || loc?.pathname === 'feed') && props.shouts.length}>
|
2024-05-10 16:52:13 +00:00
|
|
|
|
<div class={styles.filtersContainer}>
|
|
|
|
|
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
|
|
|
|
<li
|
|
|
|
|
class={clsx({
|
2024-07-03 17:38:43 +00:00
|
|
|
|
'view-switcher__item--selected': searchParams?.by === 'after' || !searchParams?.by
|
2024-05-10 16:52:13 +00:00
|
|
|
|
})}
|
|
|
|
|
>
|
2024-06-24 17:50:27 +00:00
|
|
|
|
<A href={loc.pathname}>{t('Recent')}</A>
|
2024-05-10 16:52:13 +00:00
|
|
|
|
</li>
|
|
|
|
|
{/*<li>*/}
|
|
|
|
|
{/* <a href="/feed/?by=views">{t('Most read')}</a>*/}
|
|
|
|
|
{/*</li>*/}
|
|
|
|
|
<li
|
|
|
|
|
class={clsx({
|
2024-06-26 08:22:05 +00:00
|
|
|
|
'view-switcher__item--selected': searchParams?.by === 'likes'
|
2024-05-10 16:52:13 +00:00
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<span class="link" onClick={() => changeSearchParams({ by: 'likes' })}>
|
|
|
|
|
{t('Top rated')}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li
|
|
|
|
|
class={clsx({
|
2024-06-26 08:22:05 +00:00
|
|
|
|
'view-switcher__item--selected': searchParams?.by === 'last_comment'
|
2024-05-10 16:52:13 +00:00
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
|
2024-07-03 17:38:43 +00:00
|
|
|
|
{t('Commented')}
|
2024-05-10 16:52:13 +00:00
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<div class={styles.dropdowns}>
|
2024-07-03 17:38:43 +00:00
|
|
|
|
<Show when={searchParams?.by && searchParams?.by !== 'after'}>
|
2024-05-10 16:52:13 +00:00
|
|
|
|
<DropDown
|
|
|
|
|
popupProps={{ horizontalAnchor: 'right' }}
|
2024-07-03 17:38:43 +00:00
|
|
|
|
options={asOptions(['week', 'month', 'year'])}
|
2024-05-10 16:52:13 +00:00
|
|
|
|
currentOption={currentPeriod()}
|
|
|
|
|
triggerCssClass={styles.periodSwitcher}
|
2024-07-03 17:38:43 +00:00
|
|
|
|
onChange={(period: Option) => changeSearchParams({ period: period.value })}
|
2024-05-10 16:52:13 +00:00
|
|
|
|
/>
|
|
|
|
|
</Show>
|
2023-12-20 08:07:57 +00:00
|
|
|
|
<DropDown
|
2024-01-06 04:06:58 +00:00
|
|
|
|
popupProps={{ horizontalAnchor: 'right' }}
|
2024-07-03 17:38:43 +00:00
|
|
|
|
options={asOptions(['followed', 'unrated', 'discussed', 'bookmarked', 'coauthored'])}
|
|
|
|
|
currentOption={asOption(loc.pathname.split('/').pop() || '')}
|
2023-12-20 08:07:57 +00:00
|
|
|
|
triggerCssClass={styles.periodSwitcher}
|
2024-07-03 17:38:43 +00:00
|
|
|
|
onChange={(mode: Option) => navigate(`/feed/${mode.value}`)}
|
2023-12-20 08:07:57 +00:00
|
|
|
|
/>
|
2024-05-10 16:52:13 +00:00
|
|
|
|
</div>
|
2023-12-29 16:43:52 +00:00
|
|
|
|
</div>
|
2023-08-12 14:17:00 +00:00
|
|
|
|
|
2024-05-10 16:52:13 +00:00
|
|
|
|
<Show when={!isLoading()} fallback={<Loading />}>
|
2024-07-03 17:38:43 +00:00
|
|
|
|
<Show when={props.shouts.length > 0}>
|
|
|
|
|
<For each={props.shouts.slice(0, 4)}>
|
2024-05-10 16:52:13 +00:00
|
|
|
|
{(article) => (
|
|
|
|
|
<ArticleCard
|
|
|
|
|
onShare={(shared) => handleShare(shared)}
|
|
|
|
|
onInvite={() => showModal('inviteMembers')}
|
|
|
|
|
article={article}
|
|
|
|
|
settings={{ isFeedMode: true }}
|
|
|
|
|
desktopCoverSize="M"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
|
|
|
|
|
<div class={styles.asideSection}>
|
|
|
|
|
<div class={stylesBeside.besideColumnTitle}>
|
|
|
|
|
<h4>{t('Popular authors')}</h4>
|
2024-07-03 21:25:03 +00:00
|
|
|
|
<a href="/author">
|
2024-05-10 16:52:13 +00:00
|
|
|
|
{t('All authors')}
|
|
|
|
|
<Icon name="arrow-right" class={stylesBeside.icon} />
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<ul class={stylesBeside.besideColumn}>
|
|
|
|
|
<For each={topAuthors().slice(0, 5)}>
|
|
|
|
|
{(author) => (
|
|
|
|
|
<li>
|
|
|
|
|
<AuthorBadge author={author} />
|
|
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
2023-09-24 14:55:46 +00:00
|
|
|
|
|
2024-07-03 17:38:43 +00:00
|
|
|
|
<For each={props.shouts.slice(4)}>
|
2024-05-10 16:52:13 +00:00
|
|
|
|
{(article) => (
|
|
|
|
|
<ArticleCard article={article} settings={{ isFeedMode: true }} desktopCoverSize="M" />
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</Show>
|
2023-09-29 12:48:58 +00:00
|
|
|
|
</Show>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
2023-03-10 17:42:48 +00:00
|
|
|
|
|
2023-09-29 12:48:58 +00:00
|
|
|
|
<aside class={clsx('col-md-7 col-xl-6 offset-xl-1', styles.feedAside)}>
|
2023-12-18 13:22:20 +00:00
|
|
|
|
<Show when={isRightColumnLoaded()}>
|
|
|
|
|
<Show when={topComments().length > 0}>
|
|
|
|
|
<section class={styles.asideSection}>
|
|
|
|
|
<h4>{t('Comments')}</h4>
|
|
|
|
|
<For each={topComments()}>
|
|
|
|
|
{(comment) => {
|
|
|
|
|
return (
|
|
|
|
|
<div class={styles.comment}>
|
|
|
|
|
<div class={clsx('text-truncate', styles.commentBody)}>
|
2024-06-24 17:50:27 +00:00
|
|
|
|
<A
|
|
|
|
|
href={`article/${comment.shout.slug}?commentId=${comment.id}`}
|
|
|
|
|
innerHTML={comment.body || ''}
|
2023-12-18 13:22:20 +00:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class={styles.commentDetails}>
|
2023-12-20 16:54:20 +00:00
|
|
|
|
<AuthorLink author={comment.created_by as Author} size={'XS'} />
|
2023-12-18 13:22:20 +00:00
|
|
|
|
<CommentDate comment={comment} isShort={true} isLastInRow={true} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class={clsx('text-truncate', styles.commentArticleTitle)}>
|
|
|
|
|
<a href={`/${comment.shout.slug}`}>{comment.shout.title}</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
</For>
|
|
|
|
|
</section>
|
|
|
|
|
</Show>
|
2023-09-29 12:48:58 +00:00
|
|
|
|
|
2023-12-18 13:22:20 +00:00
|
|
|
|
<Show when={topTopics().length > 0}>
|
|
|
|
|
<section class={styles.asideSection}>
|
|
|
|
|
<h4>{t('Hot topics')}</h4>
|
|
|
|
|
<For each={topTopics().slice(0, 7)}>
|
|
|
|
|
{(topic) => (
|
|
|
|
|
<span class={clsx(stylesTopic.shoutTopic, styles.topic)}>
|
|
|
|
|
<a href={`/topic/${topic.slug}`}>{topic.title}</a>{' '}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</section>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<section class={clsx(styles.asideSection, styles.pinnedLinks)}>
|
|
|
|
|
<h4>{t('Knowledge base')}</h4>
|
|
|
|
|
<ul class="nodash">
|
|
|
|
|
<li>
|
2024-06-24 17:50:27 +00:00
|
|
|
|
<A href={'/guide'}>Как устроен Дискурс</A>
|
2023-12-18 13:22:20 +00:00
|
|
|
|
</li>
|
|
|
|
|
<li>
|
2024-06-24 17:50:27 +00:00
|
|
|
|
<A href="/how-to-write-a-good-article">Как создать хороший текст</A>
|
2023-12-18 13:22:20 +00:00
|
|
|
|
</li>
|
|
|
|
|
<li>
|
2024-06-24 17:50:27 +00:00
|
|
|
|
<A href="#">Правила конструктивных дискуссий</A>
|
2023-12-18 13:22:20 +00:00
|
|
|
|
</li>
|
|
|
|
|
<li>
|
2024-07-05 19:40:54 +00:00
|
|
|
|
<A href={'/principles'}>Принципы сообщества</A>
|
2023-12-18 13:22:20 +00:00
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
2023-12-14 18:45:50 +00:00
|
|
|
|
</section>
|
2024-07-03 17:38:43 +00:00
|
|
|
|
<Show when={unrated()}>
|
2023-12-18 13:22:20 +00:00
|
|
|
|
<section class={clsx(styles.asideSection)}>
|
|
|
|
|
<h4>{t('Be the first to rate')}</h4>
|
2024-07-03 17:38:43 +00:00
|
|
|
|
<For each={unrated()}>
|
2023-12-18 13:22:20 +00:00
|
|
|
|
{(article) => (
|
|
|
|
|
<ArticleCard article={article} settings={{ noimage: true, nodate: true }} />
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</section>
|
|
|
|
|
</Show>
|
2023-12-14 18:45:50 +00:00
|
|
|
|
</Show>
|
2023-09-29 12:48:58 +00:00
|
|
|
|
</aside>
|
|
|
|
|
</div>
|
2024-01-08 13:02:52 +00:00
|
|
|
|
<Show when={shareData()}>
|
|
|
|
|
<ShareModal
|
2024-06-24 17:50:27 +00:00
|
|
|
|
title={shareData()?.title || ''}
|
|
|
|
|
description={shareData()?.description || ''}
|
|
|
|
|
imageUrl={shareData()?.cover || ''}
|
|
|
|
|
shareUrl={getShareUrl({ pathname: `/${shareData()?.slug || ''}` })}
|
2024-01-08 13:02:52 +00:00
|
|
|
|
/>
|
|
|
|
|
</Show>
|
2024-02-03 05:19:15 +00:00
|
|
|
|
|
|
|
|
|
<Modal variant="medium" name="inviteCoauthors">
|
|
|
|
|
<InviteMembers variant={'coauthors'} title={t('Invite experts')} />
|
|
|
|
|
</Modal>
|
2023-04-29 13:43:50 +00:00
|
|
|
|
</div>
|
2022-09-09 11:53:35 +00:00
|
|
|
|
)
|
|
|
|
|
}
|