Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7b958c6
Refactor notification domain: Rename packages and introduce Notificat…
theMr17 Jun 6, 2025
94c53f6
Implement notification feature: Add NotificationItem component, updat…
theMr17 Jun 8, 2025
2393ab3
Remove unused variable in NotificationUi.kt
theMr17 Jun 8, 2025
f68963e
Add isRead property to NotificationUi and NotificationItem, update UI…
theMr17 Jun 8, 2025
44b2024
Add loader on NotificationScreen
theMr17 Jun 9, 2025
64a9368
Add includeRead parameter to getNotifications method and update relat…
theMr17 Jun 14, 2025
508730b
Refactor toRelativeTimeString function for improved readability and a…
theMr17 Jun 14, 2025
42236c8
Add redirectUrl to NotificationUi and related components for improved…
theMr17 Jun 14, 2025
fff72e2
Merge branch 'main' into feat/notification-screen
theMr17 Jun 20, 2025
8efe331
Add discussion icon and update NotificationItem to display badge for …
theMr17 Jun 21, 2025
7a48034
Add discussion icon to NotificationScreen for enhanced visual represe…
theMr17 Jun 21, 2025
615d0fa
Add unit tests for toRelativeTimeString function to ensure accurate t…
theMr17 Jul 7, 2025
8843bba
Refactor Notification components for improved clarity and performance
theMr17 Jul 19, 2025
eea8979
Enhance Notification components with detailed documentation and impro…
theMr17 Jul 22, 2025
2418d17
Add green color resource and update icons to use it for consistent st…
theMr17 Jul 22, 2025
2ada9f0
Enhance NotificationViewModel with comprehensive documentation for be…
theMr17 Jul 22, 2025
ea83647
Enhance NotificationItem and NotificationScreen with detailed documen…
theMr17 Jul 29, 2025
8d68d67
Enhance code consistency by adding missing commas in parameter lists …
theMr17 Jul 29, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ package com.notifier.app.core.presentation.notification
*/
class FakeNotificationPermissionState(
override val isGranted: Boolean,
override val shouldShowRationale: Boolean
override val shouldShowRationale: Boolean,
) : NotificationPermissionState {
/** No-op implementation since this is only used in tests. */
override fun requestPermission() { /* No-op for tests */ }
override fun requestPermission() {
/* No-op for tests */
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import io.ktor.client.plugins.logging.ANDROID
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.header
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
import io.ktor.http.headers
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -61,19 +61,17 @@ class HttpClientFactory @Inject constructor(
// Set default request headers and properties
defaultRequest {
contentType(ContentType.Application.Json)
}

val accessToken = runBlocking {
var retrievedToken = ""
dataStoreManager.getAccessToken().onSuccess {
retrievedToken = it
val accessToken = runBlocking {
var retrievedToken = ""
dataStoreManager.getAccessToken().onSuccess {
retrievedToken = it
}
return@runBlocking retrievedToken
}
return@runBlocking retrievedToken
}

headers {
append(HttpHeaders.Authorization, "Bearer $accessToken")
append("X-GitHub-Api-Version", "2022-11-28")
header(HttpHeaders.Authorization, "Bearer $accessToken")
header("X-GitHub-Api-Version", "2022-11-28")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fun WithNotificationPermission(
*/
@Composable
private fun NotificationPermissionHandler(
permissionState: NotificationPermissionState
permissionState: NotificationPermissionState,
) {
val context = LocalContext.current
var hasRequested by rememberSaveable { mutableStateOf(false) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.notifier.app.core.presentation.util

import java.time.Duration
import java.time.ZonedDateTime
import kotlin.math.abs

fun ZonedDateTime.toRelativeTimeString(): String {
val now = ZonedDateTime.now()
val duration = Duration.between(this, now)
val seconds = duration.seconds

val absSeconds = abs(seconds)

val minute = 60
val hour = 60 * minute
val day = 24 * hour
val week = 7 * day

return when {
absSeconds < minute -> "${absSeconds}s"
absSeconds < hour -> "${absSeconds / minute}m"
absSeconds < day -> "${absSeconds / hour}h"
absSeconds < week -> "${absSeconds / day}d"
else -> "${absSeconds / week}w"
}
}
10 changes: 10 additions & 0 deletions app/src/main/java/com/notifier/app/di/ApiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.notifier.app.di
import com.notifier.app.auth.data.networking.RemoteAuthTokenDataSource
import com.notifier.app.auth.domain.AuthTokenDataSource
import com.notifier.app.core.data.networking.HttpClientFactory
import com.notifier.app.notification.data.networking.RemoteNotificationDataSource
import com.notifier.app.notification.domain.NotificationDataSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -29,4 +31,12 @@ object ApiModule {
): AuthTokenDataSource {
return RemoteAuthTokenDataSource(httpClient)
}

@Provides
@Singleton
fun provideRemoteNotificationDataSource(
httpClient: HttpClient,
): NotificationDataSource {
return RemoteNotificationDataSource(httpClient)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import com.notifier.app.notification.data.networking.dto.OwnerDto
import com.notifier.app.notification.data.networking.dto.RepositoryDto
import com.notifier.app.notification.data.networking.dto.SubjectDto
import com.notifier.app.notification.data.util.toZonedDateTimeOrDefault
import com.notifier.app.notification.domain.Notification
import com.notifier.app.notification.domain.Owner
import com.notifier.app.notification.domain.Repository
import com.notifier.app.notification.domain.Subject
import com.notifier.app.notification.domain.model.Notification
import com.notifier.app.notification.domain.model.Owner
import com.notifier.app.notification.domain.model.Repository
import com.notifier.app.notification.domain.model.Subject

fun NotificationDto.toNotification() = Notification(
id = id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import com.notifier.app.core.domain.util.NetworkError
import com.notifier.app.core.domain.util.Result
import com.notifier.app.core.domain.util.map
import com.notifier.app.notification.data.mappers.toNotification
import com.notifier.app.notification.data.networking.dto.NotificationResponseDto
import com.notifier.app.notification.domain.Notification
import com.notifier.app.notification.data.networking.dto.NotificationDto
import com.notifier.app.notification.domain.NotificationDataSource
import com.notifier.app.notification.domain.model.Notification
import io.ktor.client.HttpClient
import io.ktor.client.request.get

Expand All @@ -34,13 +34,18 @@ class RemoteNotificationDataSource(
* @return A [Result] containing either a list of [Notification] objects on success, or a
* [NetworkError] on failure.
*/
override suspend fun getNotifications(): Result<List<Notification>, NetworkError> {
return safeCall<NotificationResponseDto> {
override suspend fun getNotifications(includeRead: Boolean): Result<List<Notification>,
NetworkError> {
return safeCall<List<NotificationDto>> {
httpClient.get(
urlString = constructUrl("/notification")
)
urlString = constructUrl("/notifications")
) {
url {
parameters.append("all", includeRead.toString())
}
}
}.map { response ->
response.data.map { it.toNotification() }
response.map { it.toNotification() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ data class NotificationDto(
@SerialName("id")
val id: String,
@SerialName("last_read_at")
val lastReadAt: String,
val lastReadAt: String?,
@SerialName("reason")
val reason: String,
@SerialName("repository")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ data class RepositoryDto(
val gitRefsUrl: String,
@SerialName("git_tags_url")
val gitTagsUrl: String,
@SerialName("git_url")
val gitUrl: String,
@SerialName("hooks_url")
val hooksUrl: String,
@SerialName("html_url")
Expand Down Expand Up @@ -83,8 +81,6 @@ data class RepositoryDto(
val pullsUrl: String,
@SerialName("releases_url")
val releasesUrl: String,
@SerialName("ssh_url")
val sshUrl: String,
@SerialName("stargazers_url")
val stargazersUrl: String,
@SerialName("statuses_url")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class SubjectDto(
@SerialName("latest_comment_url")
val latestCommentUrl: String,
val latestCommentUrl: String?,
@SerialName("title")
val title: String,
@SerialName("type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import java.time.ZonedDateTime
* Defaults to [Instant.EPOCH] at the system's default time zone.
* @return The parsed [ZonedDateTime] or the provided default value if parsing fails.
*/
fun String.toZonedDateTimeOrDefault(
fun String?.toZonedDateTimeOrDefault(
default: ZonedDateTime = Instant.EPOCH.atZone(ZoneId.systemDefault()),
): ZonedDateTime {
return try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.notifier.app.notification.domain

import com.notifier.app.core.domain.util.Error
import com.notifier.app.core.domain.util.Result
import com.notifier.app.notification.domain.model.Notification

/**
* Interface defining the data source for fetching notifications.
Expand All @@ -19,7 +20,10 @@ interface NotificationDataSource {
* - A **successful** list of [Notification] objects.
* - A **failure** with an appropriate [Error].
*
* @param includeRead Whether to include read notifications in the result. Defaults to `true`.
* @return A [Result] containing either a list of notifications or an error.
*/
suspend fun getNotifications(): Result<List<Notification>, Error>
suspend fun getNotifications(
includeRead: Boolean = true,
): Result<List<Notification>, Error>
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.notifier.app.notification.domain
package com.notifier.app.notification.domain.model

import java.time.ZonedDateTime

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.notifier.app.notification.domain
package com.notifier.app.notification.domain.model

data class Owner(
val avatarUrl: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.notifier.app.notification.domain
package com.notifier.app.notification.domain.model

data class Repository(
val fork: Boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.notifier.app.notification.domain
package com.notifier.app.notification.domain.model

data class Subject(
val latestCommentUrl: String,
val latestCommentUrl: String?,
val title: String,
val type: String,
val url: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.notifier.app.notification.presentation

/**
* A sealed interface representing different user actions in the Notification screen.
*
* These actions are dispatched based on user interactions with the notification UI,
* such as clicking on a notification or performing bulk actions.
*/
sealed interface NotificationAction
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.notifier.app.notification.presentation

import com.notifier.app.core.domain.util.NetworkError

/**
* A sealed interface representing different notification-related events.
*
* These events are used to trigger UI updates or error messages in response
* to state changes in the Notification screen.
*/
sealed interface NotificationEvent {
/**
* Event that triggers a toast or error message for a network-related error.
*
* This event is fired when a network error occurs while loading or interacting
* with notifications.
*
* @param error The network error that occurred.
*/
data class NetworkErrorEvent(val error: NetworkError) : NotificationEvent
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
package com.notifier.app.notification.presentation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.serialization.Serializable

/**
* Navigation route object for the Notification screen.
*
* This object is used for deep linking and navigation to the Notification screen within the app.
*/
@Serializable
data object NotificationScreen

/**
* The NotificationRoute Composable is the entry point to the Notification screen.
*
* It observes the notification state from the [NotificationViewModel] and renders
* the Notification screen with the latest state.
*
* @param viewModel The instance of [NotificationViewModel] used for managing notification logic
* and state.
*/
@Composable
fun NotificationRoute() {
NotificationScreen()
}
fun NotificationRoute(
viewModel: NotificationViewModel = hiltViewModel(),
) {
val state by viewModel.state.collectAsStateWithLifecycle()

NotificationScreen(state)
}
Loading
Loading