Skip to content

Commit f1d1fbf

Browse files
authored
Fix #19: Implement complete notification screen with UI, API integration, and tests (#22)
<!-- This will be automatically replaced by a summary generated by CodeRabbitAI --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new notification UI with dynamic icons, relative time formatting, and improved list display. * Added support for handling notification actions and events, including error states. * Implemented a view model to manage notification state and actions. * Added new drawable icons for discussions, issues, and pull requests. * Added a concise relative time display for notifications. * **Improvements** * Enhanced notification data handling to support nullable fields and improved URL mapping. * Refined dependency injection for notification data sources. * Updated notification retrieval to allow filtering of read/unread notifications. * Improved loading state handling and UI feedback during notification fetch. * Optimized HTTP client configuration for network requests. * **Bug Fixes** * Improved robustness by making certain fields nullable to handle missing data gracefully. * **Tests** * Added comprehensive unit tests for relative time string formatting. * **Refactor** * Restructured domain models and updated package organization for clarity and maintainability. * **Documentation** * Updated function and parameter documentation for clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai --> ## Issue Reference * Fixes #19 ## Screenshots <!-- Upload before-and-after screenshots for UI-related changes. Include both light and dark mode views if relevant. --> _TODO_: Add screenshots ## Essential Checklist <!-- Please tick the relevant boxes by putting an "x" in them (and remove additional spaces). --> * [x] The PR title starts with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ..."). * [x] The PR does not contain any unnecessary code changes from Android Studio. * [x] The PR is made from a branch that is **not** called "main" and is up-to-date with "main".
1 parent ce106f8 commit f1d1fbf

File tree

29 files changed

+648
-49
lines changed

29 files changed

+648
-49
lines changed

app/src/androidTest/java/com/notifier/app/core/presentation/notification/FakeNotificationPermissionState.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ package com.notifier.app.core.presentation.notification
1111
*/
1212
class FakeNotificationPermissionState(
1313
override val isGranted: Boolean,
14-
override val shouldShowRationale: Boolean
14+
override val shouldShowRationale: Boolean,
1515
) : NotificationPermissionState {
1616
/** No-op implementation since this is only used in tests. */
17-
override fun requestPermission() { /* No-op for tests */ }
17+
override fun requestPermission() {
18+
/* No-op for tests */
19+
}
1820
}

app/src/main/java/com/notifier/app/core/data/networking/HttpClientFactory.kt

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import io.ktor.client.plugins.logging.ANDROID
1111
import io.ktor.client.plugins.logging.LogLevel
1212
import io.ktor.client.plugins.logging.Logger
1313
import io.ktor.client.plugins.logging.Logging
14+
import io.ktor.client.request.header
1415
import io.ktor.http.ContentType
1516
import io.ktor.http.HttpHeaders
1617
import io.ktor.http.contentType
17-
import io.ktor.http.headers
1818
import io.ktor.serialization.kotlinx.json.json
1919
import kotlinx.coroutines.runBlocking
2020
import kotlinx.serialization.json.Json
@@ -61,19 +61,17 @@ class HttpClientFactory @Inject constructor(
6161
// Set default request headers and properties
6262
defaultRequest {
6363
contentType(ContentType.Application.Json)
64-
}
6564

66-
val accessToken = runBlocking {
67-
var retrievedToken = ""
68-
dataStoreManager.getAccessToken().onSuccess {
69-
retrievedToken = it
65+
val accessToken = runBlocking {
66+
var retrievedToken = ""
67+
dataStoreManager.getAccessToken().onSuccess {
68+
retrievedToken = it
69+
}
70+
return@runBlocking retrievedToken
7071
}
71-
return@runBlocking retrievedToken
72-
}
7372

74-
headers {
75-
append(HttpHeaders.Authorization, "Bearer $accessToken")
76-
append("X-GitHub-Api-Version", "2022-11-28")
73+
header(HttpHeaders.Authorization, "Bearer $accessToken")
74+
header("X-GitHub-Api-Version", "2022-11-28")
7775
}
7876
}
7977
}

app/src/main/java/com/notifier/app/core/presentation/notification/WithNotificationPermission.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ fun WithNotificationPermission(
6767
*/
6868
@Composable
6969
private fun NotificationPermissionHandler(
70-
permissionState: NotificationPermissionState
70+
permissionState: NotificationPermissionState,
7171
) {
7272
val context = LocalContext.current
7373
var hasRequested by rememberSaveable { mutableStateOf(false) }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.notifier.app.core.presentation.util
2+
3+
import java.time.Duration
4+
import java.time.ZonedDateTime
5+
import kotlin.math.abs
6+
7+
fun ZonedDateTime.toRelativeTimeString(): String {
8+
val now = ZonedDateTime.now()
9+
val duration = Duration.between(this, now)
10+
val seconds = duration.seconds
11+
12+
val absSeconds = abs(seconds)
13+
14+
val minute = 60
15+
val hour = 60 * minute
16+
val day = 24 * hour
17+
val week = 7 * day
18+
19+
return when {
20+
absSeconds < minute -> "${absSeconds}s"
21+
absSeconds < hour -> "${absSeconds / minute}m"
22+
absSeconds < day -> "${absSeconds / hour}h"
23+
absSeconds < week -> "${absSeconds / day}d"
24+
else -> "${absSeconds / week}w"
25+
}
26+
}

app/src/main/java/com/notifier/app/di/ApiModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.notifier.app.di
33
import com.notifier.app.auth.data.networking.RemoteAuthTokenDataSource
44
import com.notifier.app.auth.domain.AuthTokenDataSource
55
import com.notifier.app.core.data.networking.HttpClientFactory
6+
import com.notifier.app.notification.data.networking.RemoteNotificationDataSource
7+
import com.notifier.app.notification.domain.NotificationDataSource
68
import dagger.Module
79
import dagger.Provides
810
import dagger.hilt.InstallIn
@@ -29,4 +31,12 @@ object ApiModule {
2931
): AuthTokenDataSource {
3032
return RemoteAuthTokenDataSource(httpClient)
3133
}
34+
35+
@Provides
36+
@Singleton
37+
fun provideRemoteNotificationDataSource(
38+
httpClient: HttpClient,
39+
): NotificationDataSource {
40+
return RemoteNotificationDataSource(httpClient)
41+
}
3242
}

app/src/main/java/com/notifier/app/notification/data/mappers/NotificationMapper.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import com.notifier.app.notification.data.networking.dto.OwnerDto
55
import com.notifier.app.notification.data.networking.dto.RepositoryDto
66
import com.notifier.app.notification.data.networking.dto.SubjectDto
77
import com.notifier.app.notification.data.util.toZonedDateTimeOrDefault
8-
import com.notifier.app.notification.domain.Notification
9-
import com.notifier.app.notification.domain.Owner
10-
import com.notifier.app.notification.domain.Repository
11-
import com.notifier.app.notification.domain.Subject
8+
import com.notifier.app.notification.domain.model.Notification
9+
import com.notifier.app.notification.domain.model.Owner
10+
import com.notifier.app.notification.domain.model.Repository
11+
import com.notifier.app.notification.domain.model.Subject
1212

1313
fun NotificationDto.toNotification() = Notification(
1414
id = id,

app/src/main/java/com/notifier/app/notification/data/networking/RemoteNotificationDataSource.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import com.notifier.app.core.domain.util.NetworkError
66
import com.notifier.app.core.domain.util.Result
77
import com.notifier.app.core.domain.util.map
88
import com.notifier.app.notification.data.mappers.toNotification
9-
import com.notifier.app.notification.data.networking.dto.NotificationResponseDto
10-
import com.notifier.app.notification.domain.Notification
9+
import com.notifier.app.notification.data.networking.dto.NotificationDto
1110
import com.notifier.app.notification.domain.NotificationDataSource
11+
import com.notifier.app.notification.domain.model.Notification
1212
import io.ktor.client.HttpClient
1313
import io.ktor.client.request.get
1414

@@ -34,13 +34,18 @@ class RemoteNotificationDataSource(
3434
* @return A [Result] containing either a list of [Notification] objects on success, or a
3535
* [NetworkError] on failure.
3636
*/
37-
override suspend fun getNotifications(): Result<List<Notification>, NetworkError> {
38-
return safeCall<NotificationResponseDto> {
37+
override suspend fun getNotifications(includeRead: Boolean): Result<List<Notification>,
38+
NetworkError> {
39+
return safeCall<List<NotificationDto>> {
3940
httpClient.get(
40-
urlString = constructUrl("/notification")
41-
)
41+
urlString = constructUrl("/notifications")
42+
) {
43+
url {
44+
parameters.append("all", includeRead.toString())
45+
}
46+
}
4247
}.map { response ->
43-
response.data.map { it.toNotification() }
48+
response.map { it.toNotification() }
4449
}
4550
}
4651
}

app/src/main/java/com/notifier/app/notification/data/networking/dto/NotificationDto.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ data class NotificationDto(
88
@SerialName("id")
99
val id: String,
1010
@SerialName("last_read_at")
11-
val lastReadAt: String,
11+
val lastReadAt: String?,
1212
@SerialName("reason")
1313
val reason: String,
1414
@SerialName("repository")

app/src/main/java/com/notifier/app/notification/data/networking/dto/RepositoryDto.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ data class RepositoryDto(
4545
val gitRefsUrl: String,
4646
@SerialName("git_tags_url")
4747
val gitTagsUrl: String,
48-
@SerialName("git_url")
49-
val gitUrl: String,
5048
@SerialName("hooks_url")
5149
val hooksUrl: String,
5250
@SerialName("html_url")
@@ -83,8 +81,6 @@ data class RepositoryDto(
8381
val pullsUrl: String,
8482
@SerialName("releases_url")
8583
val releasesUrl: String,
86-
@SerialName("ssh_url")
87-
val sshUrl: String,
8884
@SerialName("stargazers_url")
8985
val stargazersUrl: String,
9086
@SerialName("statuses_url")

app/src/main/java/com/notifier/app/notification/data/networking/dto/SubjectDto.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
66
@Serializable
77
data class SubjectDto(
88
@SerialName("latest_comment_url")
9-
val latestCommentUrl: String,
9+
val latestCommentUrl: String?,
1010
@SerialName("title")
1111
val title: String,
1212
@SerialName("type")

0 commit comments

Comments
 (0)