Skip to content

Migrated Notification Module to KMP #1799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ that can be used as a dependency in any other wallet based project. It is develo
| :feature:kyc | Done | ✅ | ✅ | ❔ | ✅ | ❔ |
| :feature:make-transfer | Not started | ❌ | ❌ | ❌ | ❌ | ❌ |
| :feature:merchants | Not started | ❌ | ❌ | ❌ | ❌ | ❌ |
| :feature:notification | Not started | ❌ | | | | |
| :feature:notification | Done | ✅ | | | | |
| :feature:qr | Not started | ❌ | ❌ | ❌ | ❌ | ❌ |
| :feature:receipt | Not started | ❌ | ❌ | ❌ | ❌ | ❌ |
| :feature:request-money | Not started | ❌ | ❌ | ❌ | ❌ | ❌ |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,147 @@ object DateHelper {
return instant.format(shortMonthFormat)
}

/**
* Handles the specific format "yyyy-MM-dd HH:mm:ss.SSSSSS"
* For example "2024-09-19 05:41:18.558995"
* Possible outputs depending on current date:
* "Today at 05:41"
* "Tomorrow at 05:41"
*/
fun String.toFormattedDateTime(): String {
// Parse the datetime string
val dateTime = try {
// Split into date and time parts
val (datePart, timePart) = this.split(" ")
// Remove microseconds from time part
val simplifiedTime = timePart.split(".")[0]
// Combine date and simplified time
val isoString = "${datePart}T$simplifiedTime"
// Parse to LocalDateTime
LocalDateTime.parse(isoString)
} catch (e: Exception) {
return this // Return original string if parsing fails
}

val timeZone = TimeZone.currentSystemDefault()
val now = Clock.System.now()
val nowDateTime = now.toLocalDateTime(timeZone)

return when {
// Same year
nowDateTime.year == dateTime.year -> {
when {
// Same month
nowDateTime.monthNumber == dateTime.monthNumber -> {
when {
// Tomorrow
dateTime.dayOfMonth - nowDateTime.dayOfMonth == 1 -> {
"Tomorrow at ${dateTime.format()}"
}
// Today
dateTime.dayOfMonth == nowDateTime.dayOfMonth -> {
"Today at ${dateTime.format()}"
}
// Yesterday
nowDateTime.dayOfMonth - dateTime.dayOfMonth == 1 -> {
"Yesterday at ${dateTime.format()}"
}
// Same month but different day
else -> {
"${
dateTime.month.name.lowercase().capitalize()
} ${dateTime.dayOfMonth}, ${dateTime.format()}"
}
}
}
// Different month, same year
else -> {
"${
dateTime.month.name.lowercase().capitalize()
} ${dateTime.dayOfMonth}, ${dateTime.format()}"
}
}
}
// Different year
else -> {
"${
dateTime.month.name.lowercase().capitalize()
} ${dateTime.dayOfMonth} ${dateTime.year}, ${dateTime.format()}"
}
}
}

/**
* Input timestamp string in milliseconds
* Example timestamp "1698278400000"
* Output examples:
* "Today at 12:00"
* "Tomorrow at 15:30"
*/
fun String.toPrettyDate(): String {
val timestamp = this.toLong()
val instant = Instant.fromEpochMilliseconds(timestamp)
val timeZone = TimeZone.currentSystemDefault()
val nowDateTime = Clock.System.now().toLocalDateTime(timeZone)
val neededDateTime = instant.toLocalDateTime(timeZone)

return when {
// Same year
nowDateTime.year == neededDateTime.year -> {
when {
// Same month
nowDateTime.monthNumber == neededDateTime.monthNumber -> {
when {
// Tomorrow
neededDateTime.dayOfMonth - nowDateTime.dayOfMonth == 1 -> {
val time = neededDateTime.format()
"Tomorrow at $time"
}
// Today
neededDateTime.dayOfMonth == nowDateTime.dayOfMonth -> {
val time = neededDateTime.format()
"Today at $time"
}
// Yesterday
nowDateTime.dayOfMonth - neededDateTime.dayOfMonth == 1 -> {
val time = neededDateTime.format()
"Yesterday at $time"
}
// Same month but different day
else -> {
"${
neededDateTime.month.name.lowercase().capitalize()
} ${neededDateTime.dayOfMonth}, ${neededDateTime.format()}"
}
}
}
// Different month, same year
else -> {
"${
neededDateTime.month.name.lowercase().capitalize()
} ${neededDateTime.dayOfMonth}, ${neededDateTime.format()}"
}
}
}
// Different year
else -> {
"${
neededDateTime.month.name.lowercase().capitalize()
} ${neededDateTime.dayOfMonth} ${neededDateTime.year}, ${neededDateTime.format()}"
}
}
}

// Helper function to format time
private fun LocalDateTime.format(): String {
return "${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}"
}

// Extension to capitalize first letter
private fun String.capitalize() = replaceFirstChar {
if (it.isLowerCase()) it.titlecase() else it.toString()
}

val currentDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ package org.mifospay.core.data.repository

import kotlinx.coroutines.flow.Flow
import org.mifospay.core.common.DataState
import org.mifospay.core.network.model.NotificationPayload
import org.mifospay.core.model.notification.Notification

interface NotificationRepository {
suspend fun fetchNotifications(clientId: Long): Flow<DataState<List<NotificationPayload>>>
fun fetchNotifications(): Flow<DataState<List<Notification>>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ package org.mifospay.core.data.repositoryImp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.mifospay.core.common.DataState
import org.mifospay.core.common.asDataStateFlow
import org.mifospay.core.data.repository.NotificationRepository
import org.mifospay.core.model.notification.Notification
import org.mifospay.core.network.FineractApiManager
import org.mifospay.core.network.model.NotificationPayload

class NotificationRepositoryImpl(
private val apiManager: FineractApiManager,
private val ioDispatcher: CoroutineDispatcher,
) : NotificationRepository {
override suspend fun fetchNotifications(
clientId: Long,
): Flow<DataState<List<NotificationPayload>>> {
override fun fetchNotifications(): Flow<DataState<List<Notification>>> {
return apiManager.notificationApi
.fetchNotifications(clientId)
.fetchNotifications(true)
.map { it.pageItems }
.asDataStateFlow().flowOn(ioDispatcher)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.model.notification

import kotlinx.serialization.Serializable
import org.mifospay.core.common.DateHelper.toFormattedDateTime
import org.mifospay.core.common.Parcelable
import org.mifospay.core.common.Parcelize

@Serializable
@Parcelize
data class Notification(
val id: Long,
val objectType: String,
val objectId: Long,
val action: String,
val actorId: Long,
val content: String,
val isRead: Boolean,
val isSystemGenerated: Boolean,
val createdAt: String,
) : Parcelable {
val formattedDate = createdAt.toFormattedDateTime()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.network.model
package org.mifospay.core.model.notification

import kotlinx.serialization.Serializable
import org.mifospay.core.common.Parcelable
import org.mifospay.core.common.Parcelize

@Serializable
@Parcelize
data class NotificationPayload(
val title: String? = null,
val body: String? = null,
val timestamp: String? = null,
)
val totalFilteredRecords: Long,
val pageItems: List<Notification>,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
package org.mifospay.core.network.services

import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import kotlinx.coroutines.flow.Flow
import org.mifospay.core.network.model.NotificationPayload
import org.mifospay.core.network.utils.ApiEndPoints
import org.mifospay.core.model.notification.NotificationPayload

interface NotificationService {
@GET(ApiEndPoints.DATATABLES + "/notifications/{clientId}")
suspend fun fetchNotifications(@Path("clientId") clientId: Long): Flow<List<NotificationPayload>>
@GET("notifications/")
fun fetchNotifications(@Query("isRead") isRead: Boolean): Flow<NotificationPayload>
}
18 changes: 13 additions & 5 deletions feature/notification/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins {
alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose)
alias(libs.plugins.mifospay.cmp.feature)
alias(libs.plugins.kotlin.parcelize)
}

android {
namespace = "org.mifospay.notification"
namespace = "org.mifospay.feature.notification"
}

dependencies {
implementation(projects.libs.pullrefresh)
kotlin {
sourceSets {
commonMain.dependencies {
implementation(compose.ui)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
}
}
}
Empty file.
21 changes: 0 additions & 21 deletions feature/notification/proguard-rules.pro

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@ package org.mifospay.feature.notification

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable

const val NOTIFICATION_ROUTE = "notification_route"

fun NavGraphBuilder.notificationScreen() {
fun NavGraphBuilder.notificationScreen(
navigateBack: () -> Unit,
) {
composable(NOTIFICATION_ROUTE) {
NotificationScreen()
NotificationScreen(
navigateBack = navigateBack,
)
}
}

fun NavController.navigateToNotification() {
navigate(NOTIFICATION_ROUTE)
fun NavController.navigateToNotification(navOptions: NavOptions? = null) {
navigate(NOTIFICATION_ROUTE, navOptions)
}
Loading
Loading