From d53953e40da5e391b15816b66cc413bd159a4c2f Mon Sep 17 00:00:00 2001 From: MahmoudMabrok Date: Sun, 7 Jan 2024 21:50:54 +0200 Subject: [PATCH] feat: add event api and ui --- composeApp/build.gradle.kts | 1 + .../components/BasicUserData.kt | 40 ++++++--- .../githubactivity/components/LabeledData.kt | 33 +++---- .../components/UserEventsData.kt | 67 ++++++++++++++ .../githubactivity/data/GithubService.kt | 44 +++++---- .../githubactivity/data/GithubServiceImp.kt | 89 +++++++++++++------ .../mo3ta/githubactivity/model/Payload.kt | 2 + .../mo3ta/githubactivity/model/UserEvent.kt | 54 +++++++++++ .../mo3ta/githubactivity/model/UserEvents.kt | 8 ++ .../screens/userDetails/UserDetailsScreen.kt | 6 ++ .../userDetails/UserDetailsViewModel.kt | 44 +++++++++ gradle/libs.versions.toml | 1 + 12 files changed, 320 insertions(+), 69 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/UserEventsData.kt create mode 100644 composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/Payload.kt create mode 100644 composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvent.kt create mode 100644 composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvents.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 5fe3e8c..e4f35cd 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -47,6 +47,7 @@ kotlin { implementation(libs.moko.mvvm) implementation(libs.ktor.core) implementation(libs.contentNegotiation) + implementation(libs.logging) implementation(libs.serializationKotlinxJson) implementation(libs.composeIcons.featherIcons) implementation(libs.kotlinx.serialization.json) diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/BasicUserData.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/BasicUserData.kt index ab07861..e2919f7 100644 --- a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/BasicUserData.kt +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/BasicUserData.kt @@ -1,15 +1,19 @@ package tools.mo3ta.githubactivity.components import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,7 +30,14 @@ import tools.mo3ta.githubactivity.model.UserDetails @OptIn(ExperimentalLayoutApi::class) @Composable fun BasicUserData(userDetails: UserDetails?, stars: Int? = 0) { - Column { + Column( + modifier = Modifier + .fillMaxWidth() + .border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 16.dp) + ) { + + userDetails?.avatar_url?.let { KamelImage( asyncPainterResource(it), @@ -51,7 +62,8 @@ fun BasicUserData(userDetails: UserDetails?, stars: Int? = 0) { Text( it, modifier = Modifier.align(Alignment.CenterHorizontally), - style = MaterialTheme.typography.labelSmall + style = MaterialTheme.typography.labelSmall, + color = Color.LightGray ) } userDetails?.email?.let { @@ -69,29 +81,35 @@ fun BasicUserData(userDetails: UserDetails?, stars: Int? = 0) { ) } Spacer(modifier = Modifier.size(16.dp)) - FlowRow { + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { userDetails?.public_repos?.let { LabeledData("Public Repo", it.toString(), modifier = Modifier.wrapContentSize()) } userDetails?.followers?.let { - LabeledData("Follower", it.toString(), modifier = Modifier.weight(1f)) + LabeledData("Follower", it.toString(), modifier = Modifier.wrapContentSize()) } userDetails?.following?.let { - LabeledData("Following", it.toString(), modifier = Modifier.weight(1f)) + LabeledData("Following", it.toString(), modifier = Modifier.wrapContentSize()) } - stars?.let { LabeledData("Stars", it.toString(), modifier = Modifier.weight(1f)) } - - userDetails?.public_gists?.let { - LabeledData("Public Gists", it.toString(), modifier = Modifier.weight(1f)) + stars?.let { + LabeledData( + "Stars", + it.toString(), + modifier = Modifier.wrapContentSize() + ) } userDetails?.company?.let { - LabeledData("Company", it, modifier = Modifier.weight(1f)) + LabeledData("Company", it, modifier = Modifier.wrapContentSize()) } userDetails?.location?.let { - LabeledData("Location", it, modifier = Modifier.weight(1f)) + LabeledData("Location", it, modifier = Modifier.wrapContentSize()) } } } diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/LabeledData.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/LabeledData.kt index fe02454..1deac15 100644 --- a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/LabeledData.kt +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/LabeledData.kt @@ -2,29 +2,32 @@ package tools.mo3ta.githubactivity.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @Composable fun LabeledData(title: String, value: String, modifier: Modifier = Modifier) { - - Column( - modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp) - ) { - Text( - value, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - Text( - title, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.align(Alignment.CenterHorizontally), - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Card(modifier) { + Column( + modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp) + ) { + Text( + value, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text( + title, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.align(Alignment.CenterHorizontally), + color = Color.LightGray + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/UserEventsData.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/UserEventsData.kt new file mode 100644 index 0000000..94cb7e3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/components/UserEventsData.kt @@ -0,0 +1,67 @@ +package tools.mo3ta.githubactivity.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import tools.mo3ta.githubactivity.model.UserEvents + + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun UserEventsData(events: UserEvents?) { + events?.let { + Column( + modifier = Modifier + .fillMaxWidth() + .border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("UserEvents") + + Spacer(modifier = Modifier.size(16.dp)) + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalArrangement = Arrangement.Center + ) { + LabeledData( + "Total Push", + events.pullRequestOpened.toString(), + modifier = Modifier.wrapContentSize() + ) + LabeledData( + "PR created", + events.pullRequestOpened.toString(), + modifier = Modifier.wrapContentSize() + ) + LabeledData( + "PR Approved", + events.pullRequestApproved.toString(), + modifier = Modifier.wrapContentSize() + ) + LabeledData( + "Review Comments", + events.reviewCommentAdded.toString(), + modifier = Modifier.wrapContentSize() + ) + } + } + + } +} diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubService.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubService.kt index ecc6d31..9a14fcf 100644 --- a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubService.kt +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubService.kt @@ -2,40 +2,52 @@ package tools.mo3ta.githubactivity.data import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.Logging import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import tools.mo3ta.githubactivity.model.RepoDetails import tools.mo3ta.githubactivity.model.UserDetails +import tools.mo3ta.githubactivity.model.UserEvent interface GithubService { suspend fun getUserDetails( - userName: String): UserDetails + userName: String + ): UserDetails suspend fun loadUserRepos( - userName: String, - pageNumber: Int): List + userName: String, + pageNumber: Int + ): List + + suspend fun loadUserEvents( + userName: String, + pageNumber: Int + ): List fun closeClient() companion object { fun create( - githubKey: String, - enterprise: String, - isEnterprise: Boolean = false, + githubKey: String, + enterprise: String, + isEnterprise: Boolean = false, ): GithubService { return GithubServiceImp( - httpClient = HttpClient { - install(ContentNegotiation) { - json(json = Json { - ignoreUnknownKeys = true - }) - } - }, - githubKey, - enterprise, - isEnterprise + httpClient = HttpClient { + install(ContentNegotiation) { + json(json = Json { + ignoreUnknownKeys = true + }) + } + install(Logging) { + level = io.ktor.client.plugins.logging.LogLevel.ALL + } + }, + githubKey, + enterprise, + isEnterprise ) } } diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubServiceImp.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubServiceImp.kt index caf1204..a110bb9 100644 --- a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubServiceImp.kt +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/data/GithubServiceImp.kt @@ -8,50 +8,85 @@ import io.ktor.client.request.parameter import io.ktor.http.HttpHeaders import tools.mo3ta.githubactivity.model.RepoDetails import tools.mo3ta.githubactivity.model.UserDetails +import tools.mo3ta.githubactivity.model.UserEvent import tools.mo3ta.githubactivity.utils.NetworkUtils.prepareUrl class GithubServiceImp( - val httpClient: HttpClient, - val githubKey: String, - enterprise: String, - isEnterprise: Boolean = false + val httpClient: HttpClient, + val githubKey: String, + enterprise: String, + isEnterprise: Boolean = false ) : GithubService { - val baseUrl = prepareUrl(isEnterprise, - enterprise) + val baseUrl = prepareUrl( + isEnterprise, + enterprise + ) override suspend fun getUserDetails( - userName: String): UserDetails { + userName: String + ): UserDetails { return httpClient - .get("https://$baseUrl/users/${userName}") { - headers { - if (githubKey.isNotEmpty()) { - append(HttpHeaders.Authorization, - "Bearer $githubKey") - } + .get("https://$baseUrl/users/${userName}") { + headers { + if (githubKey.isNotEmpty()) { + append( + HttpHeaders.Authorization, + "Bearer $githubKey" + ) } - }.body() + } + }.body() } override suspend fun loadUserRepos( - userName: String, - pageNumber: Int + userName: String, + pageNumber: Int ): List { return httpClient - .get("https://$baseUrl/users/${userName}/repos") { - headers { - if (githubKey.isNotEmpty()) { - append(HttpHeaders.Authorization, - "Bearer $githubKey") - } + .get("https://$baseUrl/users/${userName}/repos") { + headers { + if (githubKey.isNotEmpty()) { + append( + HttpHeaders.Authorization, + "Bearer $githubKey" + ) } - parameter("page", - pageNumber.toString()) - parameter("per_page", - 100.toString()) - }.body() + } + parameter( + "page", + pageNumber.toString() + ) + parameter( + "per_page", + 100.toString() + ) + }.body() } + override suspend fun loadUserEvents(userName: String, pageNumber: Int): List { + return httpClient + .get("https://$baseUrl/users/${userName}/events") { + headers { + if (githubKey.isNotEmpty()) { + append( + HttpHeaders.Authorization, + "Bearer $githubKey" + ) + } + } + parameter( + "page", + pageNumber.toString() + ) + parameter( + "per_page", + 100.toString() + ) + }.body() + } + + override fun closeClient() { httpClient.close() } diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/Payload.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/Payload.kt new file mode 100644 index 0000000..c0915bc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/Payload.kt @@ -0,0 +1,2 @@ +package tools.mo3ta.githubactivity.model + diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvent.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvent.kt new file mode 100644 index 0000000..4a0e2ef --- /dev/null +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvent.kt @@ -0,0 +1,54 @@ +package tools.mo3ta.githubactivity.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserEvent( + val created_at: String? = null, + val payload: Payload? = null, + @SerialName("public") + val isPublic: Boolean? = null, + val type: String + ) { + + fun isPullRequest(): Boolean { + return type == "PullRequestEvent" && payload?.isOpened() ?: false + } + + fun isApproved(): Boolean { + return type == "PullRequestReviewEvent" && payload?.isApproved() ?: false + } + + fun isReviewComment(): Boolean { + return type == "PullRequestReviewCommentEvent" && payload?.isCreated() ?: false + } + + fun isPush(): Boolean { + return type == "PushEvent" + } +} + + +@Serializable +data class Payload(val action: String? = null, val review: Review? = null) { + fun isCreated(): Boolean { + return action == "created" + } + + fun isOpened(): Boolean { + return action == "opened" + } + + + fun isApproved(): Boolean { + return isCreated() && review?.isApproved() ?: false + } +} + +@Serializable +data class Review(val state: String? = "") { + fun isApproved(): Boolean { + return state == "approved" + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvents.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvents.kt new file mode 100644 index 0000000..e40798d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/model/UserEvents.kt @@ -0,0 +1,8 @@ +package tools.mo3ta.githubactivity.model + +data class UserEvents( + val pullRequestOpened: Int = 0, + val reviewCommentAdded: Int = 0, + val pullRequestApproved: Int = 0, + val pushCounts: Int = 0, + ) diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsScreen.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsScreen.kt index 5585e15..70b7c90 100644 --- a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsScreen.kt @@ -3,6 +3,7 @@ package tools.mo3ta.githubactivity.screens.userDetails import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -17,6 +18,7 @@ import dev.icerock.moko.mvvm.compose.viewModelFactory import tools.mo3ta.githubactivity.components.BasicUserData import tools.mo3ta.githubactivity.components.Loading import tools.mo3ta.githubactivity.components.RepoList +import tools.mo3ta.githubactivity.components.UserEventsData import tools.mo3ta.githubactivity.data.GithubService data class UserDetailsScreenData( @@ -48,6 +50,7 @@ data class UserDetailsScreen( Column( modifier = Modifier.fillMaxSize() + .padding(16.dp) .verticalScroll(rememberScrollState()) ) { if (uiState.isLoading) { @@ -56,6 +59,9 @@ data class UserDetailsScreen( BasicUserData( uiState.userData, uiState.totalStars.takeIf { it > 0 }) + Spacer(modifier = Modifier.size(16.dp)) + + UserEventsData(uiState.userEvents) Spacer(modifier = Modifier.size(16.dp)) diff --git a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsViewModel.kt b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsViewModel.kt index 9ee788e..2d88038 100644 --- a/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/tools/mo3ta/githubactivity/screens/userDetails/UserDetailsViewModel.kt @@ -12,10 +12,13 @@ import kotlinx.coroutines.launch import tools.mo3ta.githubactivity.data.GithubService import tools.mo3ta.githubactivity.model.RepoDetails import tools.mo3ta.githubactivity.model.UserDetails +import tools.mo3ta.githubactivity.model.UserEvent +import tools.mo3ta.githubactivity.model.UserEvents data class UserDetailsUiState( val isLoading: Boolean = false, val userData: UserDetails? = null, + val userEvents: UserEvents? = null, val repos: List = emptyList() ) { val totalStars = repos.sumOf { @@ -61,6 +64,7 @@ class UserDetailsViewModel( repos = allRepos.await() .sortedByDescending { it.stargazers_count }) } + loadExtraData() } catch (_: CancellationException) { _uiState.update { @@ -82,6 +86,29 @@ class UserDetailsViewModel( } } + private fun loadExtraData() { + viewModelScope.launch { + val userEvents = async { loadAllEvents() }.await() + + _uiState.update { + it.copy(userEvents = processEvents(userEvents)) + } + } + } + + private fun processEvents(userEvents: List): UserEvents { + val pullRequestOpened = userEvents.count { it.isPullRequest() } + val pullRequestApproved = userEvents.count { it.isApproved() } + val reviewComments = userEvents.count { it.isReviewComment() } + val pushes = userEvents.count { it.isPush() } + return UserEvents( + pullRequestOpened = pullRequestOpened, + pullRequestApproved = pullRequestApproved, + reviewCommentAdded = reviewComments, + pushCounts = pushes + ) + } + suspend fun loadAllRepos(): List { val allRepos = mutableListOf() var pageNumber = 1 @@ -101,6 +128,23 @@ class UserDetailsViewModel( return allRepos } + suspend fun loadAllEvents(): List { + val userEvents = mutableListOf() + var pageNumber = 1 + var events = githubApi.loadUserEvents(userName, pageNumber) + userEvents.addAll(events) + try { + while (events.isNotEmpty()) { + pageNumber += 1 + events = githubApi.loadUserEvents(userName, pageNumber) + userEvents.addAll(events) + } + } finally { + return userEvents + } + } + + override fun onCleared() { githubApi.closeClient() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10c9bde..4b56ede 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } serializationKotlinxJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } composeIcons-featherIcons = { module = "br.com.devsrsouza.compose.icons:feather", version.ref = "composeIcons" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }