one-article-fixes

This commit is contained in:
Untone 2024-07-09 17:12:13 +03:00
parent bfc78d9df3
commit d255f6f0b1
6 changed files with 68 additions and 56 deletions

View File

@ -1,4 +1,3 @@
// import { install } from 'ga-gtag'
import { createPopper } from '@popperjs/core' import { createPopper } from '@popperjs/core'
import { Link } from '@solidjs/meta' import { Link } from '@solidjs/meta'
import { A, useSearchParams } from '@solidjs/router' import { A, useSearchParams } from '@solidjs/router'
@ -10,7 +9,8 @@ import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui' import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
import type { Author, Maybe, QueryLoad_Reactions_ByArgs, Shout, Topic } from '~/graphql/schema/core.gen' import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
import { processPrepositions } from '~/intl/prepositions'
import { isCyrillic } from '~/intl/translate' import { isCyrillic } from '~/intl/translate'
import { getImageUrl } from '~/lib/getImageUrl' import { getImageUrl } from '~/lib/getImageUrl'
import { MediaItem } from '~/types/mediaitem' import { MediaItem } from '~/types/mediaitem'
@ -18,6 +18,7 @@ import { capitalize } from '~/utils/capitalize'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import { CardTopic } from '../Feed/CardTopic' import { CardTopic } from '../Feed/CardTopic'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup' import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import stylesHeader from '../Nav/Header/Header.module.scss'
import { Modal } from '../Nav/Modal' import { Modal } from '../Nav/Modal'
import { TableOfContents } from '../TableOfContents' import { TableOfContents } from '../TableOfContents'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
@ -28,15 +29,13 @@ import { Popover } from '../_shared/Popover'
import { ShareModal } from '../_shared/ShareModal' import { ShareModal } from '../_shared/ShareModal'
import { ImageSwiper } from '../_shared/SolidSwiper' import { ImageSwiper } from '../_shared/SolidSwiper'
import { VideoPlayer } from '../_shared/VideoPlayer' import { VideoPlayer } from '../_shared/VideoPlayer'
import styles from './Article.module.scss'
import { AudioHeader } from './AudioHeader' import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer' import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree' import { CommentsTree } from './CommentsTree'
import { SharePopup, getShareUrl } from './SharePopup' import { SharePopup, getShareUrl } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl' import { ShoutRatingControl } from './ShoutRatingControl'
import stylesHeader from '../Nav/Header/Header.module.scss'
import styles from './Article.module.scss'
type Props = { type Props = {
article: Shout article: Shout
scrollToComments?: boolean scrollToComments?: boolean
@ -64,6 +63,8 @@ const scrollTo = (el: HTMLElement) => {
} }
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
const COMMENTS_PER_PAGE = 30
const VOTES_PER_PAGE = 50
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>() const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>()
@ -76,9 +77,29 @@ export const FullArticle = (props: Props) => {
const { session, requireAuthentication } = useSession() const { session, requireAuthentication } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author) const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const { addSeen } = useFeed() const { addSeen } = useFeed()
const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000))) const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000)))
const [pages, setPages] = createSignal<Record<string, number>>({})
createEffect(
on(
pages,
async (p: Record<string, number>) => {
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( const canEdit = createMemo(
() => () =>
Boolean(author()?.id) && Boolean(author()?.id) &&
@ -110,14 +131,14 @@ export const FullArticle = (props: Props) => {
if (props.article.media) { if (props.article.media) {
const media = JSON.parse(props.article.media) const media = JSON.parse(props.article.media)
if (media.length > 0) { if (media.length > 0) {
return media[0].body return processPrepositions(media[0].body)
} }
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} }
return props.article.body || '' return processPrepositions(props.article.body) || ''
}) })
const imageUrls = createMemo(() => { const imageUrls = createMemo(() => {
@ -141,13 +162,7 @@ export const FullArticle = (props: Props) => {
return Array.from(imageElements).map((img) => img.src) return Array.from(imageElements).map((img) => img.src)
}) })
const media = createMemo<MediaItem[]>(() => { const media = createMemo<MediaItem[]>(() => JSON.parse(props.article.media || '[]'))
try {
return JSON.parse(props.article.media || '[]')
} catch {
return []
}
})
let commentsRef: HTMLDivElement | undefined let commentsRef: HTMLDivElement | undefined
@ -291,40 +306,25 @@ export const FullArticle = (props: Props) => {
}) })
} }
createEffect( onMount(() => {
on( console.debug(props.article)
() => props.article, setPages((_) => ({comments: 0, rating: 0}))
() => {
updateIframeSizes()
}
)
)
onMount(async () => {
const opts: QueryLoad_Reactions_ByArgs = { by: { shout: props.article.slug }, limit: 999, offset: 0 }
const _rrr = await loadReactionsBy(opts)
addSeen(props.article.slug) addSeen(props.article.slug)
setIsReactionsLoaded(true)
document.title = props.article.title document.title = props.article.title
updateIframeSizes()
window?.addEventListener('resize', updateIframeSizes) window?.addEventListener('resize', updateIframeSizes)
onCleanup(() => window.removeEventListener('resize', updateIframeSizes)) onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
createEffect(() => {
if (props.scrollToComments && commentsRef) {
scrollTo(commentsRef)
}
})
createEffect(() => {
if (searchParams?.scrollTo === 'comments' && commentsRef) {
requestAnimationFrame(() => commentsRef && scrollTo(commentsRef))
changeSearchParams({ scrollTo: undefined })
}
})
}) })
const shareUrl = getShareUrl({ pathname: `/${props.article.slug || ''}` }) 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) => const getAuthorName = (a: Author) =>
lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name
return ( return (
@ -346,7 +346,7 @@ export const FullArticle = (props: Props) => {
<h1>{props.article.title || ''}</h1> <h1>{props.article.title || ''}</h1>
<Show when={props.article.subtitle}> <Show when={props.article.subtitle}>
<h4>{props.article.subtitle || ''}</h4> <h4>{processPrepositions(props.article.subtitle || '')}</h4>
</Show> </Show>
<div class={styles.shoutAuthor}> <div class={styles.shoutAuthor}>
@ -378,7 +378,7 @@ export const FullArticle = (props: Props) => {
</div> </div>
</Show> </Show>
<Show when={props.article.lead}> <Show when={props.article.lead}>
<section class={styles.lead} innerHTML={props.article.lead || ''} /> <section class={styles.lead} innerHTML={processPrepositions(props.article.lead || '')} />
</Show> </Show>
<Show when={props.article.layout === 'audio'}> <Show when={props.article.layout === 'audio'}>
<AudioHeader <AudioHeader
@ -499,7 +499,7 @@ export const FullArticle = (props: Props) => {
title={props.article.title} title={props.article.title}
description={props.article.description || body() || media()[0]?.body} description={props.article.description || body() || media()[0]?.body}
imageUrl={props.article.cover || ''} imageUrl={props.article.cover || ''}
shareUrl={shareUrl} shareUrl={shareUrl()}
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)} onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={ trigger={
@ -600,7 +600,7 @@ export const FullArticle = (props: Props) => {
title={props.article.title} title={props.article.title}
description={props.article.description || body() || media()[0]?.body} description={props.article.description || body() || media()[0]?.body}
imageUrl={props.article.cover || ''} imageUrl={props.article.cover || ''}
shareUrl={shareUrl} shareUrl={shareUrl()}
/> />
</> </>
) )

View File

@ -33,7 +33,21 @@ interface FollowingContextType {
unfollow: (what: FollowingEntity, slug: string) => Promise<void> unfollow: (what: FollowingEntity, slug: string) => Promise<void>
} }
const FollowingContext = createContext<FollowingContextType>({} as FollowingContextType) const defaultFollowing = {
slug: '',
type: 'follow'
} as FollowingData
const FollowingContext = createContext<FollowingContextType>({
following: () => defaultFollowing,
followers: () => [],
loading: () => false,
setFollows: (_follows: AuthorFollowsResult) => undefined,
follows: {},
loadFollows: () => undefined,
follow: (_what: FollowingEntity, _slug: string) => undefined,
unfollow: (_what: FollowingEntity, _slug: string) => undefined
} as unknown as FollowingContextType)
export function useFollowing() { export function useFollowing() {
return useContext(FollowingContext) return useContext(FollowingContext)
@ -51,8 +65,6 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
communities: [] as Community[] communities: [] as Community[]
} }
const defaultFollowing = { slug: '', type: 'follow' } as FollowingData
export const FollowingProvider = (props: { children: JSX.Element }) => { export const FollowingProvider = (props: { children: JSX.Element }) => {
const [loading, setLoading] = createSignal<boolean>(false) const [loading, setLoading] = createSignal<boolean>(false)
const [followers, setFollowers] = createSignal<Author[]>([] as Author[]) const [followers, setFollowers] = createSignal<Author[]>([] as Author[])

View File

@ -40,6 +40,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
const { mutation } = useGraphQL() const { mutation } = useGraphQL()
const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => { const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => {
!opts.by && console.warn('reactions provider got wrong opts')
const fetcher = await loadReactions(opts) const fetcher = await loadReactions(opts)
const result = (await fetcher()) || [] const result = (await fetcher()) || []
console.debug('[context.reactions] loaded', result) console.debug('[context.reactions] loaded', result)

View File

@ -1,5 +1,6 @@
import { cache } from '@solidjs/router' import { cache } from '@solidjs/router'
import { defaultClient } from '~/context/graphql' import { defaultClient } from '~/context/graphql'
import getShoutQuery from '~/graphql/query/core/article-load'
import loadShoutsByQuery from '~/graphql/query/core/articles-load-by' import loadShoutsByQuery from '~/graphql/query/core/articles-load-by'
import loadShoutsSearchQuery from '~/graphql/query/core/articles-load-search' import loadShoutsSearchQuery from '~/graphql/query/core/articles-load-search'
import getAuthorQuery from '~/graphql/query/core/author-by' import getAuthorQuery from '~/graphql/query/core/author-by'
@ -69,10 +70,10 @@ export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
} }
export const getShout = (options: QueryGet_ShoutArgs) => { export const getShout = (options: QueryGet_ShoutArgs) => {
console.debug('[lib.api] get shout options', options) // console.debug('[lib.api] get shout options', options)
return cache( return cache(
async () => { async () => {
const resp = await defaultClient.query(loadReactionsByQuery, { ...options }).toPromise() const resp = await defaultClient.query(getShoutQuery, { ...options }).toPromise()
const result = resp?.data?.get_shout const result = resp?.data?.get_shout
if (result) return result as Shout if (result) return result as Shout
}, },

View File

@ -77,7 +77,7 @@ export default (
<ErrorBoundary fallback={() => <HttpStatusCode code={500} />}> <ErrorBoundary fallback={() => <HttpStatusCode code={500} />}>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Show <Show
when={!article()?.id} when={article()?.id}
fallback={ fallback={
<PageLayout isHeaderFixed={false} hideFooter={true} title={t('Nothing is here')}> <PageLayout isHeaderFixed={false} hideFooter={true} title={t('Nothing is here')}>
<FourOuFourView /> <FourOuFourView />

View File

@ -28,9 +28,7 @@ export const route = {
export default (props: RouteSectionProps<{ articles: Shout[] }>) => { export default (props: RouteSectionProps<{ articles: Shout[] }>) => {
const params = useParams() const params = useParams()
const articles = createAsync( const articles = createAsync(async () => props.data.articles || (await fetchTopicShouts(params.slug)) || [])
async () => props.data.articles || (await fetchTopicShouts(params.slug)) || []
)
const { topicEntities } = useTopics() const { topicEntities } = useTopics()
const { t } = useLocalize() const { t } = useLocalize()
const topic = createMemo(() => topicEntities?.()[params.slug]) const topic = createMemo(() => topicEntities?.()[params.slug])