diff --git a/src/components/Feed/Placeholder/Placeholder.tsx b/src/components/Feed/Placeholder/Placeholder.tsx
index 26569bba..a1f60fd3 100644
--- a/src/components/Feed/Placeholder/Placeholder.tsx
+++ b/src/components/Feed/Placeholder/Placeholder.tsx
@@ -51,7 +51,7 @@ const data: PlaceholderData = {
text: 'Placeholder feedDiscussions',
buttonLabelAuthor: 'Current discussions',
buttonLabelFeed: 'Enter',
- href: '/feed?by=last_comment'
+ href: '/feed/hot'
},
author: {
image: 'placeholder-join.webp',
@@ -71,7 +71,7 @@ const data: PlaceholderData = {
header: 'Join discussions',
text: 'Placeholder feedDiscussions',
buttonLabel: 'Go to discussions',
- href: '/feed?by=last_comment',
+ href: '/feed/hot',
profileLinks: [
{
href: '/debate',
diff --git a/src/components/Nav/Header/Header.tsx b/src/components/Nav/Header/Header.tsx
index 5b65f7e1..aae2e336 100644
--- a/src/components/Nav/Header/Header.tsx
+++ b/src/components/Nav/Header/Header.tsx
@@ -500,14 +500,14 @@ export const Header = (props: Props) => {
-
+ {/*
{t('Bookmarks')}
-
+ */}
diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx
index 00f64d31..69d32729 100644
--- a/src/components/Views/Feed/Feed.tsx
+++ b/src/components/Views/Feed/Feed.tsx
@@ -1,6 +1,6 @@
import { A, createAsync, useLocation, useNavigate, useSearchParams } from '@solidjs/router'
import { clsx } from 'clsx'
-import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
+import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { DropDown } from '~/components/_shared/DropDown'
import { Option } from '~/components/_shared/DropDown/DropDown'
import { Icon } from '~/components/_shared/Icon'
@@ -18,7 +18,7 @@ import { useUI } from '~/context/ui'
import { loadUnratedShouts } from '~/graphql/api/private'
import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
import { byCreated } from '~/lib/sort'
-import { FeedSearchParams } from '~/routes/feed/(feed)'
+import { FeedSearchParams } from '~/routes/feed/[...order]'
import { CommentDate } from '../../Article/CommentDate'
import { getShareUrl } from '../../Article/SharePopup'
import { AuthorBadge } from '../../Author/AuthorBadge'
@@ -36,6 +36,7 @@ export type PeriodType = 'week' | 'month' | 'year'
export type FeedProps = {
shouts?: Shout[]
+ mode?: '' | 'likes' | 'hot'
}
export const FeedView = (props: FeedProps) => {
@@ -53,7 +54,7 @@ export const FeedView = (props: FeedProps) => {
const [isLoading, setIsLoading] = createSignal(false)
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
const { session } = useSession()
- const { nonfeaturedFeed, setNonFeaturedFeed } = useFeed()
+ const { feed, setFeed } = useFeed()
const { loadReactionsBy } = useReactions()
const { topTopics } = useTopics()
const { topAuthors } = useAuthors()
@@ -67,20 +68,13 @@ export const FeedView = (props: FeedProps) => {
setTopComments(comments.sort(byCreated).reverse())
}
- onMount(
- () =>
- props.shouts &&
- Array.isArray(props.shouts) &&
- setNonFeaturedFeed((prev) => [...prev, ...(props.shouts || [])]) && console.info(nonfeaturedFeed())
- )
-
createEffect(
on(
- () => nonfeaturedFeed(),
+ feed,
(sss?: Shout[]) => {
if (sss && Array.isArray(sss)) {
setIsLoading(true)
- setNonFeaturedFeed((prev) => [...prev, ...sss])
+ setFeed((prev) => [...prev, ...sss])
Promise.all([
loadTopComments(),
loadReactionsBy({ by: { shouts: sss.map((s: Shout) => s.slug) } })
@@ -113,40 +107,33 @@ export const FeedView = (props: FeedProps) => {
-
+
-
+
{
navigate(`/feed/${mode.value}`)}
@@ -166,8 +153,8 @@ export const FeedView = (props: FeedProps) => {
}>
-
0}>
-
+ 0}>
+
{(article) => (
handleShare(shared)}
@@ -199,7 +186,7 @@ export const FeedView = (props: FeedProps) => {
-
+
{(article) => (
)}
diff --git a/src/components/_shared/LoadMoreWrapper.tsx b/src/components/_shared/LoadMoreWrapper.tsx
index 43403fba..130a2401 100644
--- a/src/components/_shared/LoadMoreWrapper.tsx
+++ b/src/components/_shared/LoadMoreWrapper.tsx
@@ -4,17 +4,17 @@ import { useLocalize } from '~/context/localize'
import { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
+export type LoadMoreItems = Shout[] | Author[] | Reaction[]
+
type LoadMoreProps = {
- loadFunction: (offset?: number) => void
+ loadFunction: (offset?: number) => Promise
pageSize: number
children: JSX.Element
}
-type Items = Shout[] | Author[] | Reaction[]
-
export const LoadMoreWrapper = (props: LoadMoreProps) => {
const { t } = useLocalize()
- const [items, setItems] = createSignal([])
+ const [items, setItems] = createSignal([])
const [offset, setOffset] = createSignal(0)
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(true)
const [isLoading, setIsLoading] = createSignal(false)
@@ -25,7 +25,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
const newItems = await props.loadFunction(offset())
if (!Array.isArray(newItems)) return
console.debug('[_share] load more items', newItems)
- setItems((prev) => [...prev, ...newItems])
+ setItems((prev) => [...prev, ...newItems] as LoadMoreItems)
setOffset((prev) => prev + props.pageSize)
setIsLoadMoreButtonVisible(newItems.length >= props.pageSize - 1)
setIsLoading(false)
diff --git a/src/context/feed.tsx b/src/context/feed.tsx
index cb055318..7d1617aa 100644
--- a/src/context/feed.tsx
+++ b/src/context/feed.tsx
@@ -34,9 +34,9 @@ type FeedContextType = {
seen: Accessor<{ [slug: string]: number }>
addSeen: (slug: string) => void
- // featured
- nonfeaturedFeed: Accessor
- setNonFeaturedFeed: Setter
+ // all
+ feed: Accessor
+ setFeed: Setter
// featured
featuredFeed: Accessor
@@ -62,7 +62,7 @@ export const useFeed = () => useContext(FeedContext)
export const FeedProvider = (props: { children: JSX.Element }) => {
const [sortedFeed, setSortedFeed] = createSignal([])
const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({})
- const [nonfeaturedFeed, setNonFeaturedFeed] = createSignal([])
+ const [feed, setFeed] = createSignal([])
const [featuredFeed, setFeaturedFeed] = createSignal([])
const [expoFeed, setExpoFeed] = createSignal([])
const [topFeed, setTopFeed] = createSignal([])
@@ -260,8 +260,8 @@ export const FeedProvider = (props: { children: JSX.Element }) => {
setFeaturedFeed,
expoFeed,
setExpoFeed,
- nonfeaturedFeed,
- setNonFeaturedFeed
+ feed,
+ setFeed
}}
>
{props.children}
diff --git a/src/intl/locales/ru/translation.json b/src/intl/locales/ru/translation.json
index 93b9aad2..2198e455 100644
--- a/src/intl/locales/ru/translation.json
+++ b/src/intl/locales/ru/translation.json
@@ -263,6 +263,7 @@
"Lists": "Списки",
"Literature": "Литература",
"Load more": "Показать ещё",
+ "loaded": "загружено",
"Loading": "Загрузка",
"Login and security": "Вход и безопасность",
"Logout": "Выход",
diff --git a/src/routes/(main).tsx b/src/routes/(main).tsx
index d511d792..abaf23c0 100644
--- a/src/routes/(main).tsx
+++ b/src/routes/(main).tsx
@@ -1,12 +1,10 @@
import { type RouteDefinition, type RouteSectionProps, createAsync } from '@solidjs/router'
import { Show, createEffect } from 'solid-js'
-import { LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
+import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { useFeed } from '~/context/feed'
import { useTopics } from '~/context/topics'
import { loadShouts, loadTopics } from '~/graphql/api/public'
import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
-import { byStat } from '~/lib/sort'
-import { SortFunction } from '~/types/common'
import { HomeView, HomeViewProps } from '../components/Views/Home'
import { Loading } from '../components/_shared/Loading'
import { PageLayout } from '../components/_shared/PageLayout'
@@ -74,7 +72,6 @@ export const route = {
} satisfies RouteDefinition
export default function HomePage(props: RouteSectionProps) {
- const { addTopics } = useTopics()
const { t } = useLocalize()
const {
setFeaturedFeed,
@@ -85,46 +82,38 @@ export default function HomePage(props: RouteSectionProps) {
topFeed: topRatedFeed
} = useFeed()
- const data = createAsync(async (prev?: HomeViewProps) => {
- const topics = props.data?.topics || (await fetchAllTopics())
- const offset = prev?.featuredShouts?.length || 0
- const featuredShoutsLoader = featuredLoader(offset)
- const loaded = await featuredShoutsLoader()
- setFeaturedFeed((prev) => [...prev, ...loaded||[]])
- const featuredShouts = [
- ...(prev?.featuredShouts || []),
- ...(loaded || props.data?.featuredShouts || [])
- ]
- const sortFn = byStat('viewed')
- const topViewedShouts = featuredShouts.sort(sortFn as SortFunction)
- return {
- ...prev,
- ...props.data,
- topViewedShouts,
- featuredShouts,
- topics
- }
- })
-
+ // preload all topics
+ const { addTopics, sortedTopics } = useTopics()
createEffect(() => {
- if (data()?.topics) {
- console.debug('[routes.main] topics update')
- addTopics(data()?.topics || [])
- }
+ !sortedTopics() && props.data.topics && addTopics(props.data.topics)
})
+ // load more faetured shouts
const loadMoreFeatured = async (offset?: number) => {
const shoutsLoader = featuredLoader(offset)
const loaded = await shoutsLoader()
loaded && setFeaturedFeed((prev: Shout[]) => [...prev, ...loaded])
+ return loaded as LoadMoreItems
}
+
+ // preload featured shouts
+ const shouts = createAsync(async () => {
+ if (props.data.featuredShouts) {
+ setFeaturedFeed(props.data.featuredShouts)
+ console.debug('[routes.main] featured feed preloaded')
+ return props.data.featuredShouts
+ }
+ return await loadMoreFeatured()
+ })
+
const SHOUTS_PER_PAGE = 20
+
return (
0} fallback={}>
{
return Math.floor(d.getTime() / 1000)
}
-const fetchPublishedShouts = async (offset?: number, _client?: Client) => {
- const shoutsLoader = loadShouts({ filters: { featured: undefined }, limit: SHOUTS_PER_PAGE, offset })
+const feedLoader = async (options: Partial, _client?: Client) => {
+ const shoutsLoader = loadShouts({ ...options, limit: SHOUTS_PER_PAGE } as LoadShoutsOptions)
return await shoutsLoader()
}
export const route = {
load: async ({ location: { query } }: RouteSectionProps<{ articles: Shout[] }>) => {
const offset: number = Number.parseInt(query.offset, 10)
- const result = await fetchPublishedShouts(offset)
+ const result = await feedLoader({ offset })
return result
}
}
-export default (props: RouteSectionProps) => {
- const [searchParams] = useSearchParams()
+export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => {
+ const [searchParams] = useSearchParams() // ?period=month
const { t } = useLocalize()
- const {setNonFeaturedFeed} = useFeed()
- const [offset, setOffset] = createSignal(0)
- const loadMore = async () => {
- const newOffset = offset() + SHOUTS_PER_PAGE
- setOffset(newOffset)
+ const { setFeed } = useFeed()
+
+ // preload all topics
+ const { addTopics, sortedTopics } = useTopics()
+ createEffect(() => {
+ !sortedTopics() && props.data.topics && addTopics(props.data.topics)
+ })
+
+ // load more feed
+ const loadMoreFeed = async (offset?: number) => {
+ // /feed/:order: - select order setting
+ const paramPattern = /^(hot|likes)$/
+ const order =
+ (props.params.order && paramPattern.test(props.params.order)
+ ? props.params.order === 'hot'
+ ? 'last_comment'
+ : props.params.order
+ : 'created_at') || 'created_at'
+
const options: LoadShoutsOptions = {
limit: SHOUTS_PER_PAGE,
- offset: newOffset,
- order_by: searchParams?.by
+ offset,
+ order_by: order
}
- if (searchParams?.by === 'after') {
- const period = searchParams?.by || 'month'
+ // ?period=month - time period filter
+ if (searchParams?.period) {
+ const period = searchParams?.period || 'month'
options.filters = { after: getFromDate(period as FeedPeriod) }
}
- const result = await fetchPublishedShouts(newOffset)
- result && setNonFeaturedFeed(result)
- return
+
+ const loaded = await feedLoader(options)
+ loaded && setFeed((prev: Shout[]) => [...prev, ...loaded])
+ return loaded as LoadMoreItems
}
- const shouts = createAsync(async () => props.data || await loadMore())
+
+ // preload shouts
+ const shouts = createAsync(async () => {
+ if (props.data.shouts) {
+ setFeed(props.data.shouts)
+ console.debug('[routes.main] feed preloaded')
+ return props.data.shouts
+ }
+ return (await loadMoreFeed()) as Shout[]
+ })
return (
) => {
key="feed"
desc="Independent media project about culture, science, art and society with horizontal editing"
>
-
+
-
+
diff --git a/src/routes/feed/my/[...mode]/[...order].tsx b/src/routes/feed/my/[...mode]/[...order].tsx
new file mode 100644
index 00000000..91cf0e10
--- /dev/null
+++ b/src/routes/feed/my/[...mode]/[...order].tsx
@@ -0,0 +1,114 @@
+import { RouteSectionProps, useSearchParams } from '@solidjs/router'
+import { createEffect } from 'solid-js'
+import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
+import { Feed } from '~/components/Views/Feed'
+import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
+import { PageLayout } from '~/components/_shared/PageLayout'
+import { useFeed } from '~/context/feed'
+import { useGraphQL } from '~/context/graphql'
+import { useLocalize } from '~/context/localize'
+import { ReactionsProvider } from '~/context/reactions'
+import { useTopics } from '~/context/topics'
+import {
+ loadCoauthoredShouts,
+ loadDiscussedShouts,
+ loadFollowedShouts,
+ loadUnratedShouts
+} from '~/graphql/api/private'
+import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
+
+const feeds = {
+ followed: loadFollowedShouts,
+ discussed: loadDiscussedShouts,
+ coauthored: loadCoauthoredShouts,
+ unrated: loadUnratedShouts
+}
+
+export type FeedPeriod = 'week' | 'month' | 'year'
+export type FeedSearchParams = { period?: FeedPeriod }
+
+const getFromDate = (period: FeedPeriod): number => {
+ const now = new Date()
+ let d: Date = now
+ switch (period) {
+ case 'week': {
+ d = new Date(now.setDate(now.getDate() - 7))
+ break
+ }
+ case 'month': {
+ d = new Date(now.setMonth(now.getMonth() - 1))
+ break
+ }
+ case 'year': {
+ d = new Date(now.setFullYear(now.getFullYear() - 1))
+ break
+ }
+ }
+ return Math.floor(d.getTime() / 1000)
+}
+
+// /feed/my/followed/hot
+
+export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => {
+ const [searchParams] = useSearchParams() // ?period=month
+ const { t } = useLocalize()
+ const { setFeed } = useFeed()
+ // TODO: use const { requireAuthentication } = useSession()
+ const client = useGraphQL()
+
+ // preload all topics
+ const { addTopics, sortedTopics } = useTopics()
+ createEffect(() => {
+ !sortedTopics() && props.data.topics && addTopics(props.data.topics)
+ })
+
+ // load more my feed
+ const loadMoreMyFeed = async (offset?: number) => {
+ // /feed/my/:mode:
+ const paramModePattern = /^(followed|discussed|liked|coauthored|unrated)$/
+ const mode =
+ props.params.mode && paramModePattern.test(props.params.mode) ? props.params.mode : 'followed'
+ const gqlHandler = feeds[mode as keyof typeof feeds]
+
+ // /feed/my/:mode:/:order: - select order setting
+ const paramOrderPattern = /^(hot|likes)$/
+ const order =
+ (paramOrderPattern.test(props.params.order)
+ ? props.params.order === 'hot'
+ ? 'last_comment'
+ : props.params.order
+ : 'created_at') || 'created_at'
+
+ const options: LoadShoutsOptions = {
+ limit: 20,
+ offset,
+ order_by: order
+ }
+
+ // ?period=month - time period filter
+ if (searchParams?.period) {
+ const period = searchParams?.period || 'month'
+ options.filters = { after: getFromDate(period as FeedPeriod) }
+ }
+
+ const shoutsLoader = gqlHandler(client, options)
+ const loaded = await shoutsLoader()
+ loaded && setFeed((prev: Shout[]) => [...prev, ...loaded])
+ return loaded as LoadMoreItems
+ }
+
+ return (
+
+
+
+
+
+
+
+ )
+}