Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/src/main/graphql/pub/hackers/android/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,35 @@ fragment SharedPostFields on Post {
}
}

fragment NewsStoryFields on PostLink {
id
uuid
title
description
url
siteName
author
score
postCount
discussionCount
firstSharedAt
latestActivityAt
sourceBreakdown {
local
remote
bluesky
}
image {
url
alt
width
height
}
creator {
...ActorFields
}
}

query PublicTimeline($first: Int, $after: String, $before: String, $last: Int, $languages: [Locale!]) {
publicTimeline(first: $first, after: $after, before: $before, last: $last, languages: $languages) {
edges {
Expand Down Expand Up @@ -209,6 +238,23 @@ query PersonalTimeline($first: Int, $after: String, $before: String, $last: Int,
}
}

query NewsStories($first: Int, $after: String, $order: NewsOrder) {
newsStories(first: $first, after: $after, order: $order) {
edges {
cursor
node {
...NewsStoryFields
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}

query Viewer {
viewer {
id
Expand Down
44 changes: 44 additions & 0 deletions app/src/main/graphql/pub/hackers/android/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -1358,24 +1358,54 @@ type PostEngagementStats {
shares: Int!
}

enum NewsOrder {
ALL_TIME

NEWEST

POPULAR
}

type NewsSourceBreakdown {
bluesky: Int!

local: Int!

remote: Int!
}

type PostLink implements Node {
author: String

creator: Actor

description: String

discussionCount: Int!

firstSharedAt: DateTime

id: ID!

image: PostLinkImage

latestActivityAt: DateTime

postCount: Int!

score: Float!

siteName: String

sourceBreakdown: NewsSourceBreakdown!

title: String

type: String

url: URL!

uuid: UUID!
}

type PostLinkImage {
Expand Down Expand Up @@ -1545,6 +1575,8 @@ type Query {

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

newsStories(after: String, before: String, first: Int, last: Int, order: NewsOrder = POPULAR): QueryNewsStoriesConnection!

node(id: ID!): Node

nodes(ids: [ID!]!): [Node]!
Expand Down Expand Up @@ -1589,6 +1621,18 @@ type QueryBookmarksConnectionEdge {
node: Post!
}

type QueryNewsStoriesConnection {
edges: [QueryNewsStoriesConnectionEdge!]!

pageInfo: PageInfo!
}

type QueryNewsStoriesConnectionEdge {
cursor: String!

node: PostLink!
}

type QueryPersonalTimelineConnection {
edges: [QueryPersonalTimelineConnectionEdge!]!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import pub.hackers.android.data.repository.HackersPubRepository
import pub.hackers.android.domain.model.NewsOrder

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

suspend fun HackersPubRepository.newsStoriesPage(
after: String?,
order: NewsOrder,
) = getNewsStories(after = after, refresh = (after == null), order = order)
.map { CursorPage(it.stories, it.endCursor, it.hasNextPage, it.startCursor, it.hasPreviousPage) }

suspend fun HackersPubRepository.postRepliesPage(postId: String, after: String?) =
getPostReplies(postId, after)
.map { CursorPage(it.posts, it.endCursor, it.hasNextPage) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import pub.hackers.android.graphql.LoginByUsernameMutation
import pub.hackers.android.graphql.MarkNotificationsAsReadMutation
import pub.hackers.android.graphql.MediumGeneratedAltTextQuery
import pub.hackers.android.graphql.NotificationsQuery
import pub.hackers.android.graphql.NewsStoriesQuery
import pub.hackers.android.graphql.PersonalTimelineQuery
import pub.hackers.android.graphql.PinPostMutation
import pub.hackers.android.graphql.PostQuotesQuery
Expand Down Expand Up @@ -81,10 +82,12 @@ import pub.hackers.android.graphql.UpdateNoteMutation
import pub.hackers.android.graphql.ViewerQuery
import pub.hackers.android.graphql.type.AccountLinkInput
import pub.hackers.android.graphql.type.CreateNoteMediumInput
import pub.hackers.android.graphql.type.NewsOrder as GqlNewsOrder
import pub.hackers.android.graphql.type.UpdateAccountInput
import pub.hackers.android.graphql.fragment.ActorFields
import pub.hackers.android.graphql.fragment.EngagementStatsFields
import pub.hackers.android.graphql.fragment.MediaFields
import pub.hackers.android.graphql.fragment.NewsStoryFields
import pub.hackers.android.graphql.fragment.PostFields
import pub.hackers.android.graphql.fragment.SharedPostFields
import pub.hackers.android.graphql.type.PostVisibility as GqlPostVisibility
Expand Down Expand Up @@ -237,6 +240,45 @@ class HackersPubRepository @Inject constructor(
}
}

suspend fun getNewsStories(
after: String? = null,
refresh: Boolean = false,
order: NewsOrder = NewsOrder.POPULAR,
): Result<NewsStoriesResult> {
return try {
val response = apolloClient.query(
NewsStoriesQuery(
first = Optional.present(20),
after = Optional.presentIfNotNull(after),
order = Optional.present(order.toGqlNewsOrder()),
)
)
.apply { if (refresh) fetchPolicy(FetchPolicy.NetworkOnly) }
.execute()

if (response.hasErrors()) {
Result.failure(Exception(response.errors?.firstOrNull()?.message ?: "Unknown error"))
} else {
val data = response.data?.newsStories
withContext(Dispatchers.Default) {
Result.success(
NewsStoriesResult(
stories = data?.edges?.map { edge ->
edge.node.newsStoryFields.toNewsStory()
}?.distinctBy { it.id } ?: emptyList(),
hasNextPage = data?.pageInfo?.hasNextPage ?: false,
endCursor = data?.pageInfo?.endCursor,
hasPreviousPage = data?.pageInfo?.hasPreviousPage ?: false,
startCursor = data?.pageInfo?.startCursor,
)
)
}
}
} catch (e: Exception) {
Result.failure(e)
}
}

suspend fun getSuggestedFilterLanguages(): Result<List<String>> {
return try {
val response = apolloClient.query(SuggestedFilterLanguagesQuery()).execute()
Expand Down Expand Up @@ -1796,6 +1838,37 @@ class HackersPubRepository @Inject constructor(
}

// Extension functions to convert GraphQL fragment types to domain models
private fun NewsStoryFields.toNewsStory(): NewsStory {
return NewsStory(
id = id,
uuid = uuid.toString(),
title = title,
description = description,
url = url.toString(),
siteName = siteName,
author = author,
image = image?.let { img ->
PostLinkImage(
url = img.url.toString(),
alt = img.alt,
width = img.width,
height = img.height,
)
},
creator = creator?.actorFields?.toActor(),
score = score,
postCount = postCount,
discussionCount = discussionCount,
firstSharedAt = firstSharedAt?.let { Instant.parse(it.toString()) },
latestActivityAt = latestActivityAt?.let { Instant.parse(it.toString()) },
sourceBreakdown = NewsSourceBreakdown(
local = sourceBreakdown.local,
remote = sourceBreakdown.remote,
bluesky = sourceBreakdown.bluesky,
),
)
}

private fun PostFields.toPost(
sharedPost: Post? = null,
replyTarget: Post? = null,
Expand Down Expand Up @@ -1911,6 +1984,14 @@ class HackersPubRepository @Inject constructor(
}
}

private fun NewsOrder.toGqlNewsOrder(): GqlNewsOrder {
return when (this) {
NewsOrder.POPULAR -> GqlNewsOrder.POPULAR
NewsOrder.NEWEST -> GqlNewsOrder.NEWEST
NewsOrder.ALL_TIME -> GqlNewsOrder.ALL_TIME
}
}

private fun ActorFields.toActor(): Actor {
return Actor(
id = id,
Expand Down
39 changes: 39 additions & 0 deletions app/src/main/java/pub/hackers/android/domain/model/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ data class PostLink(
val creator: Actor?
)

enum class NewsOrder {
POPULAR, NEWEST, ALL_TIME
}

@Immutable
data class NewsSourceBreakdown(
val local: Int,
val remote: Int,
val bluesky: Int
)

@Immutable
data class NewsStory(
val id: String,
val uuid: String,
val title: String?,
val description: String?,
val url: String,
val siteName: String?,
val author: String?,
val image: PostLinkImage?,
val creator: Actor?,
val score: Double,
val postCount: Int,
val discussionCount: Int,
val firstSharedAt: Instant?,
val latestActivityAt: Instant?,
val sourceBreakdown: NewsSourceBreakdown
)

@Immutable
data class Post(
val id: String,
Expand Down Expand Up @@ -278,6 +308,15 @@ data class TimelineResult(
val startCursor: String? = null,
)

@Immutable
data class NewsStoriesResult(
val stories: List<NewsStory>,
val hasNextPage: Boolean,
val endCursor: String?,
val hasPreviousPage: Boolean = false,
val startCursor: String? = null,
)

@Immutable
data class NotificationsResult(
val notifications: List<Notification>,
Expand Down
Loading
Loading