one-article-fixes
This commit is contained in:
parent
bfc78d9df3
commit
d255f6f0b1
|
@ -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()}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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[])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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])
|
||||||
|
|
Loading…
Reference in New Issue
Block a user