Skip to content

Commit dcee78d

Browse files
authored
Merge pull request #158 from malkoG/codex/home-feed-selector
Add home News feed selector
2 parents f344ee2 + d1df32d commit dcee78d

15 files changed

Lines changed: 1042 additions & 11 deletions

File tree

app/src/main/graphql/pub/hackers/android/operations.graphql

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,35 @@ fragment SharedPostFields on Post {
141141
}
142142
}
143143

144+
fragment NewsStoryFields on PostLink {
145+
id
146+
uuid
147+
title
148+
description
149+
url
150+
siteName
151+
author
152+
score
153+
postCount
154+
discussionCount
155+
firstSharedAt
156+
latestActivityAt
157+
sourceBreakdown {
158+
local
159+
remote
160+
bluesky
161+
}
162+
image {
163+
url
164+
alt
165+
width
166+
height
167+
}
168+
creator {
169+
...ActorFields
170+
}
171+
}
172+
144173
query PublicTimeline($first: Int, $after: String, $before: String, $last: Int, $languages: [Locale!]) {
145174
publicTimeline(first: $first, after: $after, before: $before, last: $last, languages: $languages) {
146175
edges {
@@ -209,6 +238,23 @@ query PersonalTimeline($first: Int, $after: String, $before: String, $last: Int,
209238
}
210239
}
211240

241+
query NewsStories($first: Int, $after: String, $order: NewsOrder) {
242+
newsStories(first: $first, after: $after, order: $order) {
243+
edges {
244+
cursor
245+
node {
246+
...NewsStoryFields
247+
}
248+
}
249+
pageInfo {
250+
hasNextPage
251+
hasPreviousPage
252+
startCursor
253+
endCursor
254+
}
255+
}
256+
}
257+
212258
query Viewer {
213259
viewer {
214260
id

app/src/main/graphql/pub/hackers/android/schema.graphqls

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,24 +1358,54 @@ type PostEngagementStats {
13581358
shares: Int!
13591359
}
13601360

1361+
enum NewsOrder {
1362+
ALL_TIME
1363+
1364+
NEWEST
1365+
1366+
POPULAR
1367+
}
1368+
1369+
type NewsSourceBreakdown {
1370+
bluesky: Int!
1371+
1372+
local: Int!
1373+
1374+
remote: Int!
1375+
}
1376+
13611377
type PostLink implements Node {
13621378
author: String
13631379

13641380
creator: Actor
13651381

13661382
description: String
13671383

1384+
discussionCount: Int!
1385+
1386+
firstSharedAt: DateTime
1387+
13681388
id: ID!
13691389

13701390
image: PostLinkImage
13711391

1392+
latestActivityAt: DateTime
1393+
1394+
postCount: Int!
1395+
1396+
score: Float!
1397+
13721398
siteName: String
13731399

1400+
sourceBreakdown: NewsSourceBreakdown!
1401+
13741402
title: String
13751403

13761404
type: String
13771405

13781406
url: URL!
1407+
1408+
uuid: UUID!
13791409
}
13801410

13811411
type PostLinkImage {
@@ -1545,6 +1575,8 @@ type Query {
15451575

15461576
markdownGuide("The locale for the Markdown guide." locale: Locale!): Document!
15471577

1578+
newsStories(after: String, before: String, first: Int, last: Int, order: NewsOrder = POPULAR): QueryNewsStoriesConnection!
1579+
15481580
node(id: ID!): Node
15491581

15501582
nodes(ids: [ID!]!): [Node]!
@@ -1589,6 +1621,18 @@ type QueryBookmarksConnectionEdge {
15891621
node: Post!
15901622
}
15911623

1624+
type QueryNewsStoriesConnection {
1625+
edges: [QueryNewsStoriesConnectionEdge!]!
1626+
1627+
pageInfo: PageInfo!
1628+
}
1629+
1630+
type QueryNewsStoriesConnectionEdge {
1631+
cursor: String!
1632+
1633+
node: PostLink!
1634+
}
1635+
15921636
type QueryPersonalTimelineConnection {
15931637
edges: [QueryPersonalTimelineConnectionEdge!]!
15941638

app/src/main/java/pub/hackers/android/data/paging/CursorPagingSource.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.paging.PagingConfig
55
import androidx.paging.PagingSource
66
import androidx.paging.PagingState
77
import pub.hackers.android.data.repository.HackersPubRepository
8+
import pub.hackers.android.domain.model.NewsOrder
89

910
/**
1011
* Shape common to every cursor-based paginated endpoint in this codebase.
@@ -107,6 +108,12 @@ suspend fun HackersPubRepository.localTimelinePage(
107108
) = getLocalTimeline(after = after, refresh = (after == null), languages = languages)
108109
.map { CursorPage(it.posts, it.endCursor, it.hasNextPage, it.startCursor, it.hasPreviousPage) }
109110

111+
suspend fun HackersPubRepository.newsStoriesPage(
112+
after: String?,
113+
order: NewsOrder,
114+
) = getNewsStories(after = after, refresh = (after == null), order = order)
115+
.map { CursorPage(it.stories, it.endCursor, it.hasNextPage, it.startCursor, it.hasPreviousPage) }
116+
110117
suspend fun HackersPubRepository.postRepliesPage(postId: String, after: String?) =
111118
getPostReplies(postId, after)
112119
.map { CursorPage(it.posts, it.endCursor, it.hasNextPage) }

app/src/main/java/pub/hackers/android/data/repository/HackersPubRepository.kt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import pub.hackers.android.graphql.LoginByUsernameMutation
4848
import pub.hackers.android.graphql.MarkNotificationsAsReadMutation
4949
import pub.hackers.android.graphql.MediumGeneratedAltTextQuery
5050
import pub.hackers.android.graphql.NotificationsQuery
51+
import pub.hackers.android.graphql.NewsStoriesQuery
5152
import pub.hackers.android.graphql.PersonalTimelineQuery
5253
import pub.hackers.android.graphql.PinPostMutation
5354
import pub.hackers.android.graphql.PostQuotesQuery
@@ -81,10 +82,12 @@ import pub.hackers.android.graphql.UpdateNoteMutation
8182
import pub.hackers.android.graphql.ViewerQuery
8283
import pub.hackers.android.graphql.type.AccountLinkInput
8384
import pub.hackers.android.graphql.type.CreateNoteMediumInput
85+
import pub.hackers.android.graphql.type.NewsOrder as GqlNewsOrder
8486
import pub.hackers.android.graphql.type.UpdateAccountInput
8587
import pub.hackers.android.graphql.fragment.ActorFields
8688
import pub.hackers.android.graphql.fragment.EngagementStatsFields
8789
import pub.hackers.android.graphql.fragment.MediaFields
90+
import pub.hackers.android.graphql.fragment.NewsStoryFields
8891
import pub.hackers.android.graphql.fragment.PostFields
8992
import pub.hackers.android.graphql.fragment.SharedPostFields
9093
import pub.hackers.android.graphql.type.PostVisibility as GqlPostVisibility
@@ -237,6 +240,45 @@ class HackersPubRepository @Inject constructor(
237240
}
238241
}
239242

243+
suspend fun getNewsStories(
244+
after: String? = null,
245+
refresh: Boolean = false,
246+
order: NewsOrder = NewsOrder.POPULAR,
247+
): Result<NewsStoriesResult> {
248+
return try {
249+
val response = apolloClient.query(
250+
NewsStoriesQuery(
251+
first = Optional.present(20),
252+
after = Optional.presentIfNotNull(after),
253+
order = Optional.present(order.toGqlNewsOrder()),
254+
)
255+
)
256+
.apply { if (refresh) fetchPolicy(FetchPolicy.NetworkOnly) }
257+
.execute()
258+
259+
if (response.hasErrors()) {
260+
Result.failure(Exception(response.errors?.firstOrNull()?.message ?: "Unknown error"))
261+
} else {
262+
val data = response.data?.newsStories
263+
withContext(Dispatchers.Default) {
264+
Result.success(
265+
NewsStoriesResult(
266+
stories = data?.edges?.map { edge ->
267+
edge.node.newsStoryFields.toNewsStory()
268+
}?.distinctBy { it.id } ?: emptyList(),
269+
hasNextPage = data?.pageInfo?.hasNextPage ?: false,
270+
endCursor = data?.pageInfo?.endCursor,
271+
hasPreviousPage = data?.pageInfo?.hasPreviousPage ?: false,
272+
startCursor = data?.pageInfo?.startCursor,
273+
)
274+
)
275+
}
276+
}
277+
} catch (e: Exception) {
278+
Result.failure(e)
279+
}
280+
}
281+
240282
suspend fun getSuggestedFilterLanguages(): Result<List<String>> {
241283
return try {
242284
val response = apolloClient.query(SuggestedFilterLanguagesQuery()).execute()
@@ -1796,6 +1838,37 @@ class HackersPubRepository @Inject constructor(
17961838
}
17971839

17981840
// Extension functions to convert GraphQL fragment types to domain models
1841+
private fun NewsStoryFields.toNewsStory(): NewsStory {
1842+
return NewsStory(
1843+
id = id,
1844+
uuid = uuid.toString(),
1845+
title = title,
1846+
description = description,
1847+
url = url.toString(),
1848+
siteName = siteName,
1849+
author = author,
1850+
image = image?.let { img ->
1851+
PostLinkImage(
1852+
url = img.url.toString(),
1853+
alt = img.alt,
1854+
width = img.width,
1855+
height = img.height,
1856+
)
1857+
},
1858+
creator = creator?.actorFields?.toActor(),
1859+
score = score,
1860+
postCount = postCount,
1861+
discussionCount = discussionCount,
1862+
firstSharedAt = firstSharedAt?.let { Instant.parse(it.toString()) },
1863+
latestActivityAt = latestActivityAt?.let { Instant.parse(it.toString()) },
1864+
sourceBreakdown = NewsSourceBreakdown(
1865+
local = sourceBreakdown.local,
1866+
remote = sourceBreakdown.remote,
1867+
bluesky = sourceBreakdown.bluesky,
1868+
),
1869+
)
1870+
}
1871+
17991872
private fun PostFields.toPost(
18001873
sharedPost: Post? = null,
18011874
replyTarget: Post? = null,
@@ -1911,6 +1984,14 @@ class HackersPubRepository @Inject constructor(
19111984
}
19121985
}
19131986

1987+
private fun NewsOrder.toGqlNewsOrder(): GqlNewsOrder {
1988+
return when (this) {
1989+
NewsOrder.POPULAR -> GqlNewsOrder.POPULAR
1990+
NewsOrder.NEWEST -> GqlNewsOrder.NEWEST
1991+
NewsOrder.ALL_TIME -> GqlNewsOrder.ALL_TIME
1992+
}
1993+
}
1994+
19141995
private fun ActorFields.toActor(): Actor {
19151996
return Actor(
19161997
id = id,

app/src/main/java/pub/hackers/android/domain/model/Models.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,36 @@ data class PostLink(
8282
val creator: Actor?
8383
)
8484

85+
enum class NewsOrder {
86+
POPULAR, NEWEST, ALL_TIME
87+
}
88+
89+
@Immutable
90+
data class NewsSourceBreakdown(
91+
val local: Int,
92+
val remote: Int,
93+
val bluesky: Int
94+
)
95+
96+
@Immutable
97+
data class NewsStory(
98+
val id: String,
99+
val uuid: String,
100+
val title: String?,
101+
val description: String?,
102+
val url: String,
103+
val siteName: String?,
104+
val author: String?,
105+
val image: PostLinkImage?,
106+
val creator: Actor?,
107+
val score: Double,
108+
val postCount: Int,
109+
val discussionCount: Int,
110+
val firstSharedAt: Instant?,
111+
val latestActivityAt: Instant?,
112+
val sourceBreakdown: NewsSourceBreakdown
113+
)
114+
85115
@Immutable
86116
data class Post(
87117
val id: String,
@@ -278,6 +308,15 @@ data class TimelineResult(
278308
val startCursor: String? = null,
279309
)
280310

311+
@Immutable
312+
data class NewsStoriesResult(
313+
val stories: List<NewsStory>,
314+
val hasNextPage: Boolean,
315+
val endCursor: String?,
316+
val hasPreviousPage: Boolean = false,
317+
val startCursor: String? = null,
318+
)
319+
281320
@Immutable
282321
data class NotificationsResult(
283322
val notifications: List<Notification>,

0 commit comments

Comments
 (0)