Repository: Infomaniak/android-SwissTransfer
Pull request #492: chore: Fix duplicate sections
Author: login: tevincent
Created: 2025-05-05T12:52:53Z | Merged: 2025-05-06T13:08:54Z

depends on https://github.com/Infomaniak/android-SwissTransfer/pull/494


Diff:
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 1438f91b1..2b835be36 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -41,5 +41,9 @@ jobs:
       - name: Clean gradle cache
         run: ./gradlew clean
 
+      # Making sure the app compiles
+      - name: Build app
+        run: ./gradlew assembleDebug
+
       - name: Run unit tests
-        run: ./gradlew testDebugUnitTest --stacktrace
+        run: ./gradlew testProdDebugUnitTest --stacktrace
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 10d482264..b8152b077 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -74,6 +74,7 @@ android {
     }
 
     productFlavors {
+        // Remember to update the command to build the app in android.yml to build the new flavor when you add a new flavor here
         create("preprod") {
             dimension = "env"
             applicationIdSuffix = ".preprod"
diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersGroupingManager.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersGroupingManager.kt
index 8ce797884..9b24a2f39 100644
--- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersGroupingManager.kt
+++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersGroupingManager.kt
@@ -22,13 +22,10 @@ import androidx.annotation.StringRes
 import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.TransferUi
 import com.infomaniak.swisstransfer.R
 import com.infomaniak.swisstransfer.ui.screen.main.transfers.TransfersGroupingManager.TransferSectionWithContains.Companion.isIn
-import java.time.DayOfWeek
-import java.time.Instant
-import java.time.LocalDate
-import java.time.ZoneId
+import java.time.*
 import java.time.format.TextStyle
 import java.time.temporal.TemporalAdjusters
-import java.util.Locale
+import java.util.*
 
 object TransfersGroupingManager {
 
@@ -41,7 +38,10 @@ object TransfersGroupingManager {
                 transfer.isIn(TransferSectionWithContains.ThisWeek, relativeTo = today) -> TransferSectionWithContains.ThisWeek
                 transfer.isIn(TransferSectionWithContains.LastWeek, relativeTo = today) -> TransferSectionWithContains.LastWeek
                 transfer.isIn(TransferSectionWithContains.ThisMonth, relativeTo = today) -> TransferSectionWithContains.ThisMonth
-                else -> TransferSection.ByMonth(transfer.createdDateTimestamp)
+                else -> {
+                    val transferCreatedLocalDate = transfer.createdDateTimestamp.toLocalDate()
+                    TransferSection.ByMonth(transferCreatedLocalDate.year, transferCreatedLocalDate.month.value)
+                }
             }
         }
     }
@@ -49,7 +49,7 @@ object TransfersGroupingManager {
     sealed class TransferSection(val title: Context.() -> String) {
         val uid get() = toString()
 
-        data class ByMonth(private val timestamp: Long) : TransferSection({ getMonthNameFromEpoch(timestamp) })
+        data class ByMonth(private val year: Int, private val month: Int) : TransferSection({ getMonthNameFromEpoch(month) })
     }
 
     sealed class TransferSectionWithContains(
@@ -98,8 +98,7 @@ object TransfersGroupingManager {
     // TODO: Adapt the first day of the week according to the local
     private fun LocalDate.firstDayOfWeek(): LocalDate = with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
 
-    fun getMonthNameFromEpoch(epochSeconds: Long): String {
-        val localDate = epochSeconds.toLocalDate()
-        return localDate.month.getDisplayName(TextStyle.FULL, Locale.getDefault()).replaceFirstChar { it.uppercase() }
+    fun getMonthNameFromEpoch(month: Int): String {
+        return Month.of(month).getDisplayName(TextStyle.FULL, Locale.getDefault()).replaceFirstChar { it.uppercase() }
     }
 }
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 2b16e78a8..62ef20942 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -15,7 +15,7 @@
   ~ You should have received a copy of the GNU General Public License
   ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   -->
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="ImpliedQuantity">
     <string name="advancedSettingsTitle">Paramètres avancés</string>
     <string name="awaitingNetwork">En attente de réseau</string>
     <string name="buttonAddFiles">Ajouter des fichiers</string>
@@ -103,6 +103,7 @@
     <plurals name="recipientHeader">
         <item quantity="one">Destinataire :</item>
         <item quantity="other">Destinataires :</item>
+        <item quantity="many">Destinataires :</item>
     </plurals>
     <string name="restrictedLocation">Infomaniak SwissTransfer n’est pas disponible dans votre région</string>
     <string name="sentEmptyTitle">Notre histoire commence ici</string>
@@ -149,6 +150,7 @@
     <plurals name="settingsValidityPeriodValue">
         <item quantity="one">%d jour</item>
         <item quantity="other">%d jours</item>
+        <item quantity="many">%d jours</item>
     </plurals>
     <string name="sharePasswordButton">Copier le mot de passe</string>
     <string name="sharePasswordDescription">Voilà le sésame pour télécharger les fichiers !</string>
@@ -208,6 +210,7 @@
     <plurals name="uploadSuccessEmailDescription">
         <item quantity="one">Le lien de téléchargement a bien été envoyé au destinataire suivant :</item>
         <item quantity="other">Le lien de téléchargement a été envoyé aux destinataires suivants :</item>
+        <item quantity="many">Le lien de téléchargement a été envoyé aux destinataires suivants :</item>
     </plurals>
     <string name="uploadSuccessEmailTitle">Hop c’est envoyé !</string>
     <string name="uploadSuccessLinkDescription">Fais scanner ce QR code autour de toi pour partager tes fichiers ou copie le lien ci-dessous.</string>
diff --git a/app/src/test/java/com/infomaniak/swisstransfer/TransferSectionGroupingTest.kt b/app/src/test/java/com/infomaniak/swisstransfer/TransferSectionGroupingTest.kt
index da32f7865..13de8360f 100644
--- a/app/src/test/java/com/infomaniak/swisstransfer/TransferSectionGroupingTest.kt
+++ b/app/src/test/java/com/infomaniak/swisstransfer/TransferSectionGroupingTest.kt
@@ -27,12 +27,13 @@ import com.infomaniak.swisstransfer.dataset.TransferSectionGroupingData.today
 import com.infomaniak.swisstransfer.dataset.TransferSectionGroupingData.yesterday
 import com.infomaniak.swisstransfer.ui.screen.main.transfers.TransfersGroupingManager.TransferSection
 import com.infomaniak.swisstransfer.ui.screen.main.transfers.TransfersGroupingManager.TransferSectionWithContains
-import com.infomaniak.swisstransfer.ui.screen.main.transfers.TransfersGroupingManager.getMonthNameFromEpoch
 import com.infomaniak.swisstransfer.ui.screen.main.transfers.TransfersGroupingManager.groupBySection
 import com.infomaniak.swisstransfer.ui.screen.main.transfers.TransfersGroupingManager.toLocalDate
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Test
+import java.time.Instant
+import java.time.ZoneId
 import kotlin.reflect.full.primaryConstructor
 
 class TransferSectionGroupingTest {
@@ -63,7 +64,8 @@ class TransferSectionGroupingTest {
 
     @Test
     fun months_areGroupedCorrectly() {
-        val decemberSection = TransferSection.ByMonth(getMonthNameFromEpoch(december.first()))
+        val yearMonth = december.first().pairOfYearMonth()
+        val decemberSection = TransferSection.ByMonth(yearMonth.first, yearMonth.second)
         assertEpochsAreGroupedInSection(december, decemberSection)
     }
 
@@ -79,7 +81,8 @@ class TransferSectionGroupingTest {
 
     @Test
     fun sections_areOrderCorrectly() {
-        val decemberSection = TransferSection.ByMonth(getMonthNameFromEpoch(december.first()))
+        val yearMonth = december.first().pairOfYearMonth()
+        val decemberSection = TransferSection.ByMonth(yearMonth.first, yearMonth.second)
         assertEquals(groupedTransferUi.keys.elementAt(0), TransferSectionWithContains.Future)
         assertEquals(groupedTransferUi.keys.elementAt(1), TransferSectionWithContains.Today)
         assertEquals(groupedTransferUi.keys.elementAt(2), TransferSectionWithContains.Yesterday)
@@ -106,13 +109,13 @@ class TransferSectionGroupingTest {
                 // Case: data class with one parameter
                 val constructor = subClass.primaryConstructor
                 assertNotNull("This test needs to create instances of this class ${subClass.simpleName}", constructor)
-                assertEquals(1, constructor?.parameters?.count())
+                assertEquals(2, constructor?.parameters?.count())
 
-                val firstInstanceUid = constructor!!.call("aaa").uid
+                val firstInstanceUid = constructor!!.call(1, 1).uid
                 assert(firstInstanceUid !in previousUids)
                 previousUids.add(firstInstanceUid)
 
-                val secondInstanceUid = constructor.call("bbb").uid
+                val secondInstanceUid = constructor.call(2, 2).uid
                 assert(secondInstanceUid !in previousUids)
                 previousUids.add(secondInstanceUid)
             }
@@ -156,5 +159,12 @@ class TransferSectionGroupingTest {
                 files = emptyList(),
             )
         }
+
+        fun Long.pairOfYearMonth(): Pair<Int, Int> {
+            val localDate = Instant.ofEpochSecond(this).atZone(ZoneId.systemDefault()).toLocalDate()
+            val year = localDate.year
+            val month = localDate.month.value
+            return Pair(year, month)
+        }
     }
 }


Reviews:
user: login: LunarX
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:08:31Z
user: login: LunarX
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:58:25Z
user: login: LunarX
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T13:08:37Z

Review comments:
diff_hunk: @@ -41,5 +41,9 @@ jobs:
       - name: Clean gradle cache
         run: ./gradlew clean
 
+      # Making sure the app compiles
+      - name: Build app
+        run: ./gradlew buildProdDebug
path: .github/workflows/android.yml
user: login: LunarX
body: The task seems to have failed because it's not recognized as a valid task
created_at: 2025-05-05T13:08:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,5 +41,9 @@ jobs:
       - name: Clean gradle cache
         run: ./gradlew clean
 
+      # Making sure the app compiles
+      - name: Build app
+        run: ./gradlew assembleProdDebug
+
       - name: Run unit tests
         run: ./gradlew testDebugUnitTest --stacktrace
path: .github/workflows/android.yml
user: login: LunarX
body: Didn't we need to change this command so the tests would run? Otherwise they weren't?
created_at: 2025-05-06T07:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +77,7 @@ android {
     }
 
     productFlavors {
+        // Don't forget to update the command to build the app in android.yml if necessary
path: app/build.gradle.kts
user: login: LunarX
body: ```suggestion
        // Don't forget to update the command to build the app in android.yml to build the new flavor when you add a new flavor here
```
created_at: 2025-05-06T07:46:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,11 +95,21 @@ object TransfersGroupingManager {
 
     fun Long.toLocalDate(): LocalDate = Instant.ofEpochSecond(this).atZone(ZoneId.systemDefault()).toLocalDate()
 
+    fun Long.toStartOfMonthInSeconds(): Long {
+        return Instant
+            .ofEpochSecond(this)
+            .atZone(ZoneId.systemDefault())
+            .withDayOfMonth(1)
+            .withSecond(0)
+            .withMinute(0)
+            .withHour(0)
+            .toEpochSecond()
+    }
path: app/src/main/java/com/infomaniak/swisstransfer/ui/screen/main/transfers/TransfersGroupingManager.kt
user: login: LunarX
body: Do you really need this?
created_at: 2025-05-06T07:48:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,7 +101,7 @@
     <string name="receivedFilesTitle">Fichiers reçus</string>
     <string name="receivedTitle">Reçu</string>
     <plurals name="recipientHeader">
-        <item quantity="one">Destinataire :</item>
+        <item quantity="one" tools:ignore="ImpliedQuantity">Destinataire :</item>
path: app/src/main/res/values-fr/strings.xml
user: login: LunarX
body: Are all of these changes really needed for your fix?
created_at: 2025-05-06T07:50:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,14 +81,15 @@ class TransferSectionGroupingTest {
 
     @Test
     fun sections_areOrderCorrectly() {
-        val decemberSection = TransferSection.ByMonth(getMonthNameFromEpoch(december.first()))
+        val yearMonth = december.first().pairOfYearMonth()
+        val decemberTimestamp = TransferSection.ByMonth(yearMonth.first, yearMonth.second)
path: app/src/test/java/com/infomaniak/swisstransfer/TransferSectionGroupingTest.kt
user: login: LunarX
body: I think the december section should not be called decemberTimestamp
created_at: 2025-05-06T07:51:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,14 +81,15 @@ class TransferSectionGroupingTest {
 
     @Test
     fun sections_areOrderCorrectly() {
-        val decemberSection = TransferSection.ByMonth(getMonthNameFromEpoch(december.first()))
+        val yearMonth = december.first().pairOfYearMonth()
+        val decemberTimestamp = TransferSection.ByMonth(yearMonth.first, yearMonth.second)
         assertEquals(groupedTransferUi.keys.elementAt(0), TransferSectionWithContains.Future)
         assertEquals(groupedTransferUi.keys.elementAt(1), TransferSectionWithContains.Today)
         assertEquals(groupedTransferUi.keys.elementAt(2), TransferSectionWithContains.Yesterday)
         assertEquals(groupedTransferUi.keys.elementAt(3), TransferSectionWithContains.ThisWeek)
         assertEquals(groupedTransferUi.keys.elementAt(4), TransferSectionWithContains.LastWeek)
         assertEquals(groupedTransferUi.keys.elementAt(5), TransferSectionWithContains.ThisMonth)
-        assertEquals(groupedTransferUi.keys.elementAt(6), decemberSection)
+        assertEquals((groupedTransferUi.keys.elementAt(6) as TransferSection.ByMonth), decemberTimestamp)
path: app/src/test/java/com/infomaniak/swisstransfer/TransferSectionGroupingTest.kt
user: login: LunarX
body: I don't think you need this cast as TransferSection.ByMonth
created_at: 2025-05-06T07:52:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,13 +109,13 @@ class TransferSectionGroupingTest {
                 // Case: data class with one parameter
                 val constructor = subClass.primaryConstructor
                 assertNotNull("This test needs to create instances of this class ${subClass.simpleName}", constructor)
-                assertEquals(1, constructor?.parameters?.count())
+                assertEquals(2, constructor?.parameters?.count())
 
-                val firstInstanceUid = constructor!!.call("aaa").uid
+                val firstInstanceUid = constructor!!.call(0, 0).uid
                 assert(firstInstanceUid !in previousUids)
                 previousUids.add(firstInstanceUid)
 
-                val secondInstanceUid = constructor.call("bbb").uid
+                val secondInstanceUid = constructor.call(1, 1).uid
path: app/src/test/java/com/infomaniak/swisstransfer/TransferSectionGroupingTest.kt
user: login: LunarX
body: maybe, just to be sure, we could make it (1, 1) and (2, 2). Just because (0, 0) is actually illegal in the code and it would be a bother if we break a test in the futur by implementing logic that breaks when (0, 0) is provided even though 0 is only provided inside of a test case
created_at: 2025-05-06T07:55:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: tevincent
created_at: 2025-05-05T16:04:32Z
body: @LunarX I had to use `./gradlew assembleProdDebug` because with `./gradlew build`, I had the Sentry related issue. With the `assembleProdDebug`, I don't have this error and if it does not compile, the CI fail so it's perfect.
author_association: CONTRIBUTOR
user: login: LunarX
created_at: 2025-05-06T07:44:55Z
body: Would it be hard to fix the sentry error instead of changing to `assembleProdDebug`?
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-06T09:43:15Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Infomaniak_android-SwissTransfer&pullRequest=492) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=Infomaniak_android-SwissTransfer&pullRequest=492&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Infomaniak_android-SwissTransfer&pullRequest=492&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Infomaniak_android-SwissTransfer&pullRequest=492&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Infomaniak_android-SwissTransfer&pullRequest=492&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Infomaniak_android-SwissTransfer&pullRequest=492&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Infomaniak_android-SwissTransfer&pullRequest=492)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T11:09:17Z
body: This PR/issue depends on:
* ~~Infomaniak/android-SwissTransfer#494~~
By **[Dependent Issues](https://github.com/z0al/dependent-issues)** (🤖). Happy coding!
<!-- By Dependent Issues (Action) - DO NOT REMOVE -->
author_association: NONE